mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 01:02:58 +00:00
added mock Panoramax (mapilio clone) to photo layer list
This commit is contained in:
@@ -1449,6 +1449,9 @@ en:
|
||||
mapilio:
|
||||
title: Mapilio
|
||||
tooltip: "Street-level photos from Mapilio"
|
||||
panoramax:
|
||||
title: Panoramax
|
||||
tooltip: "Street-level photos from Panoramax"
|
||||
street_side:
|
||||
minzoom_tooltip: "Zoom in to see street-side photos"
|
||||
local_photos:
|
||||
|
||||
@@ -13,6 +13,7 @@ import { rendererBackgroundSource } from './background_source';
|
||||
import { rendererTileLayer } from './tile_layer';
|
||||
import { utilQsString, utilStringQs } from '../util';
|
||||
import { utilRebind } from '../util/rebind';
|
||||
import panoramax from '../services/panoramax';
|
||||
|
||||
|
||||
let _imageryIndex = null;
|
||||
@@ -253,7 +254,8 @@ export function rendererBackground(context) {
|
||||
'mapillary-signs': 'Mapillary Signs',
|
||||
kartaview: 'KartaView Images',
|
||||
vegbilder: 'Norwegian Road Administration Images',
|
||||
mapilio: 'Mapilio Images'
|
||||
mapilio: 'Mapilio Images',
|
||||
panoramax: 'Panoramax Images'
|
||||
};
|
||||
|
||||
for (let layerID in photoOverlayLayers) {
|
||||
|
||||
@@ -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', 'mapilio', 'vegbilder'];
|
||||
var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax'];
|
||||
var _allPhotoTypes = ['flat', 'panoramic'];
|
||||
var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy
|
||||
var _dateFilters = ['fromDate', 'toDate'];
|
||||
|
||||
@@ -15,6 +15,7 @@ import serviceVectorTile from './vector_tile';
|
||||
import serviceWikidata from './wikidata';
|
||||
import serviceWikipedia from './wikipedia';
|
||||
import serviceMapilio from './mapilio';
|
||||
import servicePanoramax from './panoramax';
|
||||
|
||||
|
||||
export let services = {
|
||||
@@ -34,7 +35,8 @@ export let services = {
|
||||
vectorTile: serviceVectorTile,
|
||||
wikidata: serviceWikidata,
|
||||
wikipedia: serviceWikipedia,
|
||||
mapilio: serviceMapilio
|
||||
mapilio: serviceMapilio,
|
||||
panoramax: servicePanoramax
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -54,5 +56,6 @@ export {
|
||||
serviceVectorTile,
|
||||
serviceWikidata,
|
||||
serviceWikipedia,
|
||||
serviceMapilio
|
||||
serviceMapilio,
|
||||
servicePanoramax
|
||||
};
|
||||
|
||||
620
modules/services/panoramax.js
Normal file
620
modules/services/panoramax.js
Normal file
@@ -0,0 +1,620 @@
|
||||
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, utilQsString, utilStringQs, utilSetTransform } 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 = '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;
|
||||
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/pannellum.css';
|
||||
const pannellumViewerJS = 'pannellum/pannellum.js';
|
||||
const resolution = 1080;
|
||||
|
||||
let _activeImage;
|
||||
let _cache;
|
||||
let _loadViewerPromise;
|
||||
let _pannellumViewer;
|
||||
let _sceneOptions = {
|
||||
showFullscreenCtrl: false,
|
||||
autoLoad: true,
|
||||
yaw: 0,
|
||||
minHfov: 10,
|
||||
maxHfov: 90,
|
||||
hfov: 60,
|
||||
};
|
||||
let _currScene = 0;
|
||||
|
||||
|
||||
// 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);
|
||||
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 = _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, which);
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Load the data from the vector tile into cache
|
||||
function loadTileDataToCache(data, tile) {
|
||||
const vectorTile = new VectorTile(new Protobuf(data));
|
||||
let features,
|
||||
cache,
|
||||
layer,
|
||||
i,
|
||||
feature,
|
||||
loc,
|
||||
d;
|
||||
if (vectorTile.layers.hasOwnProperty(pointLayer)) {
|
||||
features = [];
|
||||
cache = _cache.images;
|
||||
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,
|
||||
capture_time: feature.properties.capture_time,
|
||||
id: feature.properties.id,
|
||||
sequence_id: feature.properties.sequence_uuid,
|
||||
heading: feature.properties.heading,
|
||||
resolution: feature.properties.resolution,
|
||||
isPano: isPano
|
||||
};
|
||||
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(lineLayer)) {
|
||||
cache = _cache.sequences;
|
||||
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]);
|
||||
if (cache.lineString[feature.properties.sequence_uuid]) {
|
||||
cache.lineString[feature.properties.sequence_uuid].push(feature);
|
||||
} else {
|
||||
cache.lineString[feature.properties.sequence_uuid] = [feature];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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];
|
||||
_sceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
// Initialize Mapilio
|
||||
init: function() {
|
||||
if (!_cache) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
this.event = utilRebind(this, dispatch, 'on');
|
||||
},
|
||||
|
||||
// Reset cache and state
|
||||
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: {} },
|
||||
requests: { loaded: {}, inflight: {} }
|
||||
};
|
||||
|
||||
_activeImage = null;
|
||||
},
|
||||
|
||||
// Get visible images
|
||||
images: function(projection) {
|
||||
const limit = 5;
|
||||
return searchLimited(limit, projection, _cache.images.rtree);
|
||||
},
|
||||
|
||||
cachedImage: function(imageKey) {
|
||||
return _cache.images.forImageId[imageKey];
|
||||
},
|
||||
|
||||
|
||||
// Load images in the visible area
|
||||
loadImages: function(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 = [];
|
||||
|
||||
_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;
|
||||
},
|
||||
|
||||
// Set the currently visible image
|
||||
setActiveImage: function(image) {
|
||||
if (image) {
|
||||
_activeImage = {
|
||||
id: image.id,
|
||||
sequence_id: image.sequence_id
|
||||
};
|
||||
} else {
|
||||
_activeImage = 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 = _activeImage && _activeImage.sequence_id;
|
||||
const selectedImageId = _activeImage && _activeImage.id;
|
||||
|
||||
const markers = context.container().selectAll('.layer-mapilio .viewfield-group');
|
||||
const sequences = context.container().selectAll('.layer-mapilio .sequence');
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
_currScene += 1;
|
||||
const sceneID = _currScene.toString();
|
||||
const options = {
|
||||
'default': { firstScene: sceneID },
|
||||
scenes: {}
|
||||
};
|
||||
options.scenes[sceneID] = _sceneOptions;
|
||||
|
||||
_pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);
|
||||
},
|
||||
|
||||
selectImage: function (context, id) {
|
||||
|
||||
let that = this;
|
||||
|
||||
let d = this.cachedImage(id);
|
||||
|
||||
this.setActiveImage(d);
|
||||
|
||||
this.updateUrlImage(d.id);
|
||||
|
||||
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 .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');
|
||||
|
||||
wrap
|
||||
.transition()
|
||||
.duration(100)
|
||||
.call(imgZoom.transform, d3_zoomIdentity);
|
||||
|
||||
wrap
|
||||
.selectAll('img')
|
||||
.remove();
|
||||
|
||||
wrap
|
||||
.selectAll('button.back')
|
||||
.classed('hide', !_cache.images.forImageId.hasOwnProperty(+id - 1));
|
||||
wrap
|
||||
.selectAll('button.forward')
|
||||
.classed('hide', !_cache.images.forImageId.hasOwnProperty(+id + 1));
|
||||
|
||||
|
||||
getImageData(d.id,d.sequence_id).then(function () {
|
||||
|
||||
if (d.isPano) {
|
||||
if (!_pannellumViewer) {
|
||||
that.initViewer();
|
||||
} else {
|
||||
// make a new scene
|
||||
_currScene += 1;
|
||||
let sceneID = _currScene.toString();
|
||||
_pannellumViewer
|
||||
.addScene(sceneID, _sceneOptions)
|
||||
.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;
|
||||
},
|
||||
|
||||
initOnlyPhoto: function (context) {
|
||||
|
||||
if (_pannellumViewer) {
|
||||
_pannellumViewer.destroy();
|
||||
_pannellumViewer = null;
|
||||
}
|
||||
|
||||
let wrap = context.container().select('#ideditor-viewer-mapilio-simple');
|
||||
|
||||
let imgWrap = wrap.select('img');
|
||||
|
||||
if (!imgWrap.empty()) {
|
||||
imgWrap.attr('src',_sceneOptions.panorama);
|
||||
} else {
|
||||
wrap.append('img')
|
||||
.attr('src',_sceneOptions.panorama);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
ensureViewerLoaded: function(context) {
|
||||
|
||||
let that = this;
|
||||
|
||||
let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');
|
||||
|
||||
if (!imgWrap.empty()) {
|
||||
imgWrap.remove();
|
||||
}
|
||||
|
||||
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)
|
||||
.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-mapilio');
|
||||
|
||||
controlsEnter
|
||||
.append('button')
|
||||
.classed('back', true)
|
||||
.on('click.back', step(-1))
|
||||
.text('◄');
|
||||
|
||||
controlsEnter
|
||||
.append('button')
|
||||
.classed('forward', true)
|
||||
.on('click.forward', step(1))
|
||||
.text('►');
|
||||
|
||||
wrapEnter
|
||||
.append('div')
|
||||
.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
|
||||
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;
|
||||
});
|
||||
|
||||
function step(stepBy) {
|
||||
return function () {
|
||||
if (!_activeImage) return;
|
||||
const imageId = _activeImage.id;
|
||||
|
||||
const nextIndex = imageId + stepBy;
|
||||
if (!nextIndex) return;
|
||||
|
||||
const nextImage = _cache.images.forImageId[nextIndex];
|
||||
|
||||
context.map().centerEase(nextImage.loc);
|
||||
|
||||
that.selectImage(context, nextImage.id);
|
||||
};
|
||||
}
|
||||
|
||||
function zoomPan(d3_event) {
|
||||
var t = d3_event.transform;
|
||||
context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')
|
||||
.call(utilSetTransform, t.x, t.y, t.k);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
return this.setStyles(context, null);
|
||||
},
|
||||
|
||||
// Return the current cache
|
||||
cache: function() {
|
||||
return _cache;
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { svgMapillarySigns } from './mapillary_signs';
|
||||
import { svgMapillaryMapFeatures } from './mapillary_map_features';
|
||||
import { svgKartaviewImages } from './kartaview_images';
|
||||
import { svgMapilioImages } from './mapilio_images';
|
||||
import { svgPanoramaxImages } from './panoramax_images';
|
||||
import { svgOsm } from './osm';
|
||||
import { svgNotes } from './notes';
|
||||
import { svgTouch } from './touch';
|
||||
@@ -40,6 +41,7 @@ export function svgLayers(projection, context) {
|
||||
{ id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },
|
||||
{ id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },
|
||||
{ id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },
|
||||
{ id: 'panoramax', layer: svgPanoramaxImages(projection, context, dispatch) },
|
||||
{ id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },
|
||||
{ id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) },
|
||||
{ id: 'debug', layer: svgDebug(projection, context, dispatch) },
|
||||
|
||||
253
modules/svg/panoramax_images.js
Normal file
253
modules/svg/panoramax_images.js
Normal file
@@ -0,0 +1,253 @@
|
||||
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 svgPanoramaxImages(projection, context, dispatch) {
|
||||
const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000);
|
||||
const minZoom = 12;
|
||||
let layer = d3_select(null);
|
||||
let _panoramax;
|
||||
const viewFieldZoomLevel = 18;
|
||||
|
||||
|
||||
function init() {
|
||||
if (svgPanoramaxImages.initialized) return;
|
||||
svgPanoramaxImages.enabled = false;
|
||||
svgPanoramaxImages.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
function getService() {
|
||||
if (services.panoramax && !_panoramax) {
|
||||
_panoramax = services.panoramax;
|
||||
_panoramax.event.on('loadedImages', throttledRedraw);
|
||||
} else if (!services.panoramax && _panoramax) {
|
||||
_panoramax = null;
|
||||
}
|
||||
|
||||
return _panoramax;
|
||||
}
|
||||
|
||||
|
||||
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 transform(d) {
|
||||
let t = svgPointTransform(projection)(d);
|
||||
if (d.heading) {
|
||||
t += ' rotate(' + Math.floor(d.heading) + ',0,0)';
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
function editOn() {
|
||||
layer.style('display', 'block');
|
||||
}
|
||||
|
||||
|
||||
function editOff() {
|
||||
layer.selectAll('.viewfield-group').remove();
|
||||
layer.style('display', 'none');
|
||||
}
|
||||
|
||||
function click(d3_event, image) {
|
||||
const service = getService();
|
||||
if (!service) return;
|
||||
|
||||
service
|
||||
.ensureViewerLoaded(context, image.id)
|
||||
.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) : []);
|
||||
let images = (service ? service.images(projection) : []);
|
||||
|
||||
let traces = layer.selectAll('.sequences').selectAll('.sequence')
|
||||
.data(sequences, function(d) { return d.properties.id; });
|
||||
|
||||
// exit
|
||||
traces.exit()
|
||||
.remove();
|
||||
|
||||
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')
|
||||
.on('mouseenter', mouseover)
|
||||
.on('mouseleave', mouseout)
|
||||
.on('click', click);
|
||||
|
||||
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(showViewfields ? [0] : []);
|
||||
|
||||
viewfields.exit()
|
||||
.remove();
|
||||
|
||||
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__.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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function drawImages(selection) {
|
||||
const enabled = svgPanoramaxImages.enabled;
|
||||
const service = getService();
|
||||
|
||||
layer = selection.selectAll('.layer-panoramax')
|
||||
.data(service ? [0] : []);
|
||||
|
||||
layer.exit()
|
||||
.remove();
|
||||
|
||||
const layerEnter = layer.enter()
|
||||
.append('g')
|
||||
.attr('class', 'layer-panoramax')
|
||||
.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();
|
||||
update();
|
||||
service.loadImages(projection);
|
||||
service.loadLines(projection);
|
||||
} else {
|
||||
editOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
drawImages.enabled = function(_) {
|
||||
if (!arguments.length) return svgPanoramaxImages.enabled;
|
||||
svgPanoramaxImages.enabled = _;
|
||||
if (svgPanoramaxImages.enabled) {
|
||||
showLayer();
|
||||
context.photos().on('change.panoramax_images', null);
|
||||
} else {
|
||||
hideLayer();
|
||||
context.photos().on('change.panoramax_images', null);
|
||||
}
|
||||
dispatch.call('change');
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
drawImages.supported = function() {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawImages;
|
||||
}
|
||||
Reference in New Issue
Block a user