mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-12 16:52:50 +00:00
to allow more snappy scrolling through the photos of a sequence using the prev/next buttons the cache is primed with information for "most of" the selected sequence, see also https://gitlab.com/panoramax/server/api/-/issues/268
764 lines
24 KiB
JavaScript
764 lines
24 KiB
JavaScript
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, utilQsString, utilStringQs, utilUniqueDomId} from '../util';
|
|
import { geoExtent, geoScaleToZoom } from '../geo';
|
|
import { t, localizer } from '../core/localizer';
|
|
import pannellumPhotoFrame from './pannellum_photo';
|
|
import planePhotoFrame from './plane_photo';
|
|
import { services } from './';
|
|
|
|
|
|
const apiUrl = 'https://api.panoramax.xyz/';
|
|
const tileUrl = apiUrl + 'api/map/{z}/{x}/{y}.mvt';
|
|
const imageDataUrl = apiUrl + 'api/collections/{collectionId}/items/{itemId}';
|
|
const sequenceDataUrl = apiUrl + 'api/collections/{collectionId}/items?limit=1000';
|
|
const userIdUrl = apiUrl + 'api/users/search?q={username}';
|
|
const usernameURL = apiUrl + 'api/users/{userId}';
|
|
const viewerUrl = apiUrl;
|
|
|
|
const highDefinition = 'hd';
|
|
const standardDefinition = 'sd';
|
|
|
|
const pictureLayer = 'pictures';
|
|
const sequenceLayer = 'sequences';
|
|
|
|
const minZoom = 10;
|
|
const imageMinZoom = 15;
|
|
const lineMinZoom = 10;
|
|
const dispatch = d3_dispatch('loadedImages', 'loadedLines', 'viewerChanged');
|
|
|
|
let _cache;
|
|
let _loadViewerPromise;
|
|
let _definition = standardDefinition;
|
|
let _isHD = false;
|
|
|
|
let _planeFrame;
|
|
let _pannellumFrame;
|
|
let _currentFrame;
|
|
|
|
let _currentScene = {
|
|
currentImage : null,
|
|
nextImage : null,
|
|
prevImage : null
|
|
};
|
|
|
|
let _activeImage;
|
|
let _isViewerOpen = false;
|
|
|
|
|
|
// 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.
|
|
* @param {number} limit Number of maximum objects to return
|
|
* @param {*} projection Current projection
|
|
* @param {*} rtree The cache
|
|
* @returns Data found
|
|
*/
|
|
function searchLimited(limit, projection, rtree) {
|
|
limit = limit || 5;
|
|
|
|
return partitionViewport(projection)
|
|
.reduce(function(result, extent) {
|
|
let found = rtree.search(extent.bbox());
|
|
const spacing = Math.max(1, Math.floor(found.length / limit));
|
|
found = found
|
|
.filter((d, idx) => idx % spacing === 0 ||
|
|
d.data.id === _activeImage?.id)
|
|
.sort((a, b) => {
|
|
if (a.data.id === _activeImage?.id) return -1;
|
|
if (b.data.id === _activeImage?.id) return 1;
|
|
return 0;
|
|
})
|
|
.slice(0, limit)
|
|
.map(d => d.data);
|
|
|
|
return (found.length ? result.concat(found) : result);
|
|
}, []);
|
|
}
|
|
|
|
/**
|
|
* Load all data for the specified type from Panoramax vector tiles
|
|
* @param {string} which Either 'images' or 'lines'
|
|
* @param {string} url Tile endpoint
|
|
* @param {number} maxZoom Maximum zoom out
|
|
* @param {*} projection Current projection
|
|
* @param {number} zoom current zoom
|
|
*/
|
|
function loadTiles(which, url, maxZoom, projection, zoom) {
|
|
const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);
|
|
const tiles = tiler.getTiles(projection);
|
|
|
|
tiles.forEach(function(tile) {
|
|
loadTile(which, url, tile, zoom);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load all data for the specified type from one vector tile
|
|
* @param {*} which Either 'images' or 'lines'
|
|
* @param {*} url Tile endpoint
|
|
* @param {*} tile Current tile
|
|
* @param {*} zoom Current zoom
|
|
*/
|
|
function loadTile(which, url, tile, zoom) {
|
|
const cache = _cache.requests;
|
|
const tileId = `${tile.id}-${which}`;
|
|
if (cache.loaded[tileId] || cache.inflight[tileId]) return;
|
|
const controller = new AbortController();
|
|
cache.inflight[tileId] = controller;
|
|
const requestUrl = url.replace('{x}', tile.xyz[0])
|
|
.replace('{y}', tile.xyz[1])
|
|
.replace('{z}', tile.xyz[2]);
|
|
|
|
fetch(requestUrl, { signal: controller.signal })
|
|
.then(function(response) {
|
|
if (!response.ok) {
|
|
throw new Error(response.status + ' ' + response.statusText);
|
|
}
|
|
cache.loaded[tileId] = true;
|
|
delete cache.inflight[tileId];
|
|
return response.arrayBuffer();
|
|
})
|
|
.then(function(data) {
|
|
if (data.byteLength === 0) {
|
|
throw new Error('No Data');
|
|
}
|
|
|
|
loadTileDataToCache(data, tile, zoom);
|
|
|
|
if (which === 'images') {
|
|
dispatch.call('loadedImages');
|
|
} else {
|
|
dispatch.call('loadedLines');
|
|
}
|
|
})
|
|
.catch(function (e) {
|
|
if (e.message === 'No Data') {
|
|
cache.loaded[tileId] = true;
|
|
} else {
|
|
console.error(e); // eslint-disable-line no-console
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches all data for the specified tile and adds them to cache
|
|
* @param {*} data Tile data
|
|
* @param {*} tile Current tile
|
|
* @param {*} zoom Current zoom
|
|
*/
|
|
function loadTileDataToCache(data, tile, zoom) {
|
|
const vectorTile = new VectorTile(new Protobuf(data));
|
|
|
|
let features,
|
|
cache,
|
|
layer,
|
|
i,
|
|
feature,
|
|
loc,
|
|
d;
|
|
|
|
if (vectorTile.layers.hasOwnProperty(pictureLayer)) {
|
|
features = [];
|
|
cache = _cache.images;
|
|
layer = vectorTile.layers[pictureLayer];
|
|
|
|
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 = {
|
|
service: 'photo',
|
|
loc: loc,
|
|
capture_time: feature.properties.ts,
|
|
capture_time_parsed: new Date(feature.properties.ts),
|
|
id: feature.properties.id,
|
|
account_id: feature.properties.account_id,
|
|
sequence_id: feature.properties.first_sequence,
|
|
heading: parseInt(feature.properties.heading, 10),
|
|
image_path: '',
|
|
isPano: feature.properties.type === 'equirectangular',
|
|
model: feature.properties.model,
|
|
};
|
|
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(sequenceLayer)) {
|
|
|
|
cache = _cache.sequences;
|
|
|
|
if (zoom >= lineMinZoom && zoom < imageMinZoom) cache = _cache.mockSequences;
|
|
|
|
layer = vectorTile.layers[sequenceLayer];
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches the username from Panoramax
|
|
* @param {string} userId
|
|
* @returns the username
|
|
*/
|
|
async function getUsername(userId) {
|
|
const cache = _cache.users;
|
|
if (cache[userId]) return cache[userId].name;
|
|
|
|
const requestUrl = usernameURL.replace('{userId}', userId);
|
|
|
|
const response = await fetch(requestUrl, { method: 'GET' });
|
|
if (!response.ok) {
|
|
throw new Error(response.status + ' ' + response.statusText);
|
|
}
|
|
const data = await response.json();
|
|
cache[userId] = data;
|
|
|
|
return data.name;
|
|
}
|
|
|
|
export default {
|
|
init: function() {
|
|
if (!_cache) {
|
|
this.reset();
|
|
}
|
|
|
|
this.event = utilRebind(this, dispatch, 'on');
|
|
},
|
|
|
|
reset: function() {
|
|
if (_cache) {
|
|
Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });
|
|
}
|
|
|
|
_cache = {
|
|
images: { rtree: new RBush(), forImageId: {} },
|
|
sequences: { rtree: new RBush(), lineString: {}, items: {} },
|
|
users: {},
|
|
mockSequences: { rtree: new RBush(), lineString: {} },
|
|
requests: { loaded: {}, inflight: {} }
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Get visible images from cache
|
|
* @param {*} projection Current Projection
|
|
* @returns images data for the current projection
|
|
*/
|
|
images: function(projection) {
|
|
const limit = 5;
|
|
return searchLimited(limit, projection, _cache.images.rtree);
|
|
},
|
|
|
|
/**
|
|
* Get a specific image from cache
|
|
* @param {*} imageKey the image id
|
|
* @returns
|
|
*/
|
|
cachedImage: function(imageKey) {
|
|
return _cache.images.forImageId[imageKey];
|
|
},
|
|
|
|
/**
|
|
* Fetches images data for the visible area
|
|
* @param {*} projection Current Projection
|
|
*/
|
|
loadImages: function(projection) {
|
|
loadTiles('images', tileUrl, imageMinZoom, projection);
|
|
},
|
|
|
|
/**
|
|
* Fetches sequences data for the visible area
|
|
* @param {*} projection Current Projection
|
|
*/
|
|
loadLines: function(projection, zoom) {
|
|
loadTiles('line', tileUrl, lineMinZoom, projection, zoom);
|
|
},
|
|
|
|
/**
|
|
* Fetches all possible userIDs from Panoramax
|
|
* @param {string} usernames one or multiple usernames
|
|
* @returns userIDs
|
|
*/
|
|
getUserIds: async function(usernames) {
|
|
const requestUrls = usernames.map(username =>
|
|
userIdUrl.replace('{username}', username));
|
|
|
|
const responses = await Promise.all(requestUrls.map(requestUrl =>
|
|
fetch(requestUrl, { method: 'GET' })));
|
|
if (responses.some(response => !response.ok)) {
|
|
const response = responses.find(response => !response.ok);
|
|
throw new Error(response.status + ' ' + response.statusText);
|
|
}
|
|
const data = await Promise.all(responses.map(response => response.json()));
|
|
// in panoramax, a username can have multiple ids, when the same name is
|
|
// used on different servers
|
|
return data.flatMap((d, i) => d.features.filter(f => f.name === usernames[i]).map(f => f.id));
|
|
},
|
|
|
|
/**
|
|
* Get visible sequences from cache
|
|
* @param {*} projection Current Projection
|
|
* @param {number} zoom Current zoom (if zoom < `lineMinZoom` less accurate lines will be drawn)
|
|
* @returns sequences data for the current projection
|
|
*/
|
|
sequences: function(projection, zoom) {
|
|
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 = [];
|
|
|
|
if (zoom >= imageMinZoom){
|
|
_cache.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 (_cache.sequences.lineString[sequenceId]) {
|
|
lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);
|
|
}
|
|
});
|
|
return lineStrings;
|
|
}
|
|
if (zoom >= lineMinZoom){
|
|
Object.keys(_cache.mockSequences.lineString).forEach(function(sequenceId) {
|
|
lineStrings = lineStrings.concat(_cache.mockSequences.lineString[sequenceId]);
|
|
});
|
|
}
|
|
return lineStrings;
|
|
},
|
|
|
|
/**
|
|
* Updates the data for the currently visible image
|
|
* @param {*} image Image data
|
|
*/
|
|
setActiveImage: function(image) {
|
|
if (image && image.id && image.sequence_id) {
|
|
_activeImage = {
|
|
id: image.id,
|
|
sequence_id: image.sequence_id,
|
|
loc: image.loc
|
|
};
|
|
} else {
|
|
_activeImage = null;
|
|
}
|
|
},
|
|
|
|
getActiveImage: function(){
|
|
return _activeImage;
|
|
},
|
|
|
|
/**
|
|
* Update the currently highlighted sequence and selected bubble
|
|
* @param {*} context Current HTML context
|
|
* @param {*} [hovered] The hovered bubble image
|
|
*/
|
|
setStyles: function(context, hovered) {
|
|
const hoveredImageId = hovered && hovered.id;
|
|
const hoveredSequenceId = hovered && hovered.sequence_id;
|
|
const selectedSequenceId = _activeImage && _activeImage.sequence_id;
|
|
const selectedImageId = _activeImage && _activeImage.id;
|
|
|
|
const markers = context.container().selectAll('.layer-panoramax .viewfield-group');
|
|
const sequences = context.container().selectAll('.layer-panoramax .sequence');
|
|
|
|
markers
|
|
.classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || 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.id === hoveredSequenceId; })
|
|
.classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });
|
|
|
|
// update viewfields if needed
|
|
context.container().selectAll('.layer-panoramax .viewfield-group .viewfield')
|
|
.attr('d', viewfieldPath);
|
|
|
|
function viewfieldPath() {
|
|
let d = this.parentNode.__data__;
|
|
if (d.isPano && d.id !== selectedImageId) {
|
|
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';
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// Get viewer status
|
|
isViewerOpen: function() {
|
|
return _isViewerOpen;
|
|
},
|
|
|
|
/**
|
|
* Updates the URL to save the current shown image
|
|
* @param {*} imageKey
|
|
*/
|
|
updateUrlImage: function(imageKey) {
|
|
const hash = utilStringQs(window.location.hash);
|
|
if (imageKey) {
|
|
hash.photo = 'panoramax/' + imageKey;
|
|
} else {
|
|
delete hash.photo;
|
|
}
|
|
window.history.replaceState(null, '', '#' + utilQsString(hash, true));
|
|
},
|
|
|
|
/**
|
|
* Loads the selected image in the frame
|
|
* @param {*} context Current HTML context
|
|
* @param {*} id of the selected image
|
|
* @returns
|
|
*/
|
|
selectImage: function (context, id) {
|
|
let that = this;
|
|
|
|
let d = that.cachedImage(id);
|
|
that.setActiveImage(d);
|
|
that.updateUrlImage(d.id);
|
|
|
|
const viewerLink = `${viewerUrl}#pic=${d.id}&focus=pic`;
|
|
|
|
let viewer = context.container()
|
|
.select('.photoviewer');
|
|
|
|
if (!viewer.empty()) viewer.datum(d);
|
|
|
|
this.setStyles(context, null);
|
|
|
|
if (!d) return this;
|
|
|
|
let wrap = context.container()
|
|
.select('.photoviewer .panoramax-wrapper');
|
|
|
|
let attribution = wrap.selectAll('.photo-attribution').text('');
|
|
|
|
let line1 = attribution
|
|
.append('div')
|
|
.attr('class', 'attribution-row');
|
|
|
|
const hdDomId = utilUniqueDomId('panoramax-hd');
|
|
|
|
let label = line1
|
|
.append('label')
|
|
.attr('for', hdDomId)
|
|
.attr('class', 'panoramax-hd');
|
|
|
|
label
|
|
.append('input')
|
|
.attr('type', 'checkbox')
|
|
.attr('id', hdDomId)
|
|
.property('checked', _isHD)
|
|
.on('click', (d3_event) => {
|
|
d3_event.stopPropagation();
|
|
_isHD = !_isHD;
|
|
_definition = _isHD ? highDefinition : standardDefinition;
|
|
that.selectImage(context, d.id)
|
|
.showViewer(context);
|
|
});
|
|
|
|
label
|
|
.append('span')
|
|
.call(t.append('panoramax.hd'));
|
|
|
|
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', 'report-photo')
|
|
.attr('href', 'mailto:signalement.ign@panoramax.fr')
|
|
.call(t.append('panoramax.report'));
|
|
|
|
attribution
|
|
.append('span')
|
|
.text('|');
|
|
|
|
attribution
|
|
.append('a')
|
|
.attr('class', 'image-link')
|
|
.attr('target', '_blank')
|
|
.attr('href', viewerLink)
|
|
.text('panoramax.xyz');
|
|
|
|
this.getImageData(d.sequence_id, d.id).then(function(data) {
|
|
_currentScene = {
|
|
currentImage: null,
|
|
nextImage: null,
|
|
prevImage: null
|
|
};
|
|
_currentScene.currentImage = data.assets[_definition];
|
|
const nextIndex = data.links.findIndex(x => x.rel === 'next');
|
|
const prevIndex = data.links.findIndex(x => x.rel === 'prev');
|
|
|
|
if (nextIndex !== -1){
|
|
_currentScene.nextImage = data.links[nextIndex];
|
|
}
|
|
if (prevIndex !== -1){
|
|
_currentScene.prevImage = data.links[prevIndex];
|
|
}
|
|
|
|
d.image_path = _currentScene.currentImage.href;
|
|
|
|
wrap
|
|
.selectAll('button.back')
|
|
.classed('hide', _currentScene.prevImage === null);
|
|
wrap
|
|
.selectAll('button.forward')
|
|
.classed('hide', _currentScene.nextImage === null);
|
|
|
|
_currentFrame = d.isPano ? _pannellumFrame : _planeFrame;
|
|
|
|
_currentFrame
|
|
.showPhotoFrame(wrap)
|
|
.selectPhoto(d, true);
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
if (d.account_id) {
|
|
attribution
|
|
.append('span')
|
|
.text('|');
|
|
|
|
let line2 = attribution
|
|
.append('span')
|
|
.attr('class', 'attribution-row');
|
|
|
|
getUsername(d.account_id).then(function(username){
|
|
line2
|
|
.append('span')
|
|
.attr('class', 'captured_by')
|
|
.text('@' + username);
|
|
});
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
photoFrame: function() {
|
|
return _currentFrame;
|
|
},
|
|
|
|
/**
|
|
* Fetches the data for a specific image
|
|
* @param {*} collectionId
|
|
* @param {*} imageId
|
|
* @returns The fetched image data
|
|
*/
|
|
getImageData: async function(collectionId, imageId) {
|
|
const cache = _cache.sequences.items;
|
|
if (cache[collectionId]) {
|
|
const cached = cache[collectionId]
|
|
.find(d => d.id === imageId);
|
|
if (cached) return cached;
|
|
} else {
|
|
// prime the cache with data from sequence
|
|
const response = await fetch(sequenceDataUrl
|
|
.replace('{collectionId}', collectionId),
|
|
{ method: 'GET' });
|
|
|
|
if (!response.ok) {
|
|
throw new Error(response.status + ' ' + response.statusText);
|
|
}
|
|
const data = (await response.json()).features;
|
|
cache[collectionId] = data;
|
|
}
|
|
|
|
const result = cache[collectionId]
|
|
.find(d => d.id === imageId);
|
|
if (result) return result;
|
|
|
|
// not found in sequence: retry to load single item data
|
|
// ideally, we'd use the `withPicture` parameter, but it is buggy:
|
|
// https://gitlab.com/panoramax/server/api/-/issues/268
|
|
const itemResponse = await fetch(imageDataUrl
|
|
.replace('{collectionId}', collectionId)
|
|
.replace('{itemId}', imageId),
|
|
{ method: 'GET' });
|
|
|
|
if (!itemResponse.ok) {
|
|
throw new Error(itemResponse.status + ' ' + itemResponse.statusText);
|
|
}
|
|
const itemData = await itemResponse.json();
|
|
cache[collectionId].push(itemData);
|
|
return itemData;
|
|
},
|
|
|
|
ensureViewerLoaded: function(context) {
|
|
|
|
let that = this;
|
|
|
|
let imgWrap = context.container()
|
|
.select('#ideditor-viewer-panoramax-simple > img');
|
|
|
|
if (!imgWrap.empty()) {
|
|
imgWrap.remove();
|
|
}
|
|
|
|
if (_loadViewerPromise) return _loadViewerPromise;
|
|
|
|
let wrap = context.container()
|
|
.select('.photoviewer')
|
|
.selectAll('.panoramax-wrapper')
|
|
.data([0]);
|
|
|
|
let wrapEnter = wrap.enter()
|
|
.append('div')
|
|
.attr('class', 'photo-wrapper panoramax-wrapper')
|
|
.classed('hide', true)
|
|
.on('dblclick.zoom', null);
|
|
|
|
wrapEnter
|
|
.append('div')
|
|
.attr('class', 'photo-attribution fillD');
|
|
|
|
const controlsEnter = wrapEnter
|
|
.append('div')
|
|
.attr('class', 'photo-controls-wrap')
|
|
.append('div')
|
|
.attr('class', 'photo-controls-panoramax');
|
|
|
|
controlsEnter
|
|
.append('button')
|
|
.classed('back', true)
|
|
.on('click.back', step(-1))
|
|
.text('◄');
|
|
|
|
controlsEnter
|
|
.append('button')
|
|
.classed('forward', true)
|
|
.on('click.forward', step(1))
|
|
.text('►');
|
|
|
|
// Register viewer resize handler
|
|
_loadViewerPromise = Promise.all([
|
|
pannellumPhotoFrame.init(context, wrapEnter),
|
|
planePhotoFrame.init(context, wrapEnter)
|
|
]).then(([pannellumPhotoFrame, planePhotoFrame]) => {
|
|
_pannellumFrame = pannellumPhotoFrame;
|
|
_pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));
|
|
_planeFrame = planePhotoFrame;
|
|
_planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));
|
|
});
|
|
|
|
/**
|
|
* Loads the next image in the sequence
|
|
* @param {number} stepBy '-1' if backwards or '1' if forward
|
|
* @returns
|
|
*/
|
|
function step(stepBy) {
|
|
return function () {
|
|
if (!_currentScene.currentImage) return;
|
|
|
|
let nextId;
|
|
if (stepBy === 1) nextId = _currentScene.nextImage.id;
|
|
else nextId = _currentScene.prevImage.id;
|
|
|
|
if (!nextId) return;
|
|
|
|
const nextImage = _cache.images.forImageId[nextId];
|
|
|
|
if (nextImage){
|
|
context.map().centerEase(nextImage.loc);
|
|
that.selectImage(context, nextImage.id);
|
|
}
|
|
};
|
|
}
|
|
|
|
return _loadViewerPromise;
|
|
},
|
|
|
|
/**
|
|
* Shows the current viewer if hidden
|
|
* @param {*} context
|
|
*/
|
|
showViewer: function (context) {
|
|
const wrap = context.container().select('.photoviewer');
|
|
const isHidden = wrap.selectAll('.photo-wrapper.panoramax-wrapper.hide').size();
|
|
if (isHidden) {
|
|
for (const service of Object.values(services)) {
|
|
if (service === this) continue;
|
|
if (typeof service.hideViewer === 'function') {
|
|
service.hideViewer(context);
|
|
}
|
|
}
|
|
wrap.classed('hide', false)
|
|
.selectAll('.photo-wrapper.panoramax-wrapper')
|
|
.classed('hide', false);
|
|
}
|
|
|
|
_isViewerOpen = true;
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Hides the current viewer if shown, resets the active image and sequence
|
|
* @param {*} context
|
|
*/
|
|
hideViewer: function (context) {
|
|
let viewer = context.container().select('.photoviewer');
|
|
if (!viewer.empty()) viewer.datum(null);
|
|
this.updateUrlImage(null);
|
|
viewer
|
|
.classed('hide', true)
|
|
.selectAll('.photo-wrapper')
|
|
.classed('hide', true);
|
|
context.container().selectAll('.viewfield-group, .sequence, .icon-sign')
|
|
.classed('currentView', false);
|
|
|
|
this.setActiveImage(null);
|
|
_isViewerOpen = false;
|
|
|
|
return this.setStyles(context, null);
|
|
},
|
|
|
|
cache: function() {
|
|
return _cache;
|
|
}
|
|
};
|