From 7c90e2d6af971a0cb2f0fce2a33d0879d9f1c330 Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Wed, 24 May 2023 14:54:56 +0300 Subject: [PATCH 01/14] first commit --- modules/renderer/background.js | 3 +- modules/renderer/photos.js | 2 +- modules/services/index.js | 7 +- modules/services/mapilio.js | 200 +++++++++++++++++++++++++++++++++ modules/svg/index.js | 1 + modules/svg/layers.js | 4 +- modules/svg/mapilio_images.js | 129 +++++++++++++++++++++ 7 files changed, 341 insertions(+), 5 deletions(-) create mode 100644 modules/services/mapilio.js create mode 100644 modules/svg/mapilio_images.js diff --git a/modules/renderer/background.js b/modules/renderer/background.js index e6ee5d051..236dfc517 100644 --- a/modules/renderer/background.js +++ b/modules/renderer/background.js @@ -251,7 +251,8 @@ export function rendererBackground(context) { mapillary: 'Mapillary Images', 'mapillary-map-features': 'Mapillary Map Features', 'mapillary-signs': 'Mapillary Signs', - kartaview: 'KartaView Images' + kartaview: 'KartaView Images', + mapilio: 'Mapilio Images' }; for (let layerID in photoOverlayLayers) { diff --git a/modules/renderer/photos.js b/modules/renderer/photos.js index ee2705b84..227b4b20a 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']; + var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio']; 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 6b1784da7..108412f4e 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -13,6 +13,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 = { @@ -30,7 +31,8 @@ export let services = { taginfo: serviceTaginfo, vectorTile: serviceVectorTile, wikidata: serviceWikidata, - wikipedia: serviceWikipedia + wikipedia: serviceWikipedia, + mapilio: serviceMapilio }; export { @@ -48,5 +50,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..40a56c8fd --- /dev/null +++ b/modules/services/mapilio.js @@ -0,0 +1,200 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; + +import Protobuf from 'pbf'; +import RBush from 'rbush'; +import { VectorTile } from '@mapbox/vector-tile'; + +import { utilRebind, utilTiler } from '../util'; + +const baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:'; +const pointsTileUrl = `${baseTileUrl}points_mapilio_map&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('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged'); + +let _mlyActiveImage; +let _mlyCache; + + +// 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) { + throw new Error('No Data'); + } + + loadTileDataToCache(data, tile, which); + + if (which === 'images') { + dispatch.call('loadedImages'); + } + }) + .catch(function() { + cache.loaded[tileId] = true; + delete cache.inflight[tileId]; + }); +} + + +// Load the data from the vector tile into cache +function loadTileDataToCache(data, tile, which) { + const vectorTile = new VectorTile(new Protobuf(data)); + let features, + cache, + layer, + i, + feature, + loc, + d; + if (vectorTile.layers.hasOwnProperty('points_mapilio_map')) { + features = []; + cache = _mlyCache.images; + layer = vectorTile.layers.points_mapilio_map; + + 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; + d = { + loc: loc, + captured_at: feature.properties.captured_at, + created_at: feature.properties.created_at, + id: feature.properties.id, + sequence_id: feature.properties.sequence_uuid, + }; + 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('sequence')) { + features = []; + cache = _mlyCache.sequences; + layer = vectorTile.layers.sequence; + + 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.id]) { + cache.lineString[feature.properties.id].push(feature); + } else { + cache.lineString[feature.properties.id] = [feature]; + } + } + } + + if (vectorTile.layers.hasOwnProperty('point')) { + features = []; + cache = _mlyCache[which]; + layer = vectorTile.layers.point; + + 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; + + d = { + loc: loc, + id: feature.properties.id, + first_seen_at: feature.properties.first_seen_at, + last_seen_at: feature.properties.last_seen_at, + value: feature.properties.value + }; + features.push({ + minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d + }); + } + if (cache.rtree) { + cache.rtree.load(features); + } + } + +} + + +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; + }, + + + // Load images in the visible area + loadImages: function(projection) { + loadTiles('images', pointsTileUrl, 14, projection); + }, + + + // 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; + + context.container().selectAll('.layer-mapilio .viewfield-group') + .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); }) + .classed('hovered', function(d) { return d.id === hoveredImageId; }); + + context.container().selectAll('.layer-mapilio .sequence') + .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; }) + .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; }); + + return this; + }, + + + // Return the current cache + cache: function() { + return _mlyCache; + } +}; diff --git a/modules/svg/index.js b/modules/svg/index.js index e2c94c316..798afe3f7 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -27,3 +27,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 169805c4c..f36c5104a 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -13,6 +13,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'; @@ -38,7 +39,8 @@ export function svgLayers(projection, context) { { id: 'kartaview', layer: svgKartaviewImages(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) }, + { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) } ]; diff --git a/modules/svg/mapilio_images.js b/modules/svg/mapilio_images.js new file mode 100644 index 000000000..7724ee126 --- /dev/null +++ b/modules/svg/mapilio_images.js @@ -0,0 +1,129 @@ +import _throttle from 'lodash-es/throttle'; + +import { select as d3_select } from 'd3-selection'; +import { services } from '../services'; + + +export function svgMapilioImages(projection, context, dispatch) { + const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + const minZoom = 12; + let layer = d3_select(null); + let _mapilio; + + + 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 editOn() { + layer.style('display', 'block'); + } + + + function editOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + + 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(); + service.loadImages(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; +} From d92afe3dc7a6d7cde5318362759cec2a493cc88b Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Wed, 24 May 2023 18:08:24 +0300 Subject: [PATCH 02/14] add point and line --- css/60_photos.css | 12 ++++ modules/services/mapilio.js | 105 +++++++++++++++++++++++----------- modules/svg/mapilio_images.js | 84 +++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 32 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index d3f2f1f96..18a7c37a9 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -244,6 +244,18 @@ stroke: #20c4ff; } +/* Mapilio Image Layer */ +.layer-mapilio { + pointer-events: none; +} +.layer-mapilio .viewfield-group * { + fill: #0056f1; + stroke: #ffffff; + fill-opacity: .7; +} +.layer-mapilio .sequence { + stroke: #0056f1; +} /* Streetside Viewer (pannellum) */ .ms-wrapper .photo-attribution .image-link { diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js index 40a56c8fd..67250ce1e 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -5,9 +5,12 @@ import RBush from 'rbush'; import { VectorTile } from '@mapbox/vector-tile'; import { utilRebind, utilTiler } from '../util'; +import {geoExtent, geoScaleToZoom} from '../geo'; const baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:'; -const pointsTileUrl = `${baseTileUrl}points_mapilio_map&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}`; +const pointLayer = 'points_mapilio_map'; +const lineLayer = 'captured_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('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged'); @@ -16,6 +19,31 @@ let _mlyActiveImage; let _mlyCache; +// 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); @@ -100,46 +128,21 @@ function loadTileDataToCache(data, tile, which) { } } - if (vectorTile.layers.hasOwnProperty('sequence')) { + if (vectorTile.layers.hasOwnProperty('captured_roads_line')) { features = []; cache = _mlyCache.sequences; - layer = vectorTile.layers.sequence; + layer = vectorTile.layers.captured_roads_line; 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.id]) { - cache.lineString[feature.properties.id].push(feature); + if (cache.lineString[feature.properties.sequence_uuid]) { + cache.lineString[feature.properties.sequence_uuid].push(feature); } else { - cache.lineString[feature.properties.id] = [feature]; + cache.lineString[feature.properties.sequence_uuid] = [feature]; } } } - if (vectorTile.layers.hasOwnProperty('point')) { - features = []; - cache = _mlyCache[which]; - layer = vectorTile.layers.point; - - 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; - - d = { - loc: loc, - id: feature.properties.id, - first_seen_at: feature.properties.first_seen_at, - last_seen_at: feature.properties.last_seen_at, - value: feature.properties.value - }; - features.push({ - minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d - }); - } - if (cache.rtree) { - cache.rtree.load(features); - } - } - } @@ -168,10 +171,48 @@ export default { _mlyActiveImage = null; }, + // Get visible images + images: function(projection) { + const limit = 5; + return searchLimited(limit, projection, _mlyCache.images.rtree); + }, + // Load images in the visible area loadImages: function(projection) { - loadTiles('images', pointsTileUrl, 14, 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; }, diff --git a/modules/svg/mapilio_images.js b/modules/svg/mapilio_images.js index 7724ee126..13d383321 100644 --- a/modules/svg/mapilio_images.js +++ b/modules/svg/mapilio_images.js @@ -2,6 +2,7 @@ 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) { @@ -55,6 +56,14 @@ export function svgMapilioImages(projection, context, dispatch) { .on('end', editOff); } + function transform(d) { + let t = svgPointTransform(projection)(d); + if (d.ca) { + t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + } + return t; + } + function editOn() { layer.style('display', 'block'); @@ -66,6 +75,79 @@ export function svgMapilioImages(projection, context, dispatch) { layer.style('display', 'none'); } + function update() { + + const z = ~~context.map().zoom(); + + const service = getService(); + let sequences = (service ? service.sequences(projection) : []); + let images = (service ? service.images(projection) : []); + + + // service.filterViewer(context); + + let traces = layer.selectAll('.sequences').selectAll('.sequence') + .data(sequences, function(d) { return d.properties.id; }); + + // exit + traces.exit() + .remove(); + // + // // enter/update + traces = 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'); + + 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([0]); + + viewfields.exit() + .remove(); + + viewfields.enter() // viewfields may or may not be drawn... + .insert('path', 'circle') // but if they are, draw below the circles + .attr('class', 'viewfield') + .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + + } + function drawImages(selection) { const enabled = svgMapilioImages.enabled; @@ -96,7 +178,9 @@ export function svgMapilioImages(projection, context, dispatch) { if (enabled) { if (service && ~~context.map().zoom() >= minZoom) { editOn(); + update(); service.loadImages(projection); + service.loadLines(projection); } else { editOff(); } From 9876e92377e33c6d8ca79176d25cf063045a3332 Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Fri, 26 May 2023 18:16:49 +0300 Subject: [PATCH 03/14] add hover and click event --- css/60_photos.css | 3 +- data/core.yaml | 3 + modules/services/mapilio.js | 233 ++++++++++++++++++++++++++++++++-- modules/svg/mapilio_images.js | 57 +++++++-- modules/ui/photoviewer.js | 1 + 5 files changed, 277 insertions(+), 20 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 18a7c37a9..ffd22f481 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -251,7 +251,8 @@ .layer-mapilio .viewfield-group * { fill: #0056f1; stroke: #ffffff; - fill-opacity: .7; + stroke-opacity: .6; + fill-opacity: .6; } .layer-mapilio .sequence { stroke: #0056f1; diff --git a/data/core.yaml b/data/core.yaml index 37e18ea69..03fc38bee 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1432,6 +1432,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/services/mapilio.js b/modules/services/mapilio.js index 67250ce1e..d3a1564ac 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -1,4 +1,5 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { select as d3_select } from 'd3-selection'; import Protobuf from 'pbf'; import RBush from 'rbush'; @@ -7,16 +8,32 @@ import { VectorTile } from '@mapbox/vector-tile'; import { utilRebind, utilTiler } from '../util'; import {geoExtent, geoScaleToZoom} from '../geo'; +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 = 'points_mapilio_map'; const lineLayer = 'captured_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('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged'); +const dispatch = d3_dispatch('change', 'loadedImages', 'loadedLine'); +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 @@ -84,6 +101,8 @@ function loadTile(which, url, tile) { if (which === 'images') { dispatch.call('loadedImages'); + } else { + dispatch.call('loadedLines'); } }) .catch(function() { @@ -94,7 +113,7 @@ function loadTile(which, url, tile) { // Load the data from the vector tile into cache -function loadTileDataToCache(data, tile, which) { +function loadTileDataToCache(data, tile) { const vectorTile = new VectorTile(new Protobuf(data)); let features, cache, @@ -113,10 +132,10 @@ function loadTileDataToCache(data, tile, which) { loc = feature.geometry.coordinates; d = { loc: loc, - captured_at: feature.properties.captured_at, created_at: feature.properties.created_at, id: feature.properties.id, sequence_id: feature.properties.sequence_uuid, + heading:feature.properties.heading }; cache.forImageId[d.id] = d; features.push({ @@ -129,7 +148,6 @@ function loadTileDataToCache(data, tile, which) { } if (vectorTile.layers.hasOwnProperty('captured_roads_line')) { - features = []; cache = _mlyCache.sequences; layer = vectorTile.layers.captured_roads_line; @@ -145,6 +163,22 @@ function loadTileDataToCache(data, tile, which) { } +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 @@ -177,6 +211,10 @@ export default { return searchLimited(limit, projection, _mlyCache.images.rtree); }, + cachedImage: function(imageKey) { + return _mlyCache.images.forImageId[imageKey]; + }, + // Load images in the visible area loadImages: function(projection) { @@ -215,24 +253,199 @@ export default { 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; - context.container().selectAll('.layer-mapilio .viewfield-group') - .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); }) - .classed('hovered', function(d) { return d.id === hoveredImageId; }); + const markers = context.container().selectAll('.layer-mapilio .viewfield-group'); + const sequences = context.container().selectAll('.layer-mapilio .sequence'); - context.container().selectAll('.layer-mapilio .sequence') - .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; }) - .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; }); + 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; }, + 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', options); + }, + + selectImage: function (context, id) { + + let that = this; + + let d = this.cachedImage(id); + + this.setActiveImage(d); + + let viewer = context.container().select('.photoviewer'); + if (!viewer.empty()) viewer.datum(d); + + this.setStyles(context, null); + + if (!d) return this; + + getImageData(d.id,d.sequence_id).then(function () { + + 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); + } + } + }); + + return this; + }, + + ensureViewerLoaded: function(context) { + 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); + + wrapEnter + .append('div') + .attr('id', 'ideditor-viewer-mapilio'); + + + // 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; + }); + + 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); + + 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() { diff --git a/modules/svg/mapilio_images.js b/modules/svg/mapilio_images.js index 13d383321..386055cc4 100644 --- a/modules/svg/mapilio_images.js +++ b/modules/svg/mapilio_images.js @@ -10,6 +10,7 @@ export function svgMapilioImages(projection, context, dispatch) { const minZoom = 12; let layer = d3_select(null); let _mapilio; + const viewFieldZoomLevel = 18; function init() { @@ -58,8 +59,8 @@ export function svgMapilioImages(projection, context, dispatch) { function transform(d) { let t = svgPointTransform(projection)(d); - if (d.ca) { - t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + if (d.heading) { + t += ' rotate(' + Math.floor(d.heading) + ',0,0)'; } return t; } @@ -75,9 +76,36 @@ export function svgMapilioImages(projection, context, dispatch) { layer.style('display', 'none'); } + function click(d3_event, image) { + const service = getService(); + if (!service) return; + + service + .ensureViewerLoaded(context) + .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) : []); @@ -92,9 +120,8 @@ export function svgMapilioImages(projection, context, dispatch) { // exit traces.exit() .remove(); - // - // // enter/update - traces = traces.enter() + + traces.enter() .append('path') .attr('class', 'sequence') .merge(traces) @@ -111,7 +138,10 @@ export function svgMapilioImages(projection, context, dispatch) { // enter const groupsEnter = groups.enter() .append('g') - .attr('class', 'viewfield-group'); + .attr('class', 'viewfield-group') + .on('mouseenter', mouseover) + .on('mouseleave', mouseout) + .on('click', click); groupsEnter .append('g') @@ -136,15 +166,24 @@ export function svgMapilioImages(projection, context, dispatch) { .attr('r', '6'); const viewfields = markers.selectAll('.viewfield') - .data([0]); + .data(showViewfields ? [0] : []); viewfields.exit() .remove(); - viewfields.enter() // viewfields may or may not be drawn... - .insert('path', 'circle') // but if they are, draw below the circles + 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__.is_pano) { + 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'; + } + } } diff --git a/modules/ui/photoviewer.js b/modules/ui/photoviewer.js index 51b92b7be..663a29360 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); } }) .append('div') .call(svgIcon('#iD-icon-close')); From 1a8ba8e3564789b7a0a1d7edc66bae11fd1312f7 Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Sat, 27 May 2023 01:11:34 +0300 Subject: [PATCH 04/14] fixed test error --- modules/svg/layers.js | 2 +- test/spec/svg/layers.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/svg/layers.js b/modules/svg/layers.js index f36c5104a..9f5e9a503 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -37,10 +37,10 @@ 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: 'debug', layer: svgDebug(projection, context, dispatch) }, { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) }, { id: 'touch', layer: svgTouch(projection, context, dispatch) }, - { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) } ]; diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 2c54936d7..22c1da3ea 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(15); + expect(nodes.length).to.eql(16); 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; @@ -39,9 +39,10 @@ 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('debug')).to.be.true; - expect(d3.select(nodes[13]).classed('geolocate')).to.be.true; - expect(d3.select(nodes[14]).classed('touch')).to.be.true; + expect(d3.select(nodes[12]).classed('mapilio')).to.be.true; + expect(d3.select(nodes[13]).classed('debug')).to.be.true; + expect(d3.select(nodes[14]).classed('geolocate')).to.be.true; + expect(d3.select(nodes[15]).classed('touch')).to.be.true; }); }); From 4616cd302f1d8e321db7f056be690d83ba060dee Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Thu, 13 Jul 2023 17:49:23 +0300 Subject: [PATCH 05/14] fixed pano screen and changed map layers name --- css/60_photos.css | 8 +++ modules/services/mapilio.js | 122 +++++++++++++++++++++++++++------- modules/svg/mapilio_images.js | 5 +- 3 files changed, 108 insertions(+), 27 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index ffd22f481..ee43cf558 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -258,6 +258,14 @@ stroke: #0056f1; } +.ideditor .mapilio-wrapper { + position: relative; + background-color: #000; + background-image: url(img/loader-black.gif); + background-position: center; + background-repeat: no-repeat; +} + /* Streetside Viewer (pannellum) */ .ms-wrapper .photo-attribution .image-link { display: block; diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js index d3a1564ac..f963b309e 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -7,12 +7,13 @@ import { VectorTile } from '@mapbox/vector-tile'; import { utilRebind, utilTiler } 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 = 'points_mapilio_map'; -const lineLayer = 'captured_roads_line'; +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; @@ -122,20 +123,28 @@ function loadTileDataToCache(data, tile) { feature, loc, d; - if (vectorTile.layers.hasOwnProperty('points_mapilio_map')) { + if (vectorTile.layers.hasOwnProperty(pointLayer)) { features = []; cache = _mlyCache.images; - layer = vectorTile.layers.points_mapilio_map; + 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, - created_at: feature.properties.created_at, + capture_time: feature.properties.capture_time, id: feature.properties.id, sequence_id: feature.properties.sequence_uuid, - heading:feature.properties.heading + heading: feature.properties.heading, + resolution: feature.properties.resolution, + isPano: isPano }; cache.forImageId[d.id] = d; features.push({ @@ -147,9 +156,9 @@ function loadTileDataToCache(data, tile) { } } - if (vectorTile.layers.hasOwnProperty('captured_roads_line')) { + if (vectorTile.layers.hasOwnProperty(lineLayer)) { cache = _mlyCache.sequences; - layer = vectorTile.layers.captured_roads_line; + 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]); @@ -316,31 +325,94 @@ export default { 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'); + getImageData(d.id,d.sequence_id).then(function () { - 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(); + if (d.isPano){ + if (!_pannellumViewer) { + that.initViewer(); + } else { + // make a new scene + _currScene += 1; + let sceneID = _currScene.toString(); _pannellumViewer - .removeScene(sceneID); + .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; }, - ensureViewerLoaded: function(context) { + initOnlyPhoto: function (context) { + + if (_pannellumViewer) { + _pannellumViewer.destroy(); + _pannellumViewer = null; + } + + let wrap = context.container().select('#ideditor-viewer-mapilio'); + + let imgWrap = wrap.select('img'); + + wrap.style('height','100%'); + + if (!imgWrap.empty()){ + imgWrap.attr('src',_mlySceneOptions.panorama); + } else { + wrap.append('img') + .attr('src',_mlySceneOptions.panorama); + } + + }, + + ensureViewerLoaded: function(context,) { + + let imgWrap = context.container().select('#ideditor-viewer-mapilio > img'); + + if (!imgWrap.empty()) { + imgWrap.remove(); + } + if (_loadViewerPromise) return _loadViewerPromise; let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper') @@ -351,6 +423,10 @@ export default { .attr('class', 'photo-wrapper mapilio-wrapper') .classed('hide', true); + wrapEnter + .append('div') + .attr('class', 'photo-attribution fillD'); + wrapEnter .append('div') .attr('id', 'ideditor-viewer-mapilio'); diff --git a/modules/svg/mapilio_images.js b/modules/svg/mapilio_images.js index 386055cc4..ddb8dc5ac 100644 --- a/modules/svg/mapilio_images.js +++ b/modules/svg/mapilio_images.js @@ -81,7 +81,7 @@ export function svgMapilioImages(projection, context, dispatch) { if (!service) return; service - .ensureViewerLoaded(context) + .ensureViewerLoaded(context, image.id) .then(function() { service .selectImage(context, image.id) @@ -111,9 +111,6 @@ export function svgMapilioImages(projection, context, dispatch) { let sequences = (service ? service.sequences(projection) : []); let images = (service ? service.images(projection) : []); - - // service.filterViewer(context); - let traces = layer.selectAll('.sequences').selectAll('.sequence') .data(sequences, function(d) { return d.properties.id; }); From a84ba5967883591edb777ed29358c9c0697fcfcc Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Mon, 17 Jul 2023 10:52:10 +0300 Subject: [PATCH 06/14] Added change image forward and backward feature --- css/60_photos.css | 11 +++++++++++ modules/services/mapilio.js | 36 ++++++++++++++++++++++++++++++++++- modules/svg/mapilio_images.js | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index ee43cf558..bae76039e 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -257,6 +257,17 @@ .layer-mapilio .sequence { stroke: #0056f1; } +.photo-controls-mapilio { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + pointer-events: initial; +} + +.photo-controls-mapilio button{ + padding:0 6px; +} .ideditor .mapilio-wrapper { position: relative; diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js index f963b309e..251be5294 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -405,7 +405,9 @@ export default { }, - ensureViewerLoaded: function(context,) { + ensureViewerLoaded: function(context) { + + let that = this; let imgWrap = context.container().select('#ideditor-viewer-mapilio > img'); @@ -427,6 +429,22 @@ export default { .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'); @@ -481,6 +499,22 @@ export default { _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); + }; + } + return _loadViewerPromise; }, diff --git a/modules/svg/mapilio_images.js b/modules/svg/mapilio_images.js index ddb8dc5ac..1f4a679a8 100644 --- a/modules/svg/mapilio_images.js +++ b/modules/svg/mapilio_images.js @@ -175,7 +175,7 @@ export function svgMapilioImages(projection, context, dispatch) { .attr('d', viewfieldPath); function viewfieldPath() { - if (this.parentNode.__data__.is_pano) { + 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'; From 3159c87ae17b31da87a7d9d929d0123fab1353d1 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Fri, 21 Jul 2023 09:34:20 +0200 Subject: [PATCH 07/14] disable deprecated `marked` options to get rid of warnings in the console --- modules/ui/init.js | 6 ++++++ 1 file changed, 6 insertions(+) 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; } From ad1e0357c6a3a67488bdb51e529b9f6503cda90a Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Fri, 21 Jul 2023 09:39:47 +0200 Subject: [PATCH 08/14] update assets --- svg/fontawesome/fas-bowl-rice.svg | 1 + svg/fontawesome/fas-building-flag.svg | 1 + svg/fontawesome/fas-person-arrow-up-from-line.svg | 1 + svg/fontawesome/fas-plate-wheat.svg | 1 + svg/fontawesome/fas-plug-circle-bolt.svg | 1 + svg/fontawesome/fas-triangle-exclamation.svg | 1 + 6 files changed, 6 insertions(+) create mode 100644 svg/fontawesome/fas-bowl-rice.svg create mode 100644 svg/fontawesome/fas-building-flag.svg create mode 100644 svg/fontawesome/fas-person-arrow-up-from-line.svg create mode 100644 svg/fontawesome/fas-plate-wheat.svg create mode 100644 svg/fontawesome/fas-plug-circle-bolt.svg create mode 100644 svg/fontawesome/fas-triangle-exclamation.svg 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 From ead86b89bd414066094cef0c509a98c731ef4f88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Jul 2023 09:40:40 +0200 Subject: [PATCH 09/14] Bump word-wrap from 1.2.3 to 1.2.4 (#9787) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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": { From 68c0bd7d46e4aa53a8b5a1fdcff21fdb1c4858ac Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Fri, 21 Jul 2023 10:11:00 +0200 Subject: [PATCH 10/14] fix margins of tag reference info --- css/80_app.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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; From 61af54c069fa11c6968c754c06ed241cd9f67d58 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Fri, 21 Jul 2023 10:44:21 +0200 Subject: [PATCH 11/14] fix combo field's dropdown when tag reference info is displayed show it immediately after the input field instead of after the tag documentation text --- modules/ui/fields/combo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From 46a2d6365f5609f66cfffe8d8b018516b82443d4 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Fri, 21 Jul 2023 11:03:28 +0200 Subject: [PATCH 12/14] show tag reference info for the currently filled-in tag value, closes #9786 --- CHANGELOG.md | 2 ++ modules/ui/field.js | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e44772fe..dd621a530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ _Breaking developer changes, which may affect downstream projects or sites that #### :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 +54,7 @@ _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 # 2.26.2 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); } From c70abfa3d1913a95d5fbf55af71ee5c63c3a70d0 Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Fri, 21 Jul 2023 14:47:26 +0300 Subject: [PATCH 13/14] added zoom, photo hash-parameter and some fixes --- css/60_photos.css | 5 ++++ modules/services/mapilio.js | 55 ++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 80f4f0138..576702263 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -276,6 +276,11 @@ background-position: center; background-repeat: no-repeat; } +#ideditor-viewer-mapilio{ + width: 100%; + height: 100%; + transform-origin: 0 0; +} /* Streetside Viewer (pannellum) */ .ms-wrapper .photo-attribution .image-link { diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js index 251be5294..b4df139c1 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -1,11 +1,12 @@ 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 } from '../util'; +import { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util'; import {geoExtent, geoScaleToZoom} from '../geo'; import {localizer} from '../core/localizer'; @@ -17,7 +18,11 @@ 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('change', 'loadedImages', 'loadedLine'); +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; @@ -94,7 +99,7 @@ function loadTile(which, url, tile) { return response.arrayBuffer(); }) .then(function(data) { - if (!data) { + if (data.byteLength === 0) { throw new Error('No Data'); } @@ -106,9 +111,12 @@ function loadTile(which, url, tile) { dispatch.call('loadedLines'); } }) - .catch(function() { - cache.loaded[tileId] = true; - delete cache.inflight[tileId]; + .catch(function (e) { + if (e.message === 'No Data') { + cache.loaded[tileId] = true; + } else { + console.log(e); + } }); } @@ -295,6 +303,18 @@ export default { 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; @@ -318,6 +338,8 @@ export default { this.setActiveImage(d); + this.updateUrlImage(d.id); + let viewer = context.container().select('.photoviewer'); if (!viewer.empty()) viewer.datum(d); @@ -346,6 +368,15 @@ export default { .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){ @@ -423,7 +454,9 @@ export default { let wrapEnter = wrap.enter() .append('div') .attr('class', 'photo-wrapper mapilio-wrapper') - .classed('hide', true); + .classed('hide', true) + .call(imgZoom.on('zoom', zoomPan)) + .on('dblclick.zoom', null); wrapEnter .append('div') @@ -515,6 +548,12 @@ export default { }; } + function zoomPan(d3_event) { + var t = d3_event.transform; + context.container().select('.photoviewer #ideditor-viewer-mapilio') + .call(utilSetTransform, t.x, t.y, t.k); + } + return _loadViewerPromise; }, @@ -544,6 +583,8 @@ export default { let viewer = context.container().select('.photoviewer'); if (!viewer.empty()) viewer.datum(null); + this.updateUrlImage(null); + viewer .classed('hide', true) .selectAll('.photo-wrapper') From ad1162dd17e51fff2d12c58091a2ae6597ec1973 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Tue, 1 Aug 2023 16:55:41 +0200 Subject: [PATCH 14/14] =?UTF-8?q?fix=20panning=20in=20mapilio=20360=C2=B0?= =?UTF-8?q?=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/60_photos.css | 5 ++++- modules/services/mapilio.js | 25 +++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 88baa03a0..438f6ad2c 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -276,7 +276,10 @@ background-position: center; background-repeat: no-repeat; } -#ideditor-viewer-mapilio { +#ideditor-viewer-mapilio-simple-wrap { + height: 100%; +} +#ideditor-viewer-mapilio-simple { width: 100%; height: 100%; transform-origin: 0 0; diff --git a/modules/services/mapilio.js b/modules/services/mapilio.js index 3f69a40b8..31acbbbbc 100644 --- a/modules/services/mapilio.js +++ b/modules/services/mapilio.js @@ -327,7 +327,7 @@ export default { }; options.scenes[sceneID] = _mlySceneOptions; - _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio', options); + _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options); }, selectImage: function (context, id) { @@ -379,7 +379,7 @@ export default { getImageData(d.id,d.sequence_id).then(function () { - if (d.isPano){ + if (d.isPano) { if (!_pannellumViewer) { that.initViewer(); } else { @@ -421,13 +421,11 @@ export default { _pannellumViewer = null; } - let wrap = context.container().select('#ideditor-viewer-mapilio'); + let wrap = context.container().select('#ideditor-viewer-mapilio-simple'); let imgWrap = wrap.select('img'); - wrap.style('height','100%'); - - if (!imgWrap.empty()){ + if (!imgWrap.empty()) { imgWrap.attr('src',_mlySceneOptions.panorama); } else { wrap.append('img') @@ -440,7 +438,7 @@ export default { let that = this; - let imgWrap = context.container().select('#ideditor-viewer-mapilio > img'); + let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img'); if (!imgWrap.empty()) { imgWrap.remove(); @@ -455,7 +453,6 @@ export default { .append('div') .attr('class', 'photo-wrapper mapilio-wrapper') .classed('hide', true) - .call(imgZoom.on('zoom', zoomPan)) .on('dblclick.zoom', null); wrapEnter @@ -480,7 +477,15 @@ export default { wrapEnter .append('div') - .attr('id', 'ideditor-viewer-mapilio'); + .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 @@ -550,7 +555,7 @@ export default { function zoomPan(d3_event) { var t = d3_event.transform; - context.container().select('.photoviewer #ideditor-viewer-mapilio') + context.container().select('.photoviewer #ideditor-viewer-mapilio-simple') .call(utilSetTransform, t.x, t.y, t.k); }