From 8e616d738402ad393b28bba30d6806fa974847fe Mon Sep 17 00:00:00 2001 From: Matias Volpe Date: Wed, 21 Aug 2019 11:57:14 -0300 Subject: [PATCH] feat: add Mapillary Map Features layer --- API.md | 2 +- css/60_photos.css | 19 +++ css/80_app.css | 6 + data/core.yaml | 4 + dist/locales/en.json | 233 +++++--------------------- modules/renderer/background.js | 1 + modules/renderer/photos.js | 2 +- modules/services/mapillary.js | 49 +++++- modules/svg/layers.js | 2 + modules/svg/mapillary_map_features.js | 173 +++++++++++++++++++ modules/ui/map_data.js | 19 ++- package.json | 3 +- test/spec/svg/layers.js | 13 +- 13 files changed, 321 insertions(+), 205 deletions(-) create mode 100644 modules/svg/mapillary_map_features.js diff --git a/API.md b/API.md index 29868521f..0413360a9 100644 --- a/API.md +++ b/API.md @@ -42,7 +42,7 @@ of iD (e.g. `http://preview.ideditor.com/release/`), the following parameters ar _Example:_ `offset=-10,5` * __`photo_overlay`__ - The street-level photo overlay layers to enable.
_Example:_ `photo_overlay=streetside,mapillary,openstreetcam`
- _Available values:_ `streetside` (Microsoft Bing), `mapillary`, `mapillary-signs`, `openstreetcam` + _Available values:_ `streetside` (Microsoft Bing), `mapillary`, `mapillary-signs`, `mapillary-map-features`, `openstreetcam` * __`presets`__ - A path to an external presets file or a comma-separated list of preset IDs. These will be the only presets the user may select.
_Example:_ `presets=https://path/to/presets.json` _Example 2:_ `presets=building,highway/residential,highway/unclassified` diff --git a/css/60_photos.css b/css/60_photos.css index 4ecfab247..26d6259c4 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -214,6 +214,25 @@ } +/* Mapillary Map Features Layer */ +.layer-mapillary-map-features { + pointer-events: none; +} +.layer-mapillary-map-features .icon-map-feature { + outline: 2px solid transparent; + pointer-events: visible; + cursor: pointer; +} +.layer-mapillary-map-features .icon-map-feature:hover { + outline: 5px solid #eebb00; + background-color: #eebb00; +} +.layer-mapillary-map-features .icon-map-feature.currentView { + outline: 5px solid #ffee00; + background-color: #ffee00; +} + + /* OpenStreetCam Image Layer */ .layer-openstreetcam { pointer-events: none; diff --git a/css/80_app.css b/css/80_app.css index 210f36cd3..0f5fddbb3 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -5691,3 +5691,9 @@ li.hide + li.version .badge .tooltip .tooltip-arrow { width: 100px; color: #7092ff; } + + +.list-item-photos.list-item-mapillary-map-features .request-data-link { + float: right; + margin-top: -20px; +} \ No newline at end of file diff --git a/data/core.yaml b/data/core.yaml index 3b772da67..bd628eb05 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1049,6 +1049,10 @@ en: hires: "High resolution" mapillary_images: tooltip: "Street-level photos from Mapillary" + mapillary_map_features: + title: "Map Features" + tooltip: "Map features from Mapillary" + request_data: "Request Data" mapillary: title: Mapillary signs: diff --git a/dist/locales/en.json b/dist/locales/en.json index 31a650a24..91784fb27 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -1315,6 +1315,11 @@ "mapillary_images": { "tooltip": "Street-level photos from Mapillary" }, + "mapillary_map_features": { + "title": "Map Features", + "tooltip": "Map features from Mapillary", + "request_data": "Request Data" + }, "mapillary": { "title": "Mapillary", "signs": { @@ -9753,44 +9758,53 @@ "imagery": { "AGIV": { "attribution": { - "text": "© agentschap Informatie Vlaanderen" + "text": "Orthophoto Flanders most recent © AGIV" }, - "name": "AIV Flanders most recent aerial imagery" + "name": "AGIV Flanders most recent aerial imagery" }, "AGIV10cm": { "attribution": { - "text": "© agentschap Informatie Vlaanderen" + "text": "Orthophoto Flanders © AGIV" }, - "name": "AIV Flanders 2013-2015 aerial imagery 10cm" + "name": "AGIV Flanders 2013-2015 aerial imagery 10cm" }, "AGIVFlandersGRB": { "attribution": { - "text": "© agentschap Informatie Vlaanderen" + "text": "GRB Flanders © AGIV" }, - "name": "AIV Flanders GRB" - }, - "AIV_DHMV_II_HILL_25cm": { - "attribution": { - "text": "© agentschap Informatie Vlaanderen" - }, - "name": "AIV Digitaal Hoogtemodel Vlaanderen II, multidirectionale hillshade 0,25 m" - }, - "AIV_DHMV_II_SVF_25cm": { - "attribution": { - "text": "© agentschap Informatie Vlaanderen" - }, - "name": "AIV Digitaal Hoogtemodel Vlaanderen II, Skyview factor 0,25 m" + "name": "AGIV Flanders GRB" }, "Bing": { "description": "Satellite and aerial imagery.", "name": "Bing aerial imagery" }, - "EOXAT2018CLOUDLESS": { + "DigitalGlobe-Premium": { "attribution": { - "text": "Sentinel-2 cloudless - https://s2maps.eu by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2017 & 2018)" + "text": "Terms & Feedback" }, - "description": "Post-processed Sentinel Satellite imagery.", - "name": "eox.at 2018 cloudless" + "description": "DigitalGlobe-Premium is a mosaic composed of DigitalGlobe basemap with select regions filled with +Vivid or custom area of interest imagery, 50cm resolution or better, and refreshed more frequently with ongoing updates.", + "name": "DigitalGlobe Premium Imagery" + }, + "DigitalGlobe-Premium-vintage": { + "attribution": { + "text": "Terms & Feedback" + }, + "description": "Imagery boundaries and capture dates. Labels appear at zoom level 13 and higher.", + "name": "DigitalGlobe Premium Imagery Vintage" + }, + "DigitalGlobe-Standard": { + "attribution": { + "text": "Terms & Feedback" + }, + "description": "DigitalGlobe-Standard is a curated set of imagery covering 86% of the earth’s landmass, with 30-60cm resolution where available, backfilled by Landsat. Average age is 2.31 years, with some areas updated 2x per year.", + "name": "DigitalGlobe Standard Imagery" + }, + "DigitalGlobe-Standard-vintage": { + "attribution": { + "text": "Terms & Feedback" + }, + "description": "Imagery boundaries and capture dates. Labels appear at zoom level 13 and higher.", + "name": "DigitalGlobe Standard Imagery Vintage" }, "EsriWorldImagery": { "attribution": { @@ -9820,20 +9834,6 @@ "description": "Satellite and aerial imagery.", "name": "Mapbox Satellite" }, - "Maxar-Premium": { - "attribution": { - "text": "Terms & Feedback" - }, - "description": "Maxar Premium is a mosaic composed of Maxar basemap with select regions filled with +Vivid or custom area of interest imagery, 50cm resolution or better, and refreshed more frequently with ongoing updates.", - "name": "Maxar Premium Imagery (Beta)" - }, - "Maxar-Standard": { - "attribution": { - "text": "Terms & Feedback" - }, - "description": "Maxar Standard is a curated set of imagery covering 86% of the earth’s landmass, with 30-60cm resolution where available, backfilled by Landsat. Average age is 2.31 years, with some areas updated 2x per year.", - "name": "Maxar Standard Imagery (Beta)" - }, "OSM_Inspector-Addresses": { "attribution": { "text": "© Geofabrik GmbH, OpenStreetMap contributors, CC-BY-SA" @@ -9882,6 +9882,9 @@ "SPW_PICC": { "name": "SPW(allonie) PICC numerical imagery" }, + "US-TIGER-Roads-2012": { + "name": "TIGER Roads 2012" + }, "US-TIGER-Roads-2014": { "description": "At zoom level 16+, public domain map data from the US Census. At lower zooms, only changes since 2006 minus changes already incorporated into OpenStreetMap", "name": "TIGER Roads 2014" @@ -9890,14 +9893,6 @@ "description": "Yellow = Public domain map data from the US Census. Red = Data not found in OpenStreetMap", "name": "TIGER Roads 2017" }, - "US-TIGER-Roads-2018": { - "description": "Yellow = Public domain map data from the US Census. Red = Data not found in OpenStreetMap", - "name": "TIGER Roads 2018" - }, - "USDA-NAIP": { - "description": "The most recent year of DOQQs from the National Agriculture Imagery Program (NAIP) for each state in the contiguous United States.", - "name": "National Agriculture Imagery Program" - }, "US_Forest_Service_roads_overlay": { "description": "Highway: Green casing = unclassified. Brown casing = track. Surface: gravel = light brown fill, Asphalt = black, paved = gray, ground =white, concrete = blue, grass = green. Seasonal = white bars", "name": "U.S. Forest Roads Overlay" @@ -9914,12 +9909,6 @@ }, "name": "UrbIS-Ortho 2017" }, - "UrbISOrtho2018": { - "attribution": { - "text": "Realized by means of Brussels UrbIS®© - Distribution & Copyright CIRB" - }, - "name": "UrbIS-Ortho 2018" - }, "UrbisAdmFR": { "attribution": { "text": "Realized by means of Brussels UrbIS®© - Distribution & Copyright CIRB" @@ -9976,131 +9965,11 @@ "description": "Orthofoto layer provided by basemap.at. \"Successor\" of geoimage.at imagery.", "name": "basemap.at Orthofoto" }, - "basemap.at-overlay": { + "hike_n_bike": { "attribution": { - "text": "basemap.at" + "text": "© OpenStreetMap contributors" }, - "description": "Annotation overlay provided by basemap.at.", - "name": "basemap.at Overlay" - }, - "basemap.at-surface": { - "attribution": { - "text": "basemap.at" - }, - "description": "Surface layer provided by basemap.at.", - "name": "basemap.at Surface" - }, - "basemap.at-terrain": { - "attribution": { - "text": "basemap.at" - }, - "description": "Terrain layer provided by basemap.at.", - "name": "basemap.at Terrain" - }, - "eufar-balaton": { - "attribution": { - "text": "EUFAR Balaton ortofotó 2010" - }, - "description": "1940 geo-tagged photography from Balaton Limnological Institute.", - "name": "EUFAR Balaton orthophotos" - }, - "finds.jp_KBN_2500": { - "attribution": { - "text": "GSI KIBAN 2500" - }, - "description": "GSI Kiban 2500 via finds.jp. Good for tracing, but a bit older.", - "name": "Japan GSI KIBAN 2500" - }, - "gsi.go.jp": { - "attribution": { - "text": "GSI Japan" - }, - "description": "Japan GSI ortho Imagery. Usually better than bing, but a bit older.", - "name": "Japan GSI ortho Imagery" - }, - "gsi.go.jp_airphoto": { - "attribution": { - "text": "GSI Japan" - }, - "description": "Japan GSI airphoto Imagery. Not fully orthorectified, but a bit newer and/or differently covered than GSI ortho Imagery.", - "name": "Japan GSI airphoto Imagery" - }, - "gsi.go.jp_seamlessphoto": { - "attribution": { - "text": "GSI Japan seamless photo" - }, - "description": "Japan GSI seamlessphoto Imagery. The collection of latest imageries of GSI ortho, airphoto, post disaster and others.", - "name": "Japan GSI seamlessphoto Imagery" - }, - "gsi.go.jp_std_map": { - "attribution": { - "text": "GSI Japan" - }, - "description": "Japan GSI Standard Map. Widely covered.", - "name": "Japan GSI Standard Map" - }, - "helsingborg-orto": { - "attribution": { - "text": "© Helsingborg municipality" - }, - "description": "Orthophotos from the municipality of Helsingborg 2016, public domain", - "name": "Helsingborg Orthophoto" - }, - "kalmar-orto-2014": { - "attribution": { - "text": "© Kalmar municipality" - }, - "description": "Orthophotos for the north coast of the municipality of Kalmar 2014", - "name": "Kalmar North Orthophoto 2014" - }, - "kalmar-orto-2016": { - "attribution": { - "text": "© Kalmar municipality" - }, - "description": "Orthophotos for the south coast of the municipality of Kalmar 2016", - "name": "Kalmar South Orthophoto 2016" - }, - "kalmar-orto-2018": { - "attribution": { - "text": "© Kalmar municipality" - }, - "description": "Orthophotos for urban areas of the municipality of Kalmar 2018", - "name": "Kalmar Urban Orthophoto 2018" - }, - "kelkkareitit": { - "attribution": { - "text": "© Kelkkareitit.fi" - }, - "description": "Kelkkareitit.fi snowmobile trails from OSM (Nordic coverage)", - "name": "Nordic snowmobile overlay" - }, - "lantmateriet-orto1960": { - "attribution": { - "text": "© Lantmäteriet, CC0" - }, - "description": "Mosaic of Swedish orthophotos from the period 1955–1965. Older and younger pictures may occur.", - "name": "Lantmäteriet Historic Orthophoto 1960" - }, - "lantmateriet-orto1975": { - "attribution": { - "text": "© Lantmäteriet, CC0" - }, - "description": "Mosaic of Swedish orthophotos from the period 1970–1980. Is under construction.", - "name": "Lantmäteriet Historic Orthophoto 1975" - }, - "lantmateriet-topowebb": { - "attribution": { - "text": "© Lantmäteriet, CC0" - }, - "description": "Topographic map of Sweden 1:50 000", - "name": "Lantmäteriet Topographic Map" - }, - "linkoping-orto": { - "attribution": { - "text": "© Linköping municipality" - }, - "description": "Orthophotos from the municipality of Linköping 2010, open data", - "name": "Linköping Orthophoto" + "name": "Hike & Bike" }, "mapbox_locator_overlay": { "attribution": { @@ -10138,8 +10007,8 @@ "attribution": { "text": "© Lantmäteriet" }, - "description": "Scan of \"Economic maps\" ca. 1950–1980", - "name": "Lantmäteriet Economic Map 1950–1980" + "description": "Scan of ´Economic maps´ ca 1950-1980", + "name": "Lantmäteriet Economic Map (historic)" }, "qa_no_address": { "attribution": { @@ -10153,26 +10022,12 @@ }, "name": "skobbler" }, - "skoterleder": { - "attribution": { - "text": "© Skoterleder.org" - }, - "description": "Snowmobile trails", - "name": "Snowmobile map Sweden" - }, "stamen-terrain-background": { "attribution": { "text": "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL" }, "name": "Stamen Terrain" }, - "stockholm-orto": { - "attribution": { - "text": "© Stockholm municipality, CC0" - }, - "description": "Orthophotos from the municipality of Stockholm 2016, CC0 license", - "name": "Stockholm Orthophoto" - }, "tf-cycle": { "attribution": { "text": "Maps © Thunderforest, Data © OpenStreetMap contributors" diff --git a/modules/renderer/background.js b/modules/renderer/background.js index f293dfafa..0bbaf5a25 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -194,6 +194,7 @@ export function rendererBackground(context) { var photoOverlayLayers = { streetside: 'Bing Streetside', mapillary: 'Mapillary Images', + 'mapillary-map-features': 'Mapillary Map Features', 'mapillary-signs': 'Mapillary Signs', openstreetcam: 'OpenStreetCam Images' }; diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index 523241a78..f1751d03b 100644 --- a/modules/renderer/photos.js +++ b/modules/renderer/photos.js @@ -6,7 +6,7 @@ import { utilQsString, utilStringQs } from '../util'; export function rendererPhotos(context) { var dispatch = d3_dispatch('change'); - var _layerIDs = ['streetside', 'mapillary', 'mapillary-signs', 'openstreetcam']; + var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'openstreetcam']; var _allPhotoTypes = ['flat', 'panoramic']; var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index 326fe9625..0c1784073 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -13,10 +13,35 @@ var apibase = 'https://a.mapillary.com/v3/'; var viewercss = 'mapillary-js/mapillary.min.css'; var viewerjs = 'mapillary-js/mapillary.min.js'; var clientId = 'NzNRM2otQkR2SHJzaXJmNmdQWVQ0dzo1ZWYyMmYwNjdmNDdlNmVi'; +var mapFeatureConfig = { + organizationKey: 'FI3NAFfzQQgdF081TRdgTy', + values: [ + 'object--bench', + 'object--bike-rack', + 'object--billboard', + 'object--fire-hydrant', + 'object--mailbox', + 'object--phone-booth', + 'object--street-light', + 'object--support--utility-pole', + 'object--traffic-light--pedestrians', + 'object--trash-can', + 'construction--flat--crosswalk-plain', + 'object--cctv-camera', + 'object--banner', + 'object--catch-basin', + 'object--manhole', + 'object--sign--advertisement', + 'object--sign--information', + 'object--sign--store', + 'object--traffic-light--*', + 'marking--discrete--crosswalk-zebra' + ].join(',') +}; var maxResults = 1000; var tileZoom = 14; var tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true); -var dispatch = d3_dispatch('loadedImages', 'loadedSigns', 'bearingChanged'); +var dispatch = d3_dispatch('loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged'); var _mlyFallback = false; var _mlyCache; var _mlyClicks; @@ -160,7 +185,7 @@ function loadNextTilePage(which, currZoom, url, tile) { // A map feature is a real world object that can be shown on a map. It could be any object // recognized from images, manually added in images, or added on the map. // Each map feature is a GeoJSON Point (located where the feature is) - } else if (which === 'map_features') { + } else if (which === 'map_features' || which === 'points') { d = { loc: loc, key: feature.properties.key, @@ -191,6 +216,8 @@ function loadNextTilePage(which, currZoom, url, tile) { dispatch.call('loadedImages'); } else if (which === 'map_features') { dispatch.call('loadedSigns'); + } else if (which === 'points') { + dispatch.call('loadedMapFeatures'); } }) .catch(function() { @@ -260,6 +287,7 @@ export default { Object.values(_mlyCache.images.inflight).forEach(abortRequest); Object.values(_mlyCache.image_detections.inflight).forEach(abortRequest); Object.values(_mlyCache.map_features.inflight).forEach(abortRequest); + Object.values(_mlyCache.points.inflight).forEach(abortRequest); Object.values(_mlyCache.sequences.inflight).forEach(abortRequest); } @@ -267,6 +295,7 @@ export default { images: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {} }, image_detections: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, forImageKey: {} }, map_features: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, + points: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush() }, sequences: { inflight: {}, loaded: {}, nextPage: {}, nextURL: {}, rtree: rbush(), forImageKey: {}, lineString: {} } }; @@ -287,6 +316,12 @@ export default { }, + mapFeatures: function(projection) { + var limit = 5; + return searchLimited(limit, projection, _mlyCache.points.rtree); + }, + + cachedImage: function(imageKey) { return _mlyCache.images.forImageKey[imageKey]; }, @@ -334,6 +369,14 @@ export default { }, + loadMapFeatures: function(projection) { + // if we are looking at signs, we'll actually need to fetch images too + loadTiles('images', apibase + 'images?', projection); + loadTiles('points', apibase + 'map_features?layers=points&min_nbr_image_detections=1&&shapes_by_organization_keys=' + mapFeatureConfig.organizationKey + '&' + 'values=' + mapFeatureConfig.values + '&', projection); + loadTiles('image_detections', apibase + 'image_detections?layers=points&shapes_by_organization_keys=' + mapFeatureConfig.organizationKey + '&' + 'values=' + mapFeatureConfig.values + '&', projection); + }, + + loadViewer: function(context) { // add mly-wrapper var wrap = d3_select('#photoviewer').selectAll('.mly-wrapper') @@ -364,7 +407,7 @@ export default { // load mapillary signs sprite var defs = context.container().select('defs'); - defs.call(svgDefs(context).addSprites, ['mapillary-sprite'], false /* don't override colors */ ); + defs.call(svgDefs(context).addSprites, ['mapillary-sprite', 'mapillary-object-sprite'], false /* don't override colors */ ); // Register viewer resize handler context.ui().photoviewer.on('resize.mapillary', function() { diff --git a/modules/svg/layers.js b/modules/svg/layers.js index 2389118b3..12595a7d7 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -9,6 +9,7 @@ import { svgImproveOSM } from './improveOSM'; import { svgStreetside } from './streetside'; import { svgMapillaryImages } from './mapillary_images'; import { svgMapillarySigns } from './mapillary_signs'; +import { svgMapillaryMapFeatures } from './mapillary_map_features'; import { svgOpenstreetcamImages } from './openstreetcam_images'; import { svgOsm } from './osm'; import { svgNotes } from './notes'; @@ -28,6 +29,7 @@ export function svgLayers(projection, context) { { id: 'improveOSM', layer: svgImproveOSM(projection, context, dispatch) }, { id: 'streetside', layer: svgStreetside(projection, context, dispatch)}, { id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) }, + { id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) }, { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) }, { id: 'openstreetcam', layer: svgOpenstreetcamImages(projection, context, dispatch) }, { id: 'debug', layer: svgDebug(projection, context, dispatch) }, diff --git a/modules/svg/mapillary_map_features.js b/modules/svg/mapillary_map_features.js new file mode 100644 index 000000000..0cf21546d --- /dev/null +++ b/modules/svg/mapillary_map_features.js @@ -0,0 +1,173 @@ +import _throttle from 'lodash-es/throttle'; +import { select as d3_select } from 'd3-selection'; +import { svgPointTransform } from './helpers'; +import { services } from '../services'; + + +export function svgMapillaryMapFeatures(projection, context, dispatch) { + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var layer = d3_select(null); + var _mapillary; + + + function init() { + if (svgMapillaryMapFeatures.initialized) return; // run once + svgMapillaryMapFeatures.enabled = false; + svgMapillaryMapFeatures.initialized = true; + } + + + function getService() { + if (services.mapillary && !_mapillary) { + _mapillary = services.mapillary; + _mapillary.event.on('loadedMapFeatures', throttledRedraw); + } else if (!services.mapillary && _mapillary) { + _mapillary = null; + } + return _mapillary; + } + + + function showLayer() { + var service = getService(); + if (!service) return; + + editOn(); + } + + + function hideLayer() { + throttledRedraw.cancel(); + editOff(); + } + + + function editOn() { + layer.style('display', 'block'); + } + + + function editOff() { + layer.selectAll('.icon-map-feature').remove(); + layer.style('display', 'none'); + } + + + function click(d) { + var service = getService(); + if (!service) return; + + context.map().centerEase(d.loc); + + var selected = service.getSelectedImage(); + var selectedImageKey = selected && selected.key; + var imageKey; + + // Pick one of the images the map feature was detected in, + // preference given to an image already selected. + d.detections.forEach(function(detection) { + if (!imageKey || selectedImageKey === detection.image_key) { + imageKey = detection.image_key; + } + }); + + service + .selectImage(null, imageKey) + .updateViewer(imageKey, context) + .showViewer(); + } + + + function update() { + var service = getService(); + var data = (service ? service.mapFeatures(projection) : []); + var viewer = d3_select('#photoviewer'); + var selected = viewer.empty() ? undefined : viewer.datum(); + var selectedImageKey = selected && selected.key; + var transform = svgPointTransform(projection); + + var mapFeatures = layer.selectAll('.icon-map-feature') + .data(data, function(d) { return d.key; }); + + // exit + mapFeatures.exit() + .remove(); + + // enter + var enter = mapFeatures.enter() + .append('use') + .attr('class', 'icon-map-feature') + .attr('width', '24px') + .attr('height', '24px') + .attr('x', '-12px') + .attr('y', '-12px') + .attr('xlink:href', function(d) { return '#' + d.value; }) + .classed('currentView', function(d) { + return d.detections.some(function(detection) { + return detection.image_key === selectedImageKey; + }); + }) + .on('click', click); + + // update + mapFeatures + .merge(enter) + .sort(function(a, b) { + return (a === selected) ? 1 + : (b === selected) ? -1 + : b.loc[1] - a.loc[1]; // sort Y + }) + .attr('transform', transform); + } + + + function drawMapFeatures(selection) { + var enabled = svgMapillaryMapFeatures.enabled; + var service = getService(); + + layer = selection.selectAll('.layer-mapillary-map-features') + .data(service ? [0] : []); + + layer.exit() + .remove(); + + layer = layer.enter() + .append('g') + .attr('class', 'layer-mapillary-map-features') + .style('display', enabled ? 'block' : 'none') + .merge(layer); + + if (enabled) { + if (service && ~~context.map().zoom() >= minZoom) { + editOn(); + update(); + service.loadMapFeatures(projection); + } else { + editOff(); + } + } + } + + + drawMapFeatures.enabled = function(_) { + if (!arguments.length) return svgMapillaryMapFeatures.enabled; + svgMapillaryMapFeatures.enabled = _; + if (svgMapillaryMapFeatures.enabled) { + showLayer(); + } else { + hideLayer(); + } + dispatch.call('change'); + return this; + }; + + + drawMapFeatures.supported = function() { + return !!getService(); + }; + + + init(); + return drawMapFeatures; +} diff --git a/modules/ui/map_data.js b/modules/ui/map_data.js index 5124f2bd9..6abfc4d8d 100644 --- a/modules/ui/map_data.js +++ b/modules/ui/map_data.js @@ -149,7 +149,7 @@ export function uiMapData(context) { .append('li') .attr('class', function(d) { var classes = 'list-item-photos list-item-' + d.id; - if (d.id === 'mapillary-signs') { + if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') { classes += ' indented'; } return classes; @@ -162,7 +162,7 @@ export function uiMapData(context) { if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip'; else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip'; else if (d.id === 'openstreetcam') titleID = 'openstreetcam_images.tooltip'; - else titleID = d.id.replace('-', '_') + '.tooltip'; + else titleID = d.id.replace(/-/g, '_') + '.tooltip'; d3_select(this) .call(tooltip() .title(t(titleID)) @@ -180,9 +180,20 @@ export function uiMapData(context) { .text(function(d) { var id = d.id; if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs'; - return t(id.replace('-', '_') + '.title'); + return t(id.replace(/-/g, '_') + '.title'); }); + labelEnter + .filter(function(d) { return d.id === 'mapillary-map-features'; }) + .append('a') + .attr('class', 'request-data-link') + .attr('target', '_blank') + .attr('tabindex', -1) + .call(svgIcon('#iD-icon-out-link', 'inline')) + .attr('href', 'https://mapillary.github.io/mapillary_solutions/data-request') + .append('span') + .text(t('mapillary_map_features.request_data')); + // Update li @@ -520,7 +531,7 @@ export function uiMapData(context) { labelEnter .append('span') .text(t('map_data.layers.custom.title')); - + liEnter .append('button') .call(tooltip() diff --git a/package.json b/package.json index 64d896439..a5d7b57ef 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "dist:svg:fa": "svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/fa-sprite.svg svg/fontawesome/*.svg", "dist:svg:tnp": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"tnp-%s\" --symbol-sprite dist/img/tnp-sprite.svg svg/the-noun-project/*.svg", "dist:svg:maki": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"maki-%s\" --symbol-sprite dist/img/maki-sprite.svg node_modules/@mapbox/maki/icons/*.svg", - "dist:svg:mapillary": "svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/mapillary-sprite.svg node_modules/mapillary_sprite_source/package_signs/*.svg", + "dist:svg:mapillary:signs": "svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/mapillary-sprite.svg node_modules/mapillary_sprite_source/package_signs/*.svg", + "dist:svg:mapillary:objects": "svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/mapillary-object-sprite.svg node_modules/mapillary_sprite_source/package_objects/*.svg", "dist:svg:temaki": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"temaki-%s\" --symbol-sprite dist/img/temaki-sprite.svg node_modules/temaki/icons/*.svg", "imagery": "node data/update_imagery", "lint": "eslint *.js test/spec modules", diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 83d659080..5ac4d8bc2 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -26,7 +26,7 @@ describe('iD.svgLayers', function () { it('creates default data layers', function () { container.call(iD.svgLayers(projection, context)); var nodes = container.selectAll('svg .data-layer').nodes(); - expect(nodes.length).to.eql(12); + expect(nodes.length).to.eql(13); expect(d3.select(nodes[0]).classed('osm')).to.be.true; expect(d3.select(nodes[1]).classed('notes')).to.be.true; expect(d3.select(nodes[2]).classed('data')).to.be.true; @@ -34,11 +34,12 @@ describe('iD.svgLayers', function () { expect(d3.select(nodes[4]).classed('improveOSM')).to.be.true; expect(d3.select(nodes[5]).classed('streetside')).to.be.true; expect(d3.select(nodes[6]).classed('mapillary')).to.be.true; - expect(d3.select(nodes[7]).classed('mapillary-signs')).to.be.true; - expect(d3.select(nodes[8]).classed('openstreetcam')).to.be.true; - expect(d3.select(nodes[9]).classed('debug')).to.be.true; - expect(d3.select(nodes[10]).classed('geolocate')).to.be.true; - expect(d3.select(nodes[11]).classed('touch')).to.be.true; + expect(d3.select(nodes[7]).classed('mapillary-map-features')).to.be.true; + expect(d3.select(nodes[8]).classed('mapillary-signs')).to.be.true; + expect(d3.select(nodes[9]).classed('openstreetcam')).to.be.true; + expect(d3.select(nodes[10]).classed('debug')).to.be.true; + expect(d3.select(nodes[11]).classed('geolocate')).to.be.true; + expect(d3.select(nodes[12]).classed('touch')).to.be.true; }); });