Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add OSM Element Tags widget #2499

Merged
merged 3 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lang/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,9 @@
"Notification.type.revisionCount": "Revision Count",
"Notification.type.system": "System",
"Notification.type.team": "Team",
"OSMElementTags.controls.viewOSM.label": "View OSM",
"OSMElementTags.elementFetchFailed": "Failed to fetch tags for {element}",
"OSMElementTags.noOSMElements": "No OSM elements identified in task",
"OSMElementHistory.controls.viewOSM.label": "View OSM",
"OSMElementHistory.elementFetchFailed": "Failed to fetch history for {element}",
"OSMElementHistory.noComment": "(no changeset comment)",
Expand Down Expand Up @@ -1724,6 +1727,8 @@
"Widgets.LeaderboardWidget.mapper": "Mappers",
"Widgets.LeaderboardWidget.reviewer": "Reviewers",
"Widgets.LeaderboardWidget.title": "Leaderboard",
"Widgets.OSMElementTagsWidget.label": "OSM Element Tags",
"Widgets.OSMElementTagsWidget.title": "OSM Element Tags",
"Widgets.OSMHistoryWidget.label": "OSM History",
"Widgets.OSMHistoryWidget.title": "OSM History",
"Widgets.ProjectAboutWidget.content": "Projects serve as a means of grouping related challenges together. All\nchallenges must belong to a project.\n\nYou can create as many projects as needed to organize your challenges, and can\ninvite other MapRoulette users to help manage them with you.\n\nProjects must be set to Discoverable before any challenges within them will\nshow up in public browsing or searching.",
Expand Down
3 changes: 2 additions & 1 deletion src/components/HOCs/WithCurrentTask/WithCurrentTask.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { fetchChallenge, fetchParentProject }
import { fetchUser } from '../../../services/User/User'
import { TaskLoadMethod }
from '../../../services/Task/TaskLoadMethod/TaskLoadMethod'
import { fetchOSMUser, fetchOSMData, fetchOSMElementHistory }
import { fetchOSMUser, fetchOSMData, fetchOSMElement, fetchOSMElementHistory }
from '../../../services/OSM/OSM'
import { fetchChallengeActions } from '../../../services/Challenge/Challenge'
import { renewVirtualChallenge }
Expand Down Expand Up @@ -277,6 +277,7 @@ export const mapDispatchToProps = (dispatch, ownProps) => {
},

fetchOSMElementHistory,
fetchOSMElement,
homersimpsons marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/OSMElementTags/Messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineMessages } from 'react-intl'

/**
* Internationalized messages for use with OSMElementTags
*/
export default defineMessages({
noOSMElements: {
id: "OSMElementTags.noOSMElements",
defaultMessage: "No OSM elements identified in task",
},

elementFetchFailed: {
id: "OSMElementTags.elementFetchFailed",
defaultMessage: "Failed to fetch tags for {element}",
},

viewOSMLabel: {
id: "OSMElementTags.controls.viewOSM.label",
defaultMessage: "View OSM",
},
})
152 changes: 152 additions & 0 deletions src/components/OSMElementTags/OSMElementTags.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Fragment, useState, useMemo } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl }
from 'react-intl'
import _map from 'lodash/map'
import _compact from 'lodash/compact'
import _flatten from 'lodash/flatten'
import AsMappableTask
from '../../interactions/Task/AsMappableTask'
import AsIdentifiableFeature
from '../../interactions/TaskFeature/AsIdentifiableFeature'
import Dropdown from '../Dropdown/Dropdown'
import SvgSymbol from '../SvgSymbol/SvgSymbol'
import BusySpinner from '../BusySpinner/BusySpinner'
import messages from './Messages'
import './OSMElementTags.scss'
import { useQuery } from 'react-query'

const OSM_SERVER = window.env.REACT_APP_OSM_SERVER

const OSMElementTags = props => {
const { fetchOSMElement, taskBundle, task: primaryTask } = props

const featureIds = useMemo(() => {
const allTasks = taskBundle ? taskBundle.tasks : [primaryTask]
return _flatten(_compact(_map(allTasks, task => {
const geometries = AsMappableTask(task).normalizedGeometries()
return geometries ? _compact(_map(
geometries.features,
f => AsIdentifiableFeature(f).normalizedTypeAndId(true, '/')
)) : null
})))
}, [primaryTask, taskBundle])

if (featureIds.length === 0) {
return (
<FormattedMessage {...messages.noOSMElements} />
)
}

const [selectedFeatureId, setSelectedFeatureId] = useState(featureIds[0])
const widgetLayoutProps = { featureIds, selectedFeatureId, setSelectedFeatureId }

const { isLoading, isError, error: fetchErr, data: element } = useQuery({
queryKey: ['OSMElement', selectedFeatureId],
queryFn: () => fetchOSMElement(selectedFeatureId)
})

if (isLoading) {
return (
<WidgetLayout {...widgetLayoutProps}>
<div className="mr-flex mr-justify-center mr-items-center mr-w-full mr-h-full">
<BusySpinner />
</div>
</WidgetLayout>
)
}

if (isError) {
return (
<WidgetLayout {...widgetLayoutProps}>
<div className="mr-flex mr-flex-col mr-text-red-light">
<FormattedMessage {...messages.elementFetchFailed} values={{element: selectedFeatureId}} />
{fetchErr && fetchErr.defaultMessage && <FormattedMessage {...fetchErr} />}
</div>
</WidgetLayout>
)
}

const tagsValues = _map(
element.tag,
({k, v}) => <Fragment key={k}><dt>{k}</dt><dd>{v}</dd></Fragment>
)

return (
<WidgetLayout {...widgetLayoutProps}>
<dl className='tag-list'>
{tagsValues}
</dl>
</WidgetLayout>
)
}

OSMElementTags.propTypes = {
task: PropTypes.object,
taskBundle: PropTypes.object,
fetchOSMElement: PropTypes.func.isRequired,
}

const WidgetLayout = props =>(
<div className="mr-mr-4">
<div className="mr-flex mr-justify-between mr-links-green-lighter mr-mb-4">
<FeatureSelectionDropdown
featureIds={props.featureIds}
selectedFeatureId={props.selectedFeatureId}
selectFeatureId={props.setSelectedFeatureId}
/>
<a
className="mr-button mr-button--xsmall"
href={`${OSM_SERVER}/${props.selectedFeatureId}`}
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage {...messages.viewOSMLabel} />
</a>
</div>
{props.children}
</div>
)

const FeatureSelectionDropdown = props => {
if (props.featureIds.length === 0) {
return null
}

const menuItems =
_map(props.featureIds, featureId => (
<li key={featureId}>
<a onClick={() => props.selectFeatureId(featureId)}>
{featureId}
</a>
</li>
))

return (
<Dropdown
{...props}
className="mr-dropdown"
dropdownButton={dropdown => (
<Fragment>
<a className="mr-flex" onClick={dropdown.toggleDropdownVisible}>
<div className="mr-mr-2">
{props.selectedFeatureId}
</div>
<SvgSymbol
sym="icon-cheveron-down"
viewBox="0 0 20 20"
className="mr-fill-current mr-w-5 mr-h-5"
/>
</a>
</Fragment>
)}
dropdownContent={({closeDropdown}) =>
<ol className="mr-list-dropdown" onClick={closeDropdown}>
{menuItems}
</ol>
}
/>
);
}

export default injectIntl(OSMElementTags)
17 changes: 17 additions & 0 deletions src/components/OSMElementTags/OSMElementTags.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import '../../variables.scss';

dl.tag-list {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-column-gap: 15px;
grid-row-gap: 0px;

dt {
font-family: monospace;
font-weight: $weight-bold;
}

dd {
font-size: $size-7;
}
}
16 changes: 16 additions & 0 deletions src/components/Widgets/OSMElementTags/Messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineMessages } from 'react-intl'

/**
* Internationalized messages for use with OSMElementTagsWidget
*/
export default defineMessages({
label: {
id: "Widgets.OSMElementTagsWidget.label",
defaultMessage: "OSM Element Tags",
},

title: {
id: "Widgets.OSMElementTagsWidget.title",
defaultMessage: "OSM Element Tags",
},
})
33 changes: 33 additions & 0 deletions src/components/Widgets/OSMElementTags/OSMElementTagsWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Component } from 'react'
import { FormattedMessage } from 'react-intl'
import { WidgetDataTarget, registerWidgetType }
from '../../../services/Widget/Widget'
import OSMElementTags from '../../OSMElementTags/OSMElementTags'
import QuickWidget from '../../QuickWidget/QuickWidget'
import messages from './Messages'

const descriptor = {
widgetKey: 'OSMElementTagsWidget',
label: messages.label,
targets: [WidgetDataTarget.task],
minWidth: 3,
defaultWidth: 4,
minHeight: 3,
defaultHeight: 6,
}

export default class OSMElementTagsWidget extends Component {
render() {
return (
<QuickWidget
{...this.props}
className=""
widgetTitle={<FormattedMessage {...messages.title} />}
>
<OSMElementTags {...this.props} />
</QuickWidget>
)
}
}

registerWidgetType(OSMElementTagsWidget, descriptor)
2 changes: 2 additions & 0 deletions src/components/Widgets/widget_registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export { default as SupplementalMapWidget }
from './SupplementalMapWidget/SupplementalMapWidget'
export { default as CustomUrlWidget }
from './CustomUrlWidget/CustomUrlWidget'
export { default as OSMElementTagsWidget }
from './OSMElementTags/OSMElementTagsWidget'
export { default as OSMHistoryWidget }
from './OSMHistoryWidget/OSMHistoryWidget'
export { default as ActivityListingWidget }
Expand Down