Split concern for panoramic and flat photos to own module.

This commit is contained in:
Noenandre
2023-03-03 01:23:31 +01:00
parent 75dba4b6c7
commit 58cceb03c2
4 changed files with 296 additions and 182 deletions
+162
View File
@@ -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();
}
};
+82
View File
@@ -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;
}
};
+50 -172
View File
@@ -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);
+2 -10
View File
@@ -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.