Skip to content

Commit

Permalink
✨ Add OSM Data widget
Browse files Browse the repository at this point in the history
  • Loading branch information
homersimpsons committed Nov 17, 2024
1 parent 141eb3e commit d6d1a11
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 1 deletion.
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",
"OSMElementData.controls.viewOSM.label": "View OSM",
"OSMElementData.elementFetchFailed": "Failed to fetch tags for {element}",
"OSMElementData.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.OSMDataWidget.label": "OSM Data",
"Widgets.OSMDataWidget.title": "OSM Data",
"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,
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/OSMElementData/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 OSMElementData
*/
export default defineMessages({
noOSMElements: {
id: "OSMElementData.noOSMElements",
defaultMessage: "No OSM elements identified in task",
},

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

viewOSMLabel: {
id: "OSMElementData.controls.viewOSM.label",
defaultMessage: "View OSM",
},
})
186 changes: 186 additions & 0 deletions src/components/OSMElementData/OSMElementData.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { Fragment, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { FormattedMessage, injectIntl }
from 'react-intl'
import _map from 'lodash/map'
import _compact from 'lodash/compact'
import _isEmpty from 'lodash/isEmpty'
import _sortBy from 'lodash/sortBy'
import _find from 'lodash/find'
import _get from 'lodash/get'
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 './OSMElementData.scss'

const OSM_SERVER = window.env.REACT_APP_OSM_SERVER

const OSMElementData = props => {
const [selectedFeatureId, setSelectedFeatureId] = useState(null)
const [element, setElement] = useState(null)
const [fetchingElement, setFetchingElement] = useState(null)
const [fetchedElement, setFetchedElement] = useState(null)
const [failedElement, setFailedElement] = useState(null)
const [fetchErr, setFetchErr] = useState(null)

const { fetchOSMElement, taskBundle } = props
const primaryTask = props.task
const allTasks = taskBundle ? taskBundle.tasks : [primaryTask]

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

// If there is no selected feature or it is no longer available (e.g. because a task bundle
// was unbundled), then update the selection
useEffect(() => {
if (!selectedFeatureId || featureIds.indexOf(selectedFeatureId) === -1) {
setSelectedFeatureId(featureIds[0] ?? null)
}
}, [featureIds, selectedFeatureId])

// Fetch and process the OSM Data
useEffect(() => {
if (selectedFeatureId === null) {
// No features to fetch, cleanup existing to ensure consistency
setElement(null)
setFetchedElement(null)
return
}

// If we're already fetching data for the active feature, or if the fetch failed, or if we already have its data,
// there's nothing to do
if (fetchingElement === selectedFeatureId || failedElement === selectedFeatureId || fetchedElement === selectedFeatureId) {
return
}

setFetchingElement(selectedFeatureId)
setFailedElement(null)
setFetchErr(null)
fetchOSMElement(selectedFeatureId).then(element => {
// If for some reason we don't get any element entries, record a failure
// to prevent further attempts to refetch
if (!element) { // TODO Can this happen ?
setFailedElement(selectedFeatureId)
setFetchingElement(null)
return
}

setFetchedElement(selectedFeatureId)
setElement(element)
setFetchingElement(null)
}).catch(err => {
setFailedElement(selectedFeatureId)
setFetchErr(err)
setFetchingElement(null)
})
}, [selectedFeatureId, fetchingElement, failedElement, fetchOSMElement])

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

const activeFeatureId = selectedFeatureId ? selectedFeatureId : featureIds[0]
if (failedElement === activeFeatureId) {
return (
<div className="mr-flex mr-flex-col mr-text-red-light">
<FormattedMessage {...messages.elementFetchFailed} values={{element: activeFeatureId}} />
{fetchErr && fetchErr.defaultMessage && <FormattedMessage {...fetchErr} />}
</div>
)
}

if (!element) {
return (
<FormattedMessage {...messages.noOSMElements} />
)
}

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

return (
<div className="mr-mr-4">
<div className="mr-flex mr-justify-between mr-links-green-lighter mr-mb-4">
<FeatureSelectionDropdown
featureIds={featureIds}
selectedFeatureId={activeFeatureId}
selectFeatureId={setSelectedFeatureId}
/>
<a
className="mr-button mr-button--xsmall"
href={`${OSM_SERVER}/${activeFeatureId}`}
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage {...messages.viewOSMLabel} />
</a>
</div>
<dl className='tag-list'>
{tagsValues}
</dl>
</div>
)
}

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

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

if (menuItems.length === 0) {
return null
}

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={() =>
<ol className="mr-list-dropdown">
{menuItems}
</ol>
}
/>
);
}

export default injectIntl(OSMElementData)
17 changes: 17 additions & 0 deletions src/components/OSMElementData/OSMElementData.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/OSMDataWidget/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 OSMDataWidget
*/
export default defineMessages({
label: {
id: "Widgets.OSMDataWidget.label",
defaultMessage: "OSM Data",
},

title: {
id: "Widgets.OSMDataWidget.title",
defaultMessage: "OSM Data",
},
})
33 changes: 33 additions & 0 deletions src/components/Widgets/OSMDataWidget/OSMDataWidget.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 OSMElementData from '../../OSMElementData/OSMElementData'
import QuickWidget from '../../QuickWidget/QuickWidget'
import messages from './Messages'

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

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

registerWidgetType(OSMDataWidget, 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 OSMDataWidget }
from './OSMDataWidget/OSMDataWidget'
export { default as OSMHistoryWidget }
from './OSMHistoryWidget/OSMHistoryWidget'
export { default as ActivityListingWidget }
Expand Down

0 comments on commit d6d1a11

Please sign in to comment.