diff --git a/modules/services/pannellum_photo.js b/modules/services/pannellum_photo.js new file mode 100644 index 000000000..f28b34753 --- /dev/null +++ b/modules/services/pannellum_photo.js @@ -0,0 +1,162 @@ +import { select as d3_select } from 'd3-selection'; +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { utilRebind } from '../util'; + + +const pannellumViewerCSS = 'pannellum/pannellum.css'; +const pannellumViewerJS = 'pannellum/pannellum.js'; +const dispatch = d3_dispatch('viewerChanged'); + +let _currScenes = []; +let _pannellumViewer; + +export default { + + init: async function(context, selection) { + + selection + .append('div') + .attr('class', 'photo-frame pannellum-frame') + .attr('id', 'ideditor-pannellum-viewer') + .classed('hide', true); + + if (!window.pannellum) { + await this.loadPannellum(context); + } + + const options = { + 'default': { firstScene: '' }, + scenes: {} + }; + + _pannellumViewer = window.pannellum.viewer('ideditor-pannellum-viewer', options); + + _pannellumViewer + .on('mousedown', () => { + d3_select(window) + .on('pointermove.pannellum mousemove.pannellum', () => { + dispatch.call('viewerChanged'); + }); + }) + .on('mouseup', () => { + d3_select(window) + .on('pointermove.pannellum mousemove.pannellum', null); + }) + .on('animatefinished', () => { + dispatch.call('viewerChanged'); + }); + + context.ui().photoviewer.on('resize.pannellum', () => { + _pannellumViewer.resize(); + }); + + this.event = utilRebind(this, dispatch, 'on'); + + return this; + }, + + loadPannellum: function(context) { + return 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 viewer css + head.selectAll('#ideditor-pannellum-viewercss') + .data([0]) + .enter() + .append('link') + .attr('id', 'ideditor-pannellum-viewercss') + .attr('rel', 'stylesheet') + .attr('crossorigin', 'anonymous') + .attr('href', context.asset(pannellumViewerCSS)) + .on('load.pannellum', loaded) + .on('error.pannellum', () => { + reject(); + }); + + // load streetside pannellum viewer js + head.selectAll('#ideditor-pannellum-viewerjs') + .data([0]) + .enter() + .append('script') + .attr('id', 'ideditor-pannellum-viewerjs') + .attr('crossorigin', 'anonymous') + .attr('src', context.asset(pannellumViewerJS)) + .on('load.pannellum', loaded) + .on('error.pannellum', () => { + reject(); + }); + }); + }, + + showPhotoFrame: function (context) { + const isHidden = context.selectAll('.photo-frame.pannellum-frame.hide').size(); + + if (isHidden) { + context + .selectAll('.photo-frame:not(.pannellum-frame)') + .classed('hide', true); + + context + .selectAll('.photo-frame.pannellum-frame') + .classed('hide', false); + } + + return this; + }, + + hidePhotoFrame: function (viewerContext) { + viewerContext + .select('photo-frame.pannellum-frame') + .classed('hide', false); + + return this; + }, + + selectPhoto: function (data, keepOrientation) { + const {key} = data; + if ( !(key in _currScenes) ) { + let newSceneOptions = { + showFullscreenCtrl: false, + autoLoad: true, + compass: true, + yaw: 0, + type: 'equirectangular', + preview: data.preview_path, + panorama: data.image_path, + northOffset: data.ca + }; + + _currScenes.push(key); + _pannellumViewer.addScene(key, newSceneOptions); + } + + let yaw = 0; + let pitch = 0; + + if (keepOrientation) { + yaw = this.getYaw(); + pitch = _pannellumViewer.getPitch(); + } + _pannellumViewer.loadScene(key, pitch, yaw); + + if (_currScenes.length > 3) { + const old_key = _currScenes.shift(); + _pannellumViewer.removeScene(old_key); + } + + return this; + }, + + getYaw: function() { + return _pannellumViewer.getYaw(); + } + +}; diff --git a/modules/services/plane_photo.js b/modules/services/plane_photo.js new file mode 100644 index 000000000..275a011d9 --- /dev/null +++ b/modules/services/plane_photo.js @@ -0,0 +1,82 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { zoom as d3_zoom } from 'd3-zoom'; +import { utilSetTransform, utilRebind } from '../util'; + +const dispatch = d3_dispatch('viewerChanged'); + +let _photo; +let imgZoom; + +export default { + + init: async function(context, selection) { + imgZoom = d3_zoom() + .extent([[0, 0], [320, 240]]) + //.translateExtent(?) + .scaleExtent([1, 15]) + .on('zoom', this.zoomPan); + + const wrapper = selection + .append('div') + .attr('class', 'photo-frame plane-frame') + .call(imgZoom) + .classed('hide', true); + + _photo = wrapper + .append('img') + .attr('class', 'plane-photo'); + + this.event = utilRebind(this, dispatch, 'on'); + + context.ui().photoviewer.on('resize.plane', (dimensions) => { + imgZoom = d3_zoom() + .extent([[0, 0], dimensions]) + //.translateExtent(?) + .scaleExtent([1, 15]) + .on('zoom', this.zoomPan); + }); + + await Promise.resolve(); + + return this; + }, + + showPhotoFrame: function (context) { + const isHidden = context.selectAll('.photo-frame.plane-frame.hide').size(); + + if (isHidden) { + context + .selectAll('.photo-frame:not(.plane-frame)') + .classed('hide', true); + + context + .selectAll('.photo-frame.plane-frame') + .classed('hide', false); + } + + return this; + }, + + hidePhotoFrame: function (context) { + context + .select('photo-frame.plane-frame') + .classed('hide', false); + + return this; + }, + + selectPhoto: function (data) { + _photo.attr('src', data.image_path); + return this; + }, + + zoomPan: function (d3_event) { + let t = d3_event.transform; + _photo.call(utilSetTransform, t.x, t.y, t.k); + }, + + getYaw: function() { + return 0; + } + +}; diff --git a/modules/services/vegbilder.js b/modules/services/vegbilder.js index 1de0fbccf..cf5581357 100644 --- a/modules/services/vegbilder.js +++ b/modules/services/vegbilder.js @@ -1,15 +1,13 @@ import { json as d3_json, xml as d3_xml} from 'd3-fetch'; 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 { pairs as d3_pairs } from 'd3-array'; import { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs} from '../util'; import {geoExtent, geoScaleToZoom, geoVecAngle} from '../geo'; +import pannellumPhotoFrame from './pannellum_photo'; +import planePhotoFrame from './plane_photo'; import RBush from 'rbush'; const owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?'; -const pannellumViewerCSS = 'pannellum/pannellum.css'; -const pannellumViewerJS = 'pannellum/pannellum.js'; const tileZoom = 14; const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true); const dispatch = d3_dispatch('loadedImages', 'viewerChanged'); @@ -18,23 +16,13 @@ const directionEnum = Object.freeze({ backward: Symbol(1) }); -let imgZoom = d3_zoom() - .extent([[0, 0], [320, 240]]) - .translateExtent([[0, 0], [320, 240]]) - .scaleExtent([1, 15]); -let _sceneOptions = { - showFullscreenCtrl: false, - autoLoad: true, - compass: true, - yaw: 0, - type: 'equirectangular', -}; -let _vegbilderCache; +let _planeFrame; +let _pannellumFrame; +let _currentFrame; let _loadViewerPromise; -let _pannellumViewer; +let _vegbilderCache; let _availableLayers; - function abortRequest(controller) { controller.abort(); } @@ -59,13 +47,13 @@ async function fetchAvailableLayers() { ); let node; _availableLayers = []; - while (node = l.iterateNext()) { + while ( (node = l.iterateNext()) !== null ) { let match = node.textContent?.match(regexMatcher); if (match) { _availableLayers.push({ name: match[0], is_sphere: !!match.groups?.image_type, - year: parseInt(match.groups?.year) + year: parseInt(match.groups?.year, 10) }); } } @@ -86,14 +74,14 @@ function filterAvailableLayers(photoContex) { } function loadWFSLayers(projection, margin, layers) { + const tiles = tiler.margin(margin).getTiles(projection); Promise.all(layers.map( - ({name}) => loadWFSLayer(name, projection, margin) + ({name}) => loadWFSLayer(name, tiles) )) .then(() => orderSequences(projection)); } -async function loadWFSLayer(layername, projection, margin) { - const tiles = tiler.margin(margin).getTiles(projection); +async function loadWFSLayer(layername, tiles) { let cache = _vegbilderCache.wfslayers.get(layername); if (!cache) { @@ -164,17 +152,19 @@ async function loadTile(cache, layername, tile) { RETNING: ca, TIDSPUNKT: captured_at, URL: image_path, + URLPREVIEW : preview_path, BILDETYPE: image_type, METER: metering, FELTKODE: lane_code } = properties; - const lane_number = parseInt(lane_code.match(/^[0-9]+/)[0]); + const lane_number = parseInt(lane_code.match(/^[0-9]+/)[0], 10); const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward; - const d = { + const data = { loc, key, ca, image_path, + preview_path, layername, road_reference: roadReference(properties), metering, @@ -184,10 +174,10 @@ async function loadTile(cache, layername, tile) { is_sphere: image_type === '360' }; - _vegbilderCache.points.set(key, d); + _vegbilderCache.points.set(key, data); return { - minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d + minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data }; }); @@ -395,29 +385,30 @@ export default { loadWFSLayers(projection, margin, layers); }, - viewer: function() { - return _pannellumViewer; - }, - - initPannellumViewer: function () { - if (!window.pannellum) return; - if (_pannellumViewer) return; - - _currScene += 1; - const sceneID = _currScene.toString(); - const options = { - 'default': { firstScene: sceneID }, - scenes: {} - }; - options.scenes[sceneID] = _sceneOptions; - - _pannellumViewer = window.pannellum.viewer('ideditor-viewer-vegbilder', options); + photoFrame: function() { + return _currentFrame; }, ensureViewerLoaded: function(context) { if (_loadViewerPromise) return _loadViewerPromise; + const step = (stepBy) => () => { + const viewer = context.container().select('.photoviewer'); + const selected = viewer.empty() ? undefined : viewer.datum(); + if (!selected) return; + + const sequence = this.getSequenceForImage(selected); + const nextIndex = sequence.images.indexOf(selected) + stepBy; + const nextImage = sequence.images[nextIndex]; + const nextKey = nextImage.key; + + if (!nextKey) return; + + context.map().centerEase(nextImage.loc); + this.selectImage(context, nextKey, true); + }; + const wrap = context.container().select('.photoviewer') .selectAll('.vegbilder-wrapper') .data([0]); @@ -447,148 +438,32 @@ export default { .on('click.forward', step(1)) .text('►'); - wrapEnter - .append('div') - .attr('class', 'vegbilder-image-wrap'); - - - context.ui().photoviewer.on('resize.vegbilder', dimensions => { - if (_pannellumViewer) { - _pannellumViewer.resize(); - } else { - imgZoom = d3_zoom() - .extent([[0, 0], dimensions]) - .translateExtent([[0, 0], dimensions]) - .scaleExtent([1, 15]) - .on('zoom', zoomPan); - } + _loadViewerPromise = Promise.all([ + pannellumPhotoFrame.init(context, wrapEnter), + planePhotoFrame.init(context, wrapEnter) + ]).then(([pannellumPhotoFrame, planePhotoFrame]) => { + _pannellumFrame = pannellumPhotoFrame; + _planeFrame = planePhotoFrame; + _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged')); }); - _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 streetside pannellum viewer css - head.selectAll('#ideditor-vegbilder-viewercss') - .data([0]) - .enter() - .append('link') - .attr('id', 'ideditor-vegbilder-viewercss') - .attr('rel', 'stylesheet') - .attr('crossorigin', 'anonymous') - .attr('href', context.asset(pannellumViewerCSS)) - .on('load.serviceVegbilder', loaded) - .on('error.serviceVegbilder', function() { - reject(); - }); - - // load streetside pannellum viewer js - head.selectAll('#ideditor-vegbilder-viewerjs') - .data([0]) - .enter() - .append('script') - .attr('id', 'ideditor-vegbilder-viewerjs') - .attr('crossorigin', 'anonymous') - .attr('src', context.asset(pannellumViewerJS)) - .on('load.serviceVegbilder', loaded) - .on('error.serviceVegbilder', function() { - reject(); - }); - }) - .catch(() => { - _loadViewerPromise = null; - }); - - const that = this; - return _loadViewerPromise; - - function zoomPan(d3_event) { - const t = d3_event.transform; - context.container().select('.photoviewer .vegbilder-image-wrap') - .call(utilSetTransform, t.x, t.y, t.k); - } - - function step(stepBy) { - return () => { - const viewer = context.container().select('.photoviewer'); - const selected = viewer.empty() ? undefined : viewer.datum(); - if (!selected) return; - - const sequence = that.getSequenceForImage(selected); - const nextIndex = sequence.images.indexOf(selected) + stepBy; - const nextImage = sequence.images[nextIndex]; - - if (!nextImage) return; - // TODO jump to a spatial and temporal close sequence when reaching the start or end. - that.selectImage(context, nextImage.key); - }; - } }, - selectImage: function(context, key) { + selectImage: function(context, key, keepOrientation) { const d = this.cachedImage(key); this.updateUrlImage(key); const viewer = context.container().select('.photoviewer'); - if (!viewer.empty()) viewer.datum(d); + if (!viewer.empty()) { viewer.datum(d); } this.setStyles(context, null, true); - context.container().selectAll('.icon-sign') - .classed('currentView', false); - if (!d) return this; const wrap = context.container().select('.photoviewer .vegbilder-wrapper'); - const imageWrap = wrap.selectAll('.vegbilder-image-wrap'); const attribution = wrap.selectAll('.photo-attribution').text(''); - wrap - .transition() - .duration(100) - .call(imgZoom.transform, d3_zoomIdentity); - - imageWrap - .selectAll('.vegbilder-image') - .remove(); - - if (!d.is_sphere) { - imageWrap - .append('img') - .attr('class', 'vegbilder-image') - .attr('src', d.image_path); - } else { - imageWrap - .append('div') - .attr('class', 'vegbilder-panorama') - .attr('id', 'vegbilder-panorama') - .on(); - - _sceneOptions.panorama = d.image_path; - _sceneOptions.northOffset = d.ca; - _pannellumViewer = window.pannellum.viewer('vegbilder-imagesphere', _sceneOptions); - _pannellumViewer - .on('mousedown', () => { - d3_select(window) - .on('mousemove', () => { - dispatch.call('viewerChanged'); - }); - }) - .on('animatefinished', () => { - d3_select(window) - .on('mousemove', null); - dispatch.call('viewerChanged'); - }); - } - if (d.captured_at) { attribution .append('span') @@ -602,7 +477,11 @@ export default { .attr('href', 'https://vegvesen.no') .text('Norwegian Public Roads Administration'); - this.showViewer(context); + _currentFrame = d.is_sphere? _pannellumFrame : _planeFrame; + + _currentFrame + .selectPhoto(d, keepOrientation) + .showPhotoFrame(wrap); return this; }, @@ -636,7 +515,7 @@ export default { .selectAll('.photo-wrapper') .classed('hide', true); - context.container().selectAll('.viewfield-group, .sequence, .icon-sign') + context.container().selectAll('.viewfield-group, .sequence') .classed('currentView', false); return this.setStyles(context, null, true); @@ -698,7 +577,6 @@ export default { return this; }, - updateUrlImage: function (key) { if (!window.mocha) { const hash = utilStringQs(window.location.hash); diff --git a/modules/svg/vegbilder.js b/modules/svg/vegbilder.js index 9794cb6ad..8b1cc9a2e 100644 --- a/modules/svg/vegbilder.js +++ b/modules/svg/vegbilder.js @@ -11,7 +11,6 @@ export function svgVegbilder(projection, context, dispatch) { const minViewfieldZoom = 18; let layer = d3_select(null); let _viewerYaw = 0; - let _selectedSequence = null; let _vegbilder; /** @@ -84,16 +83,10 @@ export function svgVegbilder(projection, context, dispatch) { layer.style('display', 'none'); } - /** - * click() Handles 'bubble' point click event. - */ function click(d3_event, d) { const service = getService(); if (!service) return; - // try to preserve the viewer rotation when staying on the same sequence - _selectedSequence = d.sequence_reference; - service .ensureViewerLoaded(context) .then(() => { @@ -141,11 +134,10 @@ export function svgVegbilder(projection, context, dispatch) { const service = getService(); if (!service) return; - const viewer = service.viewer(); - if (!viewer) return; + const frame = service.photoFrame(); // update viewfield rotation - _viewerYaw = viewer.getYaw(); + _viewerYaw = frame.getYaw(); // avoid updating if the map is currently transformed // e.g. during drags or easing.