diff --git a/CHANGELOG.md b/CHANGELOG.md index a84da40501..686266c723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :scissors: Operations #### :camera: Street-Level * Show Mapillary username and deep link to external viewer on Mapillary photos ([#10135], thanks [@Sushil642]) +* Add button to directly attach the id of a mapillary photo as the `mapillary` tag of selected map features ([#9339]) #### :white_check_mark: Validation * Drop validation which checks for [old style multipolygons](https://wiki.openstreetmap.org/wiki/Old_style_multipolygons), as these have long been [fixed](https://blog.jochentopf.com/2017-08-28-polygon-fixing-effort-concluded.html) in OSM * Upgrade closed ways with `traffic_calming=island` to `area:highway=traffic_island` ([id-tagging-schema#1162]) @@ -60,6 +61,7 @@ _Breaking developer changes, which may affect downstream projects or sites that [#5420]: https://github.com/openstreetmap/iD/issues/5420 [#7653]: https://github.com/openstreetmap/iD/issues/7653 [#8415]: https://github.com/openstreetmap/iD/issues/8415 +[#9339]: https://github.com/openstreetmap/iD/issues/9339 [#9439]: https://github.com/openstreetmap/iD/issues/9439 [#10135]: https://github.com/openstreetmap/iD/issues/10135 [#10145]: https://github.com/openstreetmap/iD/issues/10145 diff --git a/css/60_photos.css b/css/60_photos.css index 8bf5e32096..2794c48d66 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -33,6 +33,15 @@ z-index: 50; } +.photoviewer button.set-photo-from-viewer { + border-radius: 0; + padding: 5px; + position: absolute; + left: 5px; + top: 5px; + z-index: 50; +} + .photoviewer button.resize-handle-xy { border-radius: 0; position: absolute; diff --git a/data/core.yaml b/data/core.yaml index 5a13caeb30..d81519189b 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -788,6 +788,11 @@ en: inch: in max_length_reached: "This string is longer than the maximum length of {maxChars} characters. Anything exceeding that length will be truncated." set_today: "Sets the value to today." + set_photo_from_viewer: + enable: "Tag this photo id on the currently selected map feature" + disable: + already_set: "This photo is already tagged on the selected map feature" + too_far: "This photo is too far away from the selected map feature" background: title: Background description: Background Settings diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index fb64e1eab7..8e0c2fe321 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -30,6 +30,7 @@ let _mlyShowFeatureDetections = false; let _mlyShowSignDetections = false; let _mlyViewer; let _mlyViewerFilter = ['all']; +let _isViewerOpen = false; // Load all data for the specified type from Mapillary vector tiles @@ -478,6 +479,8 @@ export default { _mlyViewer.resize(); } + _isViewerOpen = true; + return this; }, @@ -504,10 +507,18 @@ export default { dispatch.call('loadedMapFeatures'); dispatch.call('loadedSigns'); + _isViewerOpen = false; + return this.setStyles(context, null); }, + // Get viewer status + isViewerOpen: function() { + return _isViewerOpen; + }, + + // Update the URL with current image id updateUrlImage: function(imageId) { if (!window.mocha) { diff --git a/modules/ui/photoviewer.js b/modules/ui/photoviewer.js index 0d1240ff88..71cbf2da10 100644 --- a/modules/ui/photoviewer.js +++ b/modules/ui/photoviewer.js @@ -6,8 +6,11 @@ import { t } from '../core/localizer'; import { dispatch as d3_dispatch } from 'd3-dispatch'; import { svgIcon } from '../svg/icon'; import { utilGetDimensions } from '../util/dimensions'; -import { utilRebind } from '../util'; +import { utilRebind, utilStringQs } from '../util'; import { services } from '../services'; +import { uiTooltip } from './tooltip'; +import { actionChangeTags } from '../actions'; +import { geoSphericalDistance } from '../geo'; export function uiPhotoviewer(context) { @@ -61,6 +64,129 @@ export function uiPhotoviewer(context) { buildResizeListener(selection, 'resize', dispatch, { resizeOnY: true }) ); + // update sett_photo_from_viewer button on selection change and when tags change + context.features().on('change.setPhotoFromViewer', function() { + setPhotoFromViewerButton(); + }); + context.history().on('change.setPhotoFromViewer', function() { + setPhotoFromViewerButton(); + }); + + + function setPhotoFromViewerButton() { + if (services.mapillary.isViewerOpen()) { + if (context.mode().id !== 'select' || !(layerStatus('mapillary') && getServiceId() === 'mapillary')) { + buttonRemove(); + } else { + if (selection.select('.set-photo-from-viewer').empty()) { + const button = buttonCreate(); + button.on('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + setMapillaryPhotoId(); + buttonDisable('already_set'); + }); + } + buttonShowHide(); + } + + function setMapillaryPhotoId() { + const service = services.mapillary; + const image = service.getActiveImage(); + + const action = graph => + context.selectedIDs().reduce((graph, entityID) => { + const tags = graph.entity(entityID).tags; + const action = actionChangeTags(entityID, {...tags, mapillary: image.id}); + return action(graph); + }, graph); + + const annotation = t('operations.change_tags.annotation'); + context.perform(action, annotation); + } + } + + function layerStatus(which) { + const layers = context.layers(); + const layer = layers.layer(which); + return layer.enabled(); + } + + function getServiceId() { + const hash = utilStringQs(window.location.hash); + let serviceId; + if (hash.photo) { + let result = hash.photo.split('/'); + serviceId = result[0]; + } + return serviceId; + } + + function buttonCreate() { + const button = selection.selectAll('.set-photo-from-viewer').data([0]); + const buttonEnter = button.enter() + .append('button') + .attr('class', 'set-photo-from-viewer') + .call(svgIcon('#iD-icon-plus')) + .call(uiTooltip() + .title(() => t.append('inspector.set_photo_from_viewer')) + .placement('right') + ); + + buttonEnter.select('.tooltip') + .classed('dark', true) + .style('width', '300px'); + + return buttonEnter; + } + + function buttonRemove() { + const button = selection.selectAll('.set-photo-from-viewer').data([0]); + button.remove(); + } + + function buttonShowHide() { + const activeImage = services.mapillary.getActiveImage(); + + const graph = context.graph(); + const entities = context.selectedIDs() + .map(id => graph.entity(id)); + + if (entities.map(entity => entity.tags.mapillary) + .every(value => value === activeImage?.id)) { + buttonDisable('already_set'); + } else if (activeImage && entities.map(entity => entity.extent().center()) + .every(loc => geoSphericalDistance(loc, activeImage.loc) > 100)) { + buttonDisable('too_far'); + } else { + buttonDisable(false); + } + } + + function buttonDisable(reason) { + const disabled = reason !== false; + const button = selection.selectAll('.set-photo-from-viewer').data([0]); + button.attr('disabled', disabled ? 'true' : null); + button.classed('disabled', disabled); + button.call(uiTooltip().destroyAny); + if (disabled) { + button.call(uiTooltip() + .title(() => t.append(`inspector.set_photo_from_viewer.disable.${reason}`)) + .placement('right') + ); + } else { + button.call(uiTooltip() + .title(() => t.append('inspector.set_photo_from_viewer.enable')) + .placement('right') + ); + } + + button.select('.tooltip') + .classed('dark', true) + .style('width', '300px'); + } + } + function buildResizeListener(target, eventName, dispatch, options) { var resizeOnX = !!options.resizeOnX;