diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e44772fe..d2927eb39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,8 +37,11 @@ _Breaking developer changes, which may affect downstream projects or sites that
# Unreleased (2.27.0-dev)
+#### :mega: Release Highlights
+* Add [_Mapilio_](https://mapilio.com/openstreetmap) as new street-level imagery provider ([#9664], thanks [@channel-s])
#### :tada: New Features
#### :sparkles: Usability & Accessibility
+* Show tag reference information for the currently filled-in tag value in UI fields (if available), instead of only showing the more generic _key_ documentation of the field ([#9786])
#### :scissors: Operations
#### :camera: Street-Level
#### :white_check_mark: Validation
@@ -53,6 +56,9 @@ _Breaking developer changes, which may affect downstream projects or sites that
#### :hammer: Development
[#8997]: https://github.com/openstreetmap/iD/issues/8997
+[#9786]: https://github.com/openstreetmap/iD/issues/9786
+[#9664]: https://github.com/openstreetmap/iD/pull/9664
+[@channel-s]: https://github.com/channel-s
# 2.26.2
diff --git a/css/60_photos.css b/css/60_photos.css
index db66d5da8..98d568a68 100644
--- a/css/60_photos.css
+++ b/css/60_photos.css
@@ -254,6 +254,46 @@
stroke: #20c4ff;
}
+/* Mapilio Image Layer */
+.layer-mapilio {
+ pointer-events: none;
+}
+.layer-mapilio .viewfield-group * {
+ fill: #0056f1;
+ stroke: #ffffff;
+ stroke-opacity: .6;
+ fill-opacity: .6;
+}
+.layer-mapilio .sequence {
+ stroke: #0056f1;
+}
+.photo-controls-mapilio {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+}
+
+.photo-controls-mapilio button {
+ padding:0 6px;
+ pointer-events: initial;
+}
+
+.ideditor .mapilio-wrapper {
+ position: relative;
+ background-color: #000;
+ background-image: url(img/loader-black.gif);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+#ideditor-viewer-mapilio-simple-wrap {
+ height: 100%;
+}
+#ideditor-viewer-mapilio-simple {
+ width: 100%;
+ height: 100%;
+ transform-origin: 0 0;
+}
/* Streetside Viewer (pannellum) */
.ms-wrapper .photo-attribution .image-link {
@@ -306,6 +346,10 @@ label.streetside-hires {
margin: 0 5px;
}
+.pnlm-zoom-controls {
+ margin-top: 6px;
+}
+
/* Mapillary viewer */
#ideditor-mly .domRenderer .TagSymbol {
diff --git a/css/80_app.css b/css/80_app.css
index 8121a1b50..faab82563 100644
--- a/css/80_app.css
+++ b/css/80_app.css
@@ -2730,14 +2730,19 @@ button.raw-tag-option svg.icon {
}
.tag-reference-body.expanded {
padding-bottom: 10px;
- display: inline-block;
+ padding-left: 10px;
+ display: block;
}
-.tag-reference-description {
-
+.ideditor[dir='rtl'] .tag-reference-body.expanded {
+ padding-left: 0;
+ padding-right: 10px;
}
.tag-reference-link {
display: block;
}
+.tag-reference-link .icon:first-child {
+ margin-left: 0;
+}
img.tag-reference-wiki-image {
float: right;
diff --git a/data/core.yaml b/data/core.yaml
index 8a0664c0f..b5be4d062 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -1437,6 +1437,9 @@ en:
kartaview:
title: KartaView
view_on_kartaview: "View this image on KartaView"
+ mapilio:
+ title: Mapilio
+ tooltip: "Street-level photos from Mapilio"
note:
note: Note
title: Edit note
diff --git a/modules/renderer/background.js b/modules/renderer/background.js
index 9cdae67ca..90d69d97f 100644
--- a/modules/renderer/background.js
+++ b/modules/renderer/background.js
@@ -252,7 +252,8 @@ export function rendererBackground(context) {
'mapillary-map-features': 'Mapillary Map Features',
'mapillary-signs': 'Mapillary Signs',
kartaview: 'KartaView Images',
- vegbilder: 'Norwegian road administration images'
+ vegbilder: 'Norwegian Road Administration Images',
+ mapilio: 'Mapilio Images'
};
for (let layerID in photoOverlayLayers) {
diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js
index 51b7f1f22..7c3fa952a 100644
--- a/modules/renderer/photos.js
+++ b/modules/renderer/photos.js
@@ -7,7 +7,7 @@ import { utilQsString, utilStringQs } from '../util';
export function rendererPhotos(context) {
var dispatch = d3_dispatch('change');
- var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'vegbilder'];
+ var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder'];
var _allPhotoTypes = ['flat', 'panoramic'];
var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy
var _dateFilters = ['fromDate', 'toDate'];
diff --git a/modules/services/index.js b/modules/services/index.js
index 9d1494e02..896fcc127 100644
--- a/modules/services/index.js
+++ b/modules/services/index.js
@@ -14,6 +14,7 @@ import serviceTaginfo from './taginfo';
import serviceVectorTile from './vector_tile';
import serviceWikidata from './wikidata';
import serviceWikipedia from './wikipedia';
+import serviceMapilio from './mapilio';
export let services = {
@@ -32,7 +33,8 @@ export let services = {
taginfo: serviceTaginfo,
vectorTile: serviceVectorTile,
wikidata: serviceWikidata,
- wikipedia: serviceWikipedia
+ wikipedia: serviceWikipedia,
+ mapilio: serviceMapilio
};
export {
@@ -51,5 +53,6 @@ export {
serviceTaginfo,
serviceVectorTile,
serviceWikidata,
- serviceWikipedia
+ serviceWikipedia,
+ serviceMapilio
};
diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js
new file mode 100644
index 000000000..31acbbbbc
--- /dev/null
+++ b/modules/services/mapilio.js
@@ -0,0 +1,610 @@
+import { dispatch as d3_dispatch } from 'd3-dispatch';
+import { select as d3_select } from 'd3-selection';
+import { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';
+
+import Protobuf from 'pbf';
+import RBush from 'rbush';
+import { VectorTile } from '@mapbox/vector-tile';
+
+import { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util';
+import {geoExtent, geoScaleToZoom} from '../geo';
+import {localizer} from '../core/localizer';
+
+const apiUrl = 'https://end.mapilio.com';
+const imageBaseUrl = 'https://cdn.mapilio.com/im';
+const baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:';
+const pointLayer = 'map_points';
+const lineLayer = 'map_roads_line';
+const tileStyle = '&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}';
+
+const minZoom = 14;
+const dispatch = d3_dispatch('loadedImages', 'loadedLines');
+const imgZoom = d3_zoom()
+ .extent([[0, 0], [320, 240]])
+ .translateExtent([[0, 0], [320, 240]])
+ .scaleExtent([1, 15]);
+const pannellumViewerCSS = 'pannellum-streetside/pannellum.css';
+const pannellumViewerJS = 'pannellum-streetside/pannellum.js';
+const resolution = 1080;
+
+let _mlyActiveImage;
+let _mlyCache;
+let _loadViewerPromise;
+let _pannellumViewer;
+let _mlySceneOptions = {
+ showFullscreenCtrl: false,
+ autoLoad: true,
+ yaw: 0,
+ minHfov: 10,
+ maxHfov: 90,
+ hfov: 60,
+};
+let _currScene = 0;
+
+
+// Partition viewport into higher zoom tiles
+function partitionViewport(projection) {
+ const z = geoScaleToZoom(projection.scale());
+ const z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5
+ const tiler = utilTiler().zoomExtent([z2, z2]);
+
+ return tiler.getTiles(projection)
+ .map(function(tile) { return tile.extent; });
+}
+
+
+// Return no more than `limit` results per partition.
+function searchLimited(limit, projection, rtree) {
+ limit = limit || 5;
+
+ return partitionViewport(projection)
+ .reduce(function(result, extent) {
+ const found = rtree.search(extent.bbox())
+ .slice(0, limit)
+ .map(function(d) { return d.data; });
+
+ return (found.length ? result.concat(found) : result);
+ }, []);
+}
+
+// Load all data for the specified type from Mapilio vector tiles
+function loadTiles(which, url, maxZoom, projection) {
+ const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);
+ const tiles = tiler.getTiles(projection);
+
+ tiles.forEach(function(tile) {
+ loadTile(which, url, tile);
+ });
+}
+
+
+// Load all data for the specified type from one vector tile
+function loadTile(which, url, tile) {
+ const cache = _mlyCache.requests;
+ const tileId = `${tile.id}-${which}`;
+ if (cache.loaded[tileId] || cache.inflight[tileId]) return;
+ const controller = new AbortController();
+ cache.inflight[tileId] = controller;
+ const requestUrl = url.replace('{x}', tile.xyz[0])
+ .replace('{y}', tile.xyz[1])
+ .replace('{z}', tile.xyz[2]);
+
+ fetch(requestUrl, { signal: controller.signal })
+ .then(function(response) {
+ if (!response.ok) {
+ throw new Error(response.status + ' ' + response.statusText);
+ }
+ cache.loaded[tileId] = true;
+ delete cache.inflight[tileId];
+ return response.arrayBuffer();
+ })
+ .then(function(data) {
+ if (data.byteLength === 0) {
+ throw new Error('No Data');
+ }
+
+ loadTileDataToCache(data, tile, which);
+
+ if (which === 'images') {
+ dispatch.call('loadedImages');
+ } else {
+ dispatch.call('loadedLines');
+ }
+ })
+ .catch(function (e) {
+ if (e.message === 'No Data') {
+ cache.loaded[tileId] = true;
+ } else {
+ console.error(e); // eslint-disable-line no-console
+ }
+ });
+}
+
+
+// Load the data from the vector tile into cache
+function loadTileDataToCache(data, tile) {
+ const vectorTile = new VectorTile(new Protobuf(data));
+ let features,
+ cache,
+ layer,
+ i,
+ feature,
+ loc,
+ d;
+ if (vectorTile.layers.hasOwnProperty(pointLayer)) {
+ features = [];
+ cache = _mlyCache.images;
+ layer = vectorTile.layers[pointLayer];
+
+ for (i = 0; i < layer.length; i++) {
+ feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
+ loc = feature.geometry.coordinates;
+
+ let resolutionArr = feature.properties.resolution.split('x');
+ let sourceWidth = Math.max(resolutionArr[0], resolutionArr[1]);
+ let sourceHeight = Math.min(resolutionArr[0] ,resolutionArr[1]);
+ let isPano = sourceWidth % sourceHeight === 0;
+
+ d = {
+ loc: loc,
+ capture_time: feature.properties.capture_time,
+ id: feature.properties.id,
+ sequence_id: feature.properties.sequence_uuid,
+ heading: feature.properties.heading,
+ resolution: feature.properties.resolution,
+ isPano: isPano
+ };
+ cache.forImageId[d.id] = d;
+ features.push({
+ minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
+ });
+ }
+ if (cache.rtree) {
+ cache.rtree.load(features);
+ }
+ }
+
+ if (vectorTile.layers.hasOwnProperty(lineLayer)) {
+ cache = _mlyCache.sequences;
+ layer = vectorTile.layers[lineLayer];
+
+ for (i = 0; i < layer.length; i++) {
+ feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
+ if (cache.lineString[feature.properties.sequence_uuid]) {
+ cache.lineString[feature.properties.sequence_uuid].push(feature);
+ } else {
+ cache.lineString[feature.properties.sequence_uuid] = [feature];
+ }
+ }
+ }
+
+}
+
+function getImageData(imageId, sequenceId) {
+
+ return fetch(apiUrl + `/api/sequence-detail?sequence_uuid=${sequenceId}`, {method: 'GET'})
+ .then(function (response) {
+ if (!response.ok) {
+ throw new Error(response.status + ' ' + response.statusText);
+ }
+ return response.json();
+ })
+ .then(function (data) {
+ let index = data.data.findIndex((feature) => feature.id === imageId);
+ const {filename, uploaded_hash} = data.data[index];
+ _mlySceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;
+ });
+}
+
+
+export default {
+ // Initialize Mapilio
+ init: function() {
+ if (!_mlyCache) {
+ this.reset();
+ }
+
+ this.event = utilRebind(this, dispatch, 'on');
+ },
+
+ // Reset cache and state
+ reset: function() {
+ if (_mlyCache) {
+ Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });
+ }
+
+ _mlyCache = {
+ images: { rtree: new RBush(), forImageId: {} },
+ sequences: { rtree: new RBush(), lineString: {} },
+ requests: { loaded: {}, inflight: {} }
+ };
+
+ _mlyActiveImage = null;
+ },
+
+ // Get visible images
+ images: function(projection) {
+ const limit = 5;
+ return searchLimited(limit, projection, _mlyCache.images.rtree);
+ },
+
+ cachedImage: function(imageKey) {
+ return _mlyCache.images.forImageId[imageKey];
+ },
+
+
+ // Load images in the visible area
+ loadImages: function(projection) {
+ let url = baseTileUrl + pointLayer + tileStyle;
+ loadTiles('images', url, 14, projection);
+ },
+
+ // Load line in the visible area
+ loadLines: function(projection) {
+ let url = baseTileUrl + lineLayer + tileStyle;
+ loadTiles('line', url, 14, projection);
+ },
+
+ // Get visible sequences
+ sequences: function(projection) {
+ const viewport = projection.clipExtent();
+ const min = [viewport[0][0], viewport[1][1]];
+ const max = [viewport[1][0], viewport[0][1]];
+ const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
+ const sequenceIds = {};
+ let lineStrings = [];
+
+ _mlyCache.images.rtree.search(bbox)
+ .forEach(function(d) {
+ if (d.data.sequence_id) {
+ sequenceIds[d.data.sequence_id] = true;
+ }
+ });
+
+ Object.keys(sequenceIds).forEach(function(sequenceId) {
+ if (_mlyCache.sequences.lineString[sequenceId]) {
+ lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);
+ }
+ });
+
+ return lineStrings;
+ },
+
+ // Set the currently visible image
+ setActiveImage: function(image) {
+ if (image) {
+ _mlyActiveImage = {
+ id: image.id,
+ sequence_id: image.sequence_id
+ };
+ } else {
+ _mlyActiveImage = null;
+ }
+ },
+
+
+ // Update the currently highlighted sequence and selected bubble.
+ setStyles: function(context, hovered) {
+ const hoveredImageId = hovered && hovered.id;
+ const hoveredSequenceId = hovered && hovered.sequence_id;
+ const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;
+ const selectedImageId = _mlyActiveImage && _mlyActiveImage.id;
+
+ const markers = context.container().selectAll('.layer-mapilio .viewfield-group');
+ const sequences = context.container().selectAll('.layer-mapilio .sequence');
+
+ markers.classed('highlighted', function(d) { return d.id === hoveredImageId; })
+ .classed('hovered', function(d) { return d.id === hoveredImageId; })
+ .classed('currentView', function(d) { return d.id === selectedImageId; });
+
+ sequences.classed('highlighted', function(d) { return d.properties.sequence_uuid === hoveredSequenceId; })
+ .classed('currentView', function(d) { return d.properties.sequence_uuid === selectedSequenceId; });
+
+ return this;
+ },
+
+ updateUrlImage: function(imageKey) {
+ if (!window.mocha) {
+ var hash = utilStringQs(window.location.hash);
+ if (imageKey) {
+ hash.photo = 'mapilio/' + imageKey;
+ } else {
+ delete hash.photo;
+ }
+ window.location.replace('#' + utilQsString(hash, true));
+ }
+ },
+
+ initViewer: function () {
+ if (!window.pannellum) return;
+ if (_pannellumViewer) return;
+
+ _currScene += 1;
+ const sceneID = _currScene.toString();
+ const options = {
+ 'default': { firstScene: sceneID },
+ scenes: {}
+ };
+ options.scenes[sceneID] = _mlySceneOptions;
+
+ _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);
+ },
+
+ selectImage: function (context, id) {
+
+ let that = this;
+
+ let d = this.cachedImage(id);
+
+ this.setActiveImage(d);
+
+ this.updateUrlImage(d.id);
+
+ let viewer = context.container().select('.photoviewer');
+ if (!viewer.empty()) viewer.datum(d);
+
+ this.setStyles(context, null);
+
+ if (!d) return this;
+
+ let wrap = context.container().select('.photoviewer .mapilio-wrapper');
+ let attribution = wrap.selectAll('.photo-attribution').text('');
+
+ if (d.capture_time) {
+ attribution
+ .append('span')
+ .attr('class', 'captured_at')
+ .text(localeDateString(d.capture_time));
+
+ attribution
+ .append('span')
+ .text('|');
+ }
+
+ attribution
+ .append('a')
+ .attr('class', 'image-link')
+ .attr('target', '_blank')
+ .attr('href', `https://mapilio.com/app?lat=${d.loc[1]}&lng=${d.loc[0]}&zoom=17&pId=${d.id}`)
+ .text('mapilio.com');
+
+ wrap
+ .transition()
+ .duration(100)
+ .call(imgZoom.transform, d3_zoomIdentity);
+
+ wrap
+ .selectAll('img')
+ .remove();
+
+ getImageData(d.id,d.sequence_id).then(function () {
+
+ if (d.isPano) {
+ if (!_pannellumViewer) {
+ that.initViewer();
+ } else {
+ // make a new scene
+ _currScene += 1;
+ let sceneID = _currScene.toString();
+ _pannellumViewer
+ .addScene(sceneID, _mlySceneOptions)
+ .loadScene(sceneID);
+
+ // remove previous scene
+ if (_currScene > 2) {
+ sceneID = (_currScene - 1).toString();
+ _pannellumViewer
+ .removeScene(sceneID);
+ }
+ }
+ } else {
+ // make non-panoramic photo viewer
+ that.initOnlyPhoto(context);
+ }
+ });
+
+ function localeDateString(s) {
+ if (!s) return null;
+ var options = { day: 'numeric', month: 'short', year: 'numeric' };
+ var d = new Date(s);
+ if (isNaN(d.getTime())) return null;
+ return d.toLocaleDateString(localizer.localeCode(), options);
+ }
+
+ return this;
+ },
+
+ initOnlyPhoto: function (context) {
+
+ if (_pannellumViewer) {
+ _pannellumViewer.destroy();
+ _pannellumViewer = null;
+ }
+
+ let wrap = context.container().select('#ideditor-viewer-mapilio-simple');
+
+ let imgWrap = wrap.select('img');
+
+ if (!imgWrap.empty()) {
+ imgWrap.attr('src',_mlySceneOptions.panorama);
+ } else {
+ wrap.append('img')
+ .attr('src',_mlySceneOptions.panorama);
+ }
+
+ },
+
+ ensureViewerLoaded: function(context) {
+
+ let that = this;
+
+ let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');
+
+ if (!imgWrap.empty()) {
+ imgWrap.remove();
+ }
+
+ if (_loadViewerPromise) return _loadViewerPromise;
+
+ let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper')
+ .data([0]);
+
+ let wrapEnter = wrap.enter()
+ .append('div')
+ .attr('class', 'photo-wrapper mapilio-wrapper')
+ .classed('hide', true)
+ .on('dblclick.zoom', null);
+
+ wrapEnter
+ .append('div')
+ .attr('class', 'photo-attribution fillD');
+
+ const controlsEnter = wrapEnter
+ .append('div')
+ .attr('class', 'photo-controls-wrap')
+ .append('div')
+ .attr('class', 'photo-controls-mapilio');
+
+ controlsEnter
+ .append('button')
+ .on('click.back', step(-1))
+ .text('◄');
+
+ controlsEnter
+ .append('button')
+ .on('click.forward', step(1))
+ .text('►');
+
+ wrapEnter
+ .append('div')
+ .attr('id', 'ideditor-viewer-mapilio-pnlm');
+
+ wrapEnter
+ .append('div')
+ .attr('id', 'ideditor-viewer-mapilio-simple-wrap')
+ .call(imgZoom.on('zoom', zoomPan))
+ .append('div')
+ .attr('id', 'ideditor-viewer-mapilio-simple');
+
+
+
+ // Register viewer resize handler
+ context.ui().photoviewer.on('resize.mapilio', () => {
+ if (_pannellumViewer) {
+ _pannellumViewer.resize();
+ }
+ });
+
+ _loadViewerPromise = new Promise((resolve, reject) => {
+ let loadedCount = 0;
+ function loaded() {
+ loadedCount += 1;
+
+ // wait until both files are loaded
+ if (loadedCount === 2) resolve();
+ }
+
+ const head = d3_select('head');
+
+ // load pannellum-viewercss
+ head.selectAll('#ideditor-mapilio-viewercss')
+ .data([0])
+ .enter()
+ .append('link')
+ .attr('id', 'ideditor-mapilio-viewercss')
+ .attr('rel', 'stylesheet')
+ .attr('crossorigin', 'anonymous')
+ .attr('href', context.asset(pannellumViewerCSS))
+ .on('load.serviceMapilio', loaded)
+ .on('error.serviceMapilio', function() {
+ reject();
+ });
+
+ // load pannellum-viewerjs
+ head.selectAll('#ideditor-mapilio-viewerjs')
+ .data([0])
+ .enter()
+ .append('script')
+ .attr('id', 'ideditor-mapilio-viewerjs')
+ .attr('crossorigin', 'anonymous')
+ .attr('src', context.asset(pannellumViewerJS))
+ .on('load.serviceMapilio', loaded)
+ .on('error.serviceMapilio', function() {
+ reject();
+ });
+ })
+ .catch(function() {
+ _loadViewerPromise = null;
+ });
+
+ function step(stepBy) {
+ return function () {
+ if (!_mlyActiveImage) return;
+ const imageId = _mlyActiveImage.id;
+
+ const nextIndex = imageId + stepBy;
+ if (!nextIndex) return;
+
+ const nextImage = _mlyCache.images.forImageId[nextIndex];
+
+ context.map().centerEase(nextImage.loc);
+
+ that.selectImage(context, nextImage.id);
+ };
+ }
+
+ function zoomPan(d3_event) {
+ var t = d3_event.transform;
+ context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')
+ .call(utilSetTransform, t.x, t.y, t.k);
+ }
+
+ return _loadViewerPromise;
+ },
+
+ showViewer:function (context) {
+ let wrap = context.container().select('.photoviewer')
+ .classed('hide', false);
+
+ let isHidden = wrap.selectAll('.photo-wrapper.mapilio-wrapper.hide').size();
+
+ if (isHidden) {
+ wrap
+ .selectAll('.photo-wrapper:not(.mapilio-wrapper)')
+ .classed('hide', true);
+
+ wrap
+ .selectAll('.photo-wrapper.mapilio-wrapper')
+ .classed('hide', false);
+ }
+
+ return this;
+ },
+
+ /**
+ * hideViewer()
+ */
+ hideViewer: function (context) {
+ let viewer = context.container().select('.photoviewer');
+ if (!viewer.empty()) viewer.datum(null);
+
+ this.updateUrlImage(null);
+
+ viewer
+ .classed('hide', true)
+ .selectAll('.photo-wrapper')
+ .classed('hide', true);
+
+ context.container().selectAll('.viewfield-group, .sequence, .icon-sign')
+ .classed('currentView', false);
+
+ this.setActiveImage();
+
+ return this.setStyles(context, null);
+ },
+
+ // Return the current cache
+ cache: function() {
+ return _mlyCache;
+ }
+};
diff --git a/modules/svg/index.js b/modules/svg/index.js
index 733a12e9b..c7fe6ca0c 100644
--- a/modules/svg/index.js
+++ b/modules/svg/index.js
@@ -28,3 +28,4 @@ export { svgTagPattern } from './tag_pattern.js';
export { svgTouch } from './touch.js';
export { svgTurns } from './turns.js';
export { svgVertices } from './vertices.js';
+export { svgMapilioImages } from './mapilio_images.js';
diff --git a/modules/svg/layers.js b/modules/svg/layers.js
index 56c7015f5..36f6d767e 100644
--- a/modules/svg/layers.js
+++ b/modules/svg/layers.js
@@ -14,6 +14,7 @@ import { svgMapillaryPosition } from './mapillary_position';
import { svgMapillarySigns } from './mapillary_signs';
import { svgMapillaryMapFeatures } from './mapillary_map_features';
import { svgKartaviewImages } from './kartaview_images';
+import { svgMapilioImages } from './mapilio_images';
import { svgOsm } from './osm';
import { svgNotes } from './notes';
import { svgTouch } from './touch';
@@ -37,10 +38,11 @@ export function svgLayers(projection, context) {
{ id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
{ id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },
+ { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },
{ id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },
{ id: 'debug', layer: svgDebug(projection, context, dispatch) },
{ id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },
- { id: 'touch', layer: svgTouch(projection, context, dispatch) }
+ { id: 'touch', layer: svgTouch(projection, context, dispatch) },
];
diff --git a/modules/svg/mapilio_images.js b/modules/svg/mapilio_images.js
new file mode 100644
index 000000000..1f4a679a8
--- /dev/null
+++ b/modules/svg/mapilio_images.js
@@ -0,0 +1,249 @@
+import _throttle from 'lodash-es/throttle';
+
+import { select as d3_select } from 'd3-selection';
+import { services } from '../services';
+import {svgPath, svgPointTransform} from './helpers';
+
+
+export function svgMapilioImages(projection, context, dispatch) {
+ const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
+ const minZoom = 12;
+ let layer = d3_select(null);
+ let _mapilio;
+ const viewFieldZoomLevel = 18;
+
+
+ function init() {
+ if (svgMapilioImages.initialized) return;
+ svgMapilioImages.enabled = false;
+ svgMapilioImages.initialized = true;
+ }
+
+
+ function getService() {
+ if (services.mapilio && !_mapilio) {
+ _mapilio = services.mapilio;
+ _mapilio.event.on('loadedImages', throttledRedraw);
+ } else if (!services.mapilio && _mapilio) {
+ _mapilio = null;
+ }
+
+ return _mapilio;
+ }
+
+
+ function showLayer() {
+ const service = getService();
+ if (!service) return;
+
+ editOn();
+
+ layer
+ .style('opacity', 0)
+ .transition()
+ .duration(250)
+ .style('opacity', 1)
+ .on('end', function () { dispatch.call('change'); });
+ }
+
+
+ function hideLayer() {
+ throttledRedraw.cancel();
+
+ layer
+ .transition()
+ .duration(250)
+ .style('opacity', 0)
+ .on('end', editOff);
+ }
+
+ function transform(d) {
+ let t = svgPointTransform(projection)(d);
+ if (d.heading) {
+ t += ' rotate(' + Math.floor(d.heading) + ',0,0)';
+ }
+ return t;
+ }
+
+
+ function editOn() {
+ layer.style('display', 'block');
+ }
+
+
+ function editOff() {
+ layer.selectAll('.viewfield-group').remove();
+ layer.style('display', 'none');
+ }
+
+ function click(d3_event, image) {
+ const service = getService();
+ if (!service) return;
+
+ service
+ .ensureViewerLoaded(context, image.id)
+ .then(function() {
+ service
+ .selectImage(context, image.id)
+ .showViewer(context);
+ });
+
+ context.map().centerEase(image.loc);
+ }
+
+ function mouseover(d3_event, image) {
+ const service = getService();
+ if (service) service.setStyles(context, image);
+ }
+
+
+ function mouseout() {
+ const service = getService();
+ if (service) service.setStyles(context, null);
+ }
+
+ function update() {
+
+ const z = ~~context.map().zoom();
+ const showViewfields = (z >= viewFieldZoomLevel);
+
+ const service = getService();
+ let sequences = (service ? service.sequences(projection) : []);
+ let images = (service ? service.images(projection) : []);
+
+ let traces = layer.selectAll('.sequences').selectAll('.sequence')
+ .data(sequences, function(d) { return d.properties.id; });
+
+ // exit
+ traces.exit()
+ .remove();
+
+ traces.enter()
+ .append('path')
+ .attr('class', 'sequence')
+ .merge(traces)
+ .attr('d', svgPath(projection).geojson);
+
+
+ const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
+ .data(images, function(d) { return d.id; });
+
+ // exit
+ groups.exit()
+ .remove();
+
+ // enter
+ const groupsEnter = groups.enter()
+ .append('g')
+ .attr('class', 'viewfield-group')
+ .on('mouseenter', mouseover)
+ .on('mouseleave', mouseout)
+ .on('click', click);
+
+ groupsEnter
+ .append('g')
+ .attr('class', 'viewfield-scale');
+
+ // update
+ const markers = groups
+ .merge(groupsEnter)
+ .sort(function(a, b) {
+ return b.loc[1] - a.loc[1]; // sort Y
+ })
+ .attr('transform', transform)
+ .select('.viewfield-scale');
+
+
+ markers.selectAll('circle')
+ .data([0])
+ .enter()
+ .append('circle')
+ .attr('dx', '0')
+ .attr('dy', '0')
+ .attr('r', '6');
+
+ const viewfields = markers.selectAll('.viewfield')
+ .data(showViewfields ? [0] : []);
+
+ viewfields.exit()
+ .remove();
+
+ viewfields.enter()
+ .insert('path', 'circle')
+ .attr('class', 'viewfield')
+ .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')
+ .attr('d', viewfieldPath);
+
+ function viewfieldPath() {
+ if (this.parentNode.__data__.isPano) {
+ return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';
+ } else {
+ return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';
+ }
+ }
+
+ }
+
+
+ function drawImages(selection) {
+ const enabled = svgMapilioImages.enabled;
+ const service = getService();
+
+ layer = selection.selectAll('.layer-mapilio')
+ .data(service ? [0] : []);
+
+ layer.exit()
+ .remove();
+
+ const layerEnter = layer.enter()
+ .append('g')
+ .attr('class', 'layer-mapilio')
+ .style('display', enabled ? 'block' : 'none');
+
+ layerEnter
+ .append('g')
+ .attr('class', 'sequences');
+
+ layerEnter
+ .append('g')
+ .attr('class', 'markers');
+
+ layer = layerEnter
+ .merge(layer);
+
+ if (enabled) {
+ if (service && ~~context.map().zoom() >= minZoom) {
+ editOn();
+ update();
+ service.loadImages(projection);
+ service.loadLines(projection);
+ } else {
+ editOff();
+ }
+ }
+ }
+
+
+ drawImages.enabled = function(_) {
+ if (!arguments.length) return svgMapilioImages.enabled;
+ svgMapilioImages.enabled = _;
+ if (svgMapilioImages.enabled) {
+ showLayer();
+ context.photos().on('change.mapilio_images', null);
+ } else {
+ hideLayer();
+ context.photos().on('change.mapilio_images', null);
+ }
+ dispatch.call('change');
+ return this;
+ };
+
+
+ drawImages.supported = function() {
+ return !!getService();
+ };
+
+
+ init();
+ return drawImages;
+}
diff --git a/modules/ui/field.js b/modules/ui/field.js
index b75680d7f..c7f0f2ca5 100644
--- a/modules/ui/field.js
+++ b/modules/ui/field.js
@@ -204,7 +204,11 @@ export function uiField(context, presetField, entityIDs, options) {
referenceKey = referenceKey.replace(/:$/, '');
}
- reference = uiTagReference(d.reference || { key: referenceKey }, context);
+ var referenceOptions = d.reference || {
+ key: referenceKey,
+ value: _tags[referenceKey]
+ };
+ reference = uiTagReference(referenceOptions, context);
if (_state === 'hover') {
reference.showing(false);
}
diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js
index 78066bd70..4aaa05b50 100644
--- a/modules/ui/fields/combo.js
+++ b/modules/ui/fields/combo.js
@@ -483,7 +483,7 @@ export function uiFieldCombo(field, context) {
.attr('type', 'text')
.attr('id', field.domId)
.call(utilNoAuto)
- .call(initCombo, selection)
+ .call(initCombo, _container)
.merge(_input);
if (_isSemi) {
diff --git a/modules/ui/init.js b/modules/ui/init.js
index 79b4cd57c..8616dcd98 100644
--- a/modules/ui/init.js
+++ b/modules/ui/init.js
@@ -1,3 +1,4 @@
+import { marked } from 'marked';
import {
select as d3_select
} from 'd3-selection';
@@ -689,5 +690,10 @@ export function uiInit(context) {
_saveLoading = d3_select(null);
});
+ marked.use({
+ mangle: false,
+ headerIds: false,
+ });
+
return ui;
}
diff --git a/modules/ui/photoviewer.js b/modules/ui/photoviewer.js
index 6876aec7a..0d1240ff8 100644
--- a/modules/ui/photoviewer.js
+++ b/modules/ui/photoviewer.js
@@ -24,6 +24,7 @@ export function uiPhotoviewer(context) {
if (services.streetside) { services.streetside.hideViewer(context); }
if (services.mapillary) { services.mapillary.hideViewer(context); }
if (services.kartaview) { services.kartaview.hideViewer(context); }
+ if (services.mapilio) { services.mapilio.hideViewer(context); }
if (services.vegbilder) { services.vegbilder.hideViewer(context); }
})
.append('div')
diff --git a/package-lock.json b/package-lock.json
index 9cf230137..b23e6ce55 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9878,9 +9878,10 @@
}
},
"node_modules/word-wrap": {
- "version": "1.2.3",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -16685,7 +16686,9 @@
}
},
"word-wrap": {
- "version": "1.2.3",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
+ "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wordwrap": {
diff --git a/svg/fontawesome/fas-bowl-rice.svg b/svg/fontawesome/fas-bowl-rice.svg
new file mode 100644
index 000000000..ecb974b82
--- /dev/null
+++ b/svg/fontawesome/fas-bowl-rice.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-building-flag.svg b/svg/fontawesome/fas-building-flag.svg
new file mode 100644
index 000000000..b65fb056d
--- /dev/null
+++ b/svg/fontawesome/fas-building-flag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-person-arrow-up-from-line.svg b/svg/fontawesome/fas-person-arrow-up-from-line.svg
new file mode 100644
index 000000000..32cd12381
--- /dev/null
+++ b/svg/fontawesome/fas-person-arrow-up-from-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-plate-wheat.svg b/svg/fontawesome/fas-plate-wheat.svg
new file mode 100644
index 000000000..d1927b0ff
--- /dev/null
+++ b/svg/fontawesome/fas-plate-wheat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-plug-circle-bolt.svg b/svg/fontawesome/fas-plug-circle-bolt.svg
new file mode 100644
index 000000000..35423deca
--- /dev/null
+++ b/svg/fontawesome/fas-plug-circle-bolt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/fontawesome/fas-triangle-exclamation.svg b/svg/fontawesome/fas-triangle-exclamation.svg
new file mode 100644
index 000000000..00bba534d
--- /dev/null
+++ b/svg/fontawesome/fas-triangle-exclamation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js
index d92f96ffa..bfec1c9ce 100644
--- a/test/spec/svg/layers.js
+++ b/test/spec/svg/layers.js
@@ -39,6 +39,7 @@ describe('iD.svgLayers', function () {
expect(d3.select(nodes[9]).classed('mapillary-map-features')).to.be.true;
expect(d3.select(nodes[10]).classed('mapillary-signs')).to.be.true;
expect(d3.select(nodes[11]).classed('kartaview')).to.be.true;
+ expect(d3.select(nodes[12]).classed('mapilio')).to.be.true;
expect(d3.select(nodes[12]).classed('vegbilder')).to.be.true;
expect(d3.select(nodes[13]).classed('debug')).to.be.true;
expect(d3.select(nodes[14]).classed('geolocate')).to.be.true;