From 9876e92377e33c6d8ca79176d25cf063045a3332 Mon Sep 17 00:00:00 2001 From: sezerbozbiyik Date: Fri, 26 May 2023 18:16:49 +0300 Subject: [PATCH] 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'));