mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-20 15:34:49 +02:00
Merge remote-tracking branch 'noenandre/vegbilder' into develop
This commit is contained in:
+5
-2
@@ -37,14 +37,15 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
||||
|
||||
# Unreleased (2.27.0-dev)
|
||||
|
||||
#### :mega: Release Highlights
|
||||
* Add [_Mapilio_](https://mapilio.com/openstreetmap) as new street-level imagery provider ([#9664], thanks [@channel-s])
|
||||
#### :tada: New Features
|
||||
#### :sparkles: Usability & Accessibility
|
||||
* Show tag reference information for the currently filled-in tag value in UI fields (if available), instead of only showing the more generic _key_ documentation of the field ([#9786])
|
||||
* Don't suggest _discardable_ (i.e. deprecated and automatically removed tags) in the auto-suggestions of the raw tag editor ([#9817], thanks [@k-yle])
|
||||
#### :scissors: Operations
|
||||
#### :camera: Street-Level
|
||||
* Add [_Mapilio_](https://mapilio.com/openstreetmap) as new street-level imagery provider ([#9664], thanks [@channel-s])
|
||||
* Add photos from the [Norwegian Public Road Administration](https://vegbilder.atlas.vegvesen.no/) as new street-level imagery provider in Norway ([#9509], thanks [@noenandre])
|
||||
* Gray out street level layers in "Map Data" pane when map is zoomed out too far
|
||||
#### :white_check_mark: Validation
|
||||
#### :bug: Bugfixes
|
||||
* Validator: Don't falsely flag certain tags as "should be a closed area" if the tag also allows both area and line geometries in two separate presets (e.g. `highway=elevator` in the "Elevator" and "Inclined Lift" presets)
|
||||
@@ -60,10 +61,12 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
||||
|
||||
[#8997]: https://github.com/openstreetmap/iD/issues/8997
|
||||
[#9233]: https://github.com/openstreetmap/iD/issues/9233
|
||||
[#9509]: https://github.com/openstreetmap/iD/pull/9509
|
||||
[#9664]: https://github.com/openstreetmap/iD/pull/9664
|
||||
[#9786]: https://github.com/openstreetmap/iD/issues/9786
|
||||
[#9817]: https://github.com/openstreetmap/iD/pull/9817
|
||||
[@channel-s]: https://github.com/channel-s
|
||||
[@noenandre]: https://github.com/noenandre
|
||||
|
||||
|
||||
# 2.26.2
|
||||
|
||||
+43
-4
@@ -68,12 +68,10 @@
|
||||
}
|
||||
|
||||
|
||||
.photo-wrapper,
|
||||
.photo-wrapper img {
|
||||
.photo-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-wrapper .photo-attribution {
|
||||
@@ -185,6 +183,18 @@
|
||||
stroke-opacity: 0.85; /* bump opacity - only one per road */
|
||||
}
|
||||
|
||||
/* Vegbilder Image Layer */
|
||||
.layer-vegbilder {
|
||||
pointer-events: none;
|
||||
}
|
||||
.layer-vegbilder .viewfield-group * {
|
||||
fill: #ed9300;
|
||||
}
|
||||
.layer-vegbilder .sequence {
|
||||
stroke: #ed9300;
|
||||
stroke-opacity: 0.85; /* bump opacity - only one per road */
|
||||
}
|
||||
|
||||
|
||||
/* Mapillary Image Layer */
|
||||
.layer-mapillary {
|
||||
@@ -313,7 +323,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ms-wrapper .pnlm-compass.pnlm-control {
|
||||
.ms-wrapper .pnlm-compass.pnlm-control,
|
||||
.vegbilder-wrapper .pnlm-compass.pnlm-control {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
left: 4px;
|
||||
@@ -376,6 +387,13 @@ label.streetside-hires {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.kartaview-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.kartaview-wrapper .photo-attribution a:active {
|
||||
color: #20c4ff;
|
||||
}
|
||||
@@ -391,6 +409,26 @@ label.streetside-hires {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vegbilder-wrapper {
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.vegbilder-wrapper .plane-frame {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-image: url(img/loader-black.gif);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.vegbilder-wrapper .plane-frame > img.plane-photo{
|
||||
width: auto;
|
||||
height: 100%;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
/* photo-controls (step forward, back, rotate) */
|
||||
.photo-controls-wrap {
|
||||
@@ -412,6 +450,7 @@ label.streetside-hires {
|
||||
.photo-controls button:focus {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
line-height: 18px;
|
||||
background: rgba(0,0,0,0.65);
|
||||
color: #eee;
|
||||
border-radius: 0;
|
||||
|
||||
@@ -1358,6 +1358,11 @@ en:
|
||||
report: Report a privacy concern with this image
|
||||
view_on_bing: "View on Bing Maps"
|
||||
hires: "High resolution"
|
||||
vegbilder:
|
||||
title: "Vegbilder"
|
||||
tooltip: "Street-level photos from the Norwegian Public Roads Administration"
|
||||
publisher: "Norwegian Public Roads Administration"
|
||||
view_on: "View it on Vegbilder"
|
||||
mapillary_images:
|
||||
tooltip: "Street-level photos from Mapillary"
|
||||
mapillary_map_features:
|
||||
@@ -1435,6 +1440,8 @@ en:
|
||||
mapilio:
|
||||
title: Mapilio
|
||||
tooltip: "Street-level photos from Mapilio"
|
||||
street_side:
|
||||
minzoom_tooltip: "Zoom in to see street-side photos"
|
||||
note:
|
||||
note: Note
|
||||
title: Edit note
|
||||
|
||||
@@ -252,6 +252,7 @@ export function rendererBackground(context) {
|
||||
'mapillary-map-features': 'Mapillary Map Features',
|
||||
'mapillary-signs': 'Mapillary Signs',
|
||||
kartaview: 'KartaView Images',
|
||||
vegbilder: 'Norwegian Road Administration Images',
|
||||
mapilio: 'Mapilio Images'
|
||||
};
|
||||
|
||||
|
||||
@@ -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'];
|
||||
var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder'];
|
||||
var _allPhotoTypes = ['flat', 'panoramic'];
|
||||
var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy
|
||||
var _dateFilters = ['fromDate', 'toDate'];
|
||||
@@ -119,12 +119,12 @@ export function rendererPhotos(context) {
|
||||
}
|
||||
|
||||
photos.shouldFilterByDate = function() {
|
||||
return showsLayer('mapillary') || showsLayer('kartaview') || showsLayer('streetside');
|
||||
return showsLayer('mapillary') || showsLayer('kartaview') || showsLayer('streetside') || showsLayer('vegbilder');
|
||||
};
|
||||
|
||||
photos.shouldFilterByPhotoType = function() {
|
||||
return showsLayer('mapillary') ||
|
||||
(showsLayer('streetside') && showsLayer('kartaview'));
|
||||
(showsLayer('streetside') && showsLayer('kartaview')) || showsLayer('vegbilder');
|
||||
};
|
||||
|
||||
photos.shouldFilterByUsername = function() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import serviceMapRules from './maprules';
|
||||
import serviceNominatim from './nominatim';
|
||||
import serviceNsi from './nsi';
|
||||
import serviceKartaview from './kartaview';
|
||||
import serviceVegbilder from './vegbilder';
|
||||
import serviceOsm from './osm';
|
||||
import serviceOsmWikibase from './osm_wikibase';
|
||||
import serviceStreetside from './streetside';
|
||||
@@ -24,6 +25,7 @@ export let services = {
|
||||
mapillary: serviceMapillary,
|
||||
nsi: serviceNsi,
|
||||
kartaview: serviceKartaview,
|
||||
vegbilder: serviceVegbilder,
|
||||
osm: serviceOsm,
|
||||
osmWikibase: serviceOsmWikibase,
|
||||
maprules: serviceMapRules,
|
||||
@@ -44,6 +46,7 @@ export {
|
||||
serviceNominatim,
|
||||
serviceNsi,
|
||||
serviceKartaview,
|
||||
serviceVegbilder,
|
||||
serviceOsm,
|
||||
serviceOsmWikibase,
|
||||
serviceStreetside,
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
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) {
|
||||
const head = d3_select('head');
|
||||
|
||||
return Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
// 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', resolve)
|
||||
.on('error.pannellum', reject);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
// load 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', resolve)
|
||||
.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: false,
|
||||
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);
|
||||
dispatch.call('viewerChanged');
|
||||
|
||||
if (_currScenes.length > 3) {
|
||||
const old_key = _currScenes.shift();
|
||||
_pannellumViewer.removeScene(old_key);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getYaw: function() {
|
||||
return _pannellumViewer.getYaw();
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';
|
||||
import { utilSetTransform, utilRebind } from '../util';
|
||||
|
||||
const dispatch = d3_dispatch('viewerChanged');
|
||||
|
||||
let _photo;
|
||||
let _wrapper;
|
||||
let imgZoom;
|
||||
let _widthOverflow;
|
||||
|
||||
function zoomPan (d3_event) {
|
||||
let t = d3_event.transform;
|
||||
_photo.call(utilSetTransform, t.x, t.y, t.k);
|
||||
}
|
||||
|
||||
function zoomBeahvior () {
|
||||
const {width: wrapperWidth, height: wrapperHeight} = _wrapper.node().getBoundingClientRect();
|
||||
const {naturalHeight, naturalWidth} = _photo.node();
|
||||
const intrinsicRatio = naturalWidth / naturalHeight;
|
||||
_widthOverflow = wrapperHeight * intrinsicRatio - wrapperWidth;
|
||||
return d3_zoom()
|
||||
.extent([[0, 0], [wrapperWidth, wrapperHeight]])
|
||||
.translateExtent([[0, 0], [wrapperWidth + _widthOverflow, wrapperHeight]])
|
||||
.scaleExtent([1, 15])
|
||||
.on('zoom', zoomPan);
|
||||
}
|
||||
|
||||
function loadImage (selection, path) {
|
||||
return new Promise((resolve) => {
|
||||
selection.attr('src', path);
|
||||
selection.on('load', () => {
|
||||
resolve(selection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
init: async function(context, selection) {
|
||||
this.event = utilRebind(this, dispatch, 'on');
|
||||
|
||||
_wrapper = selection
|
||||
.append('div')
|
||||
.attr('class', 'photo-frame plane-frame')
|
||||
.classed('hide', true);
|
||||
|
||||
_photo = _wrapper
|
||||
.append('img')
|
||||
.attr('class', 'plane-photo');
|
||||
|
||||
context.ui().photoviewer.on('resize.plane', () => {
|
||||
imgZoom = zoomBeahvior();
|
||||
_wrapper.call(imgZoom);
|
||||
});
|
||||
|
||||
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, keepOrientation) {
|
||||
dispatch.call('viewerChanged');
|
||||
loadImage(_photo, '');
|
||||
loadImage(_photo, data.image_path)
|
||||
.then(() => {
|
||||
if (!keepOrientation) {
|
||||
imgZoom = zoomBeahvior();
|
||||
_wrapper.call(imgZoom);
|
||||
_wrapper.call(imgZoom.transform, d3_zoomIdentity.translate(-_widthOverflow / 2, 0));
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
getYaw: function() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -20,8 +20,8 @@ import { utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, util
|
||||
const bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?';
|
||||
const streetsideImagesApi = 'https://t.ssl.ak.tiles.virtualearth.net/tiles/';
|
||||
const bubbleAppKey = 'AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm';
|
||||
const pannellumViewerCSS = 'pannellum-streetside/pannellum.css';
|
||||
const pannellumViewerJS = 'pannellum-streetside/pannellum.js';
|
||||
const pannellumViewerCSS = 'pannellum/pannellum.css';
|
||||
const pannellumViewerJS = 'pannellum/pannellum.js';
|
||||
const maxResults = 2000;
|
||||
const tileZoom = 16.5;
|
||||
const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
import { json as d3_json, xml as d3_xml} from 'd3-fetch';
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { pairs as d3_pairs } from 'd3-array';
|
||||
import RBush from 'rbush';
|
||||
import { iso1A2Codes } from '@rapideditor/country-coder';
|
||||
import { t, localizer } from '../core/localizer';
|
||||
import { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs} from '../util';
|
||||
import {geoExtent, geoScaleToZoom, geoVecAngle, geoVecEqual} from '../geo';
|
||||
import pannellumPhotoFrame from './pannellum_photo';
|
||||
import planePhotoFrame from './plane_photo';
|
||||
|
||||
|
||||
const owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?';
|
||||
const tileZoom = 14;
|
||||
const tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);
|
||||
const dispatch = d3_dispatch('loadedImages', 'viewerChanged');
|
||||
const directionEnum = Object.freeze({
|
||||
forward: Symbol(0),
|
||||
backward: Symbol(1)
|
||||
});
|
||||
|
||||
let _planeFrame;
|
||||
let _pannellumFrame;
|
||||
let _currentFrame;
|
||||
let _loadViewerPromise;
|
||||
let _vegbilderCache;
|
||||
|
||||
async function fetchAvailableLayers() {
|
||||
const params = {
|
||||
service: 'WFS',
|
||||
request: 'GetCapabilities',
|
||||
version: '2.0.0',
|
||||
};
|
||||
|
||||
const urlForRequest = owsEndpoint + utilQsString(params);
|
||||
const repsonse = await d3_xml(urlForRequest);
|
||||
const xPathSelector = '/wfs:WFS_Capabilities/wfs:FeatureTypeList/wfs:FeatureType/wfs:Name';
|
||||
const regexMatcher = /^vegbilder_1_0:Vegbilder(?<image_type>_360)?_(?<year>\d{4})$/;
|
||||
const NSResolver = repsonse.createNSResolver(repsonse);
|
||||
const l = repsonse.evaluate(
|
||||
xPathSelector,
|
||||
repsonse,
|
||||
NSResolver,
|
||||
XPathResult.ANY_TYPE
|
||||
);
|
||||
let node;
|
||||
const availableLayers = [];
|
||||
while ( (node = l.iterateNext()) !== null ) {
|
||||
const match = node.textContent?.match(regexMatcher);
|
||||
if (match) {
|
||||
availableLayers.push({
|
||||
name: match[0],
|
||||
is_sphere: !!match.groups?.image_type,
|
||||
year: parseInt(match.groups?.year, 10)
|
||||
});
|
||||
}
|
||||
}
|
||||
return availableLayers;
|
||||
}
|
||||
|
||||
function filterAvailableLayers(photoContex) {
|
||||
const fromDateString = photoContex.fromDate();
|
||||
const toDateString = photoContex.toDate();
|
||||
const fromYear = fromDateString ? new Date(fromDateString).getFullYear() : 2016;
|
||||
const toYear = toDateString ? new Date(toDateString).getFullYear() : null;
|
||||
const showsFlat = photoContex.showsFlat();
|
||||
const showsPano = photoContex.showsPanoramic();
|
||||
return Array.from(_vegbilderCache.wfslayers.values()).filter(({layerInfo}) => (
|
||||
(layerInfo.year >= fromYear) &&
|
||||
(!toYear || (layerInfo.year <= toYear)) &&
|
||||
((!layerInfo.is_sphere && showsFlat) || (layerInfo.is_sphere && showsPano))
|
||||
));
|
||||
}
|
||||
|
||||
function loadWFSLayers(projection, margin, wfslayers) {
|
||||
const tiles = tiler.margin(margin).getTiles(projection);
|
||||
for (const cache of wfslayers) {
|
||||
loadWFSLayer(projection, cache, tiles);
|
||||
}
|
||||
}
|
||||
|
||||
function loadWFSLayer(projection, cache, tiles) {
|
||||
// abort inflight requests that are no longer needed
|
||||
for (const [key, controller] of cache.inflight.entries()) {
|
||||
const wanted = tiles.some(tile => key === tile.id);
|
||||
if (!wanted) {
|
||||
controller.abort();
|
||||
cache.inflight.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
Promise.all(tiles.map(
|
||||
tile => loadTile(cache, cache.layerInfo.name, tile)
|
||||
)).then(() => orderSequences(projection, cache));
|
||||
}
|
||||
|
||||
/**
|
||||
* loadNextTilePage() load data for the next tile page in line.
|
||||
*/
|
||||
async function loadTile(cache, typename, tile) {
|
||||
const bbox = tile.extent.bbox();
|
||||
const tileid = tile.id;
|
||||
if ((cache.loaded.get(tileid) === true) || cache.inflight.has(tileid)) return;
|
||||
|
||||
const params = {
|
||||
service: 'WFS',
|
||||
request: 'GetFeature',
|
||||
version: '2.0.0',
|
||||
typenames: typename,
|
||||
bbox: [bbox.minY, bbox.minX, bbox.maxY, bbox.maxX].join(','),
|
||||
outputFormat: 'json'
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
cache.inflight.set(tileid, controller);
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
const urlForRequest = owsEndpoint + utilQsString(params);
|
||||
|
||||
let featureCollection;
|
||||
try {
|
||||
featureCollection = await d3_json(urlForRequest, options);
|
||||
} catch {
|
||||
cache.loaded.set(tileid, false);
|
||||
return;
|
||||
} finally {
|
||||
cache.inflight.delete(tileid);
|
||||
}
|
||||
|
||||
cache.loaded.set(tileid, true);
|
||||
|
||||
if (featureCollection.features.length === 0) { return; }
|
||||
|
||||
const features = featureCollection.features.map(feature => {
|
||||
const loc = feature.geometry.coordinates;
|
||||
const key = feature.id;
|
||||
const properties = feature.properties;
|
||||
const {
|
||||
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], 10);
|
||||
const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward;
|
||||
const data = {
|
||||
loc,
|
||||
key,
|
||||
ca,
|
||||
image_path,
|
||||
preview_path,
|
||||
road_reference: roadReference(properties),
|
||||
metering,
|
||||
lane_code,
|
||||
direction,
|
||||
captured_at: new Date(captured_at),
|
||||
is_sphere: image_type === '360'
|
||||
};
|
||||
|
||||
cache.points.set(key, data);
|
||||
|
||||
return {
|
||||
minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data
|
||||
};
|
||||
});
|
||||
|
||||
_vegbilderCache.rtree.load(features);
|
||||
dispatch.call('loadedImages');
|
||||
}
|
||||
|
||||
function orderSequences(projection, cache) {
|
||||
const {points} = cache;
|
||||
|
||||
const grouped = Array.from(points.values()).reduce((grouped, image) => {
|
||||
const key = image.road_reference;
|
||||
if (grouped.has(key)) {
|
||||
grouped.get(key).push(image);
|
||||
} else {
|
||||
grouped.set(key, [image]);
|
||||
}
|
||||
return grouped;
|
||||
}, new Map());
|
||||
|
||||
const imageSequences = Array.from(grouped.values()).reduce((imageSequences, imageGroup) => {
|
||||
imageGroup.sort((a, b) => {
|
||||
if (a.captured_at.valueOf() > b.captured_at.valueOf()) {
|
||||
return 1;
|
||||
} else if (a.captured_at.valueOf() < b.captured_at.valueOf()) {
|
||||
return -1;
|
||||
} else {
|
||||
const {direction} = a;
|
||||
if (direction === directionEnum.forward) {
|
||||
return a.metering - b.metering;
|
||||
} else {
|
||||
return b.metering - a.metering;
|
||||
}
|
||||
}
|
||||
});
|
||||
let imageSequence = [imageGroup[0]];
|
||||
let angle = null;
|
||||
for (const [lastImage, image] of d3_pairs(imageGroup)) {
|
||||
if (lastImage.ca === null) {
|
||||
const b = projection(lastImage.loc);
|
||||
const a = projection(image.loc);
|
||||
if (!geoVecEqual(a, b)) {
|
||||
angle = geoVecAngle(a, b);
|
||||
angle *= (180 / Math.PI);
|
||||
angle -= 90;
|
||||
angle = angle >= 0 ? angle : angle + 360;
|
||||
}
|
||||
lastImage.ca = angle;
|
||||
}
|
||||
if (
|
||||
image.direction === lastImage.direction &&
|
||||
image.captured_at.valueOf() - lastImage.captured_at.valueOf() <= 20000
|
||||
) {
|
||||
imageSequence.push(image);
|
||||
} else {
|
||||
imageSequences.push(imageSequence);
|
||||
imageSequence = [image];
|
||||
}
|
||||
}
|
||||
imageSequences.push(imageSequence);
|
||||
return imageSequences;
|
||||
}, []);
|
||||
|
||||
cache.sequences = imageSequences.map(images => {
|
||||
const seqence = {
|
||||
images,
|
||||
key: images[0].key,
|
||||
geometry : {
|
||||
type : 'LineString',
|
||||
coordinates : images.map(image => image.loc)
|
||||
}};
|
||||
for (const image of images) {
|
||||
_vegbilderCache.image2sequence_map.set(image.key, seqence);
|
||||
}
|
||||
return seqence;
|
||||
});
|
||||
}
|
||||
|
||||
function roadReference(properties) {
|
||||
const {
|
||||
FYLKENUMMER: county_number,
|
||||
VEGKATEGORI: road_class,
|
||||
VEGSTATUS: road_status,
|
||||
VEGNUMMER: road_number,
|
||||
STREKNING: section,
|
||||
DELSTREKNING: subsection,
|
||||
HP: parcel,
|
||||
KRYSSDEL: junction_part,
|
||||
SIDEANLEGGSDEL: services_part,
|
||||
ANKERPUNKT: anker_point,
|
||||
AAR: year,
|
||||
} = properties;
|
||||
|
||||
let reference;
|
||||
|
||||
if (year >= 2020) {
|
||||
reference = `${road_class}${road_status}${road_number} S${section}D${subsection}`;
|
||||
if (junction_part) {
|
||||
reference = `${reference} M${anker_point} KD${junction_part}`;
|
||||
} else if (services_part) {
|
||||
reference = `${reference} M${anker_point} SD${services_part}`;
|
||||
}
|
||||
} else {
|
||||
reference = `${county_number}${road_class}${road_status}${road_number} HP${parcel}`;
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
function localeTimestamp(date) {
|
||||
const options = { day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: 'numeric', minute: 'numeric', second: 'numeric' };
|
||||
return date.toLocaleString(localizer.localeCode(), options);
|
||||
}
|
||||
|
||||
function partitionViewport(projection) {
|
||||
const zoom = geoScaleToZoom(projection.scale());
|
||||
const roundZoom = (Math.ceil(zoom * 2) / 2) + 2.5; // round to next 0.5 and add 2.5
|
||||
const tiler = utilTiler().zoomExtent([roundZoom, roundZoom]);
|
||||
|
||||
return tiler.getTiles(projection)
|
||||
.map(tile => tile.extent);
|
||||
}
|
||||
|
||||
function searchLimited(limit, projection, rtree) {
|
||||
limit ??= 5;
|
||||
|
||||
return partitionViewport(projection)
|
||||
.reduce((result, extent) => {
|
||||
const found = rtree.search(extent.bbox())
|
||||
.slice(0, limit)
|
||||
.map(d => d.data);
|
||||
|
||||
return result.concat(found);
|
||||
}, []);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
init: function () {
|
||||
this.event = utilRebind(this, dispatch, 'on');
|
||||
},
|
||||
|
||||
reset: async function () {
|
||||
if (_vegbilderCache) {
|
||||
for (const layer of _vegbilderCache.wfslayers.values()) {
|
||||
for (const controller of layer.inflight.values()) {
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_vegbilderCache = {
|
||||
wfslayers: new Map(),
|
||||
rtree: new RBush(),
|
||||
image2sequence_map: new Map()
|
||||
};
|
||||
|
||||
const availableLayers = await fetchAvailableLayers();
|
||||
const {wfslayers} = _vegbilderCache;
|
||||
|
||||
for (const layerInfo of availableLayers) {
|
||||
const cache = {
|
||||
layerInfo,
|
||||
loaded: new Map(),
|
||||
inflight: new Map(),
|
||||
points: new Map(),
|
||||
sequences: []
|
||||
};
|
||||
wfslayers.set(layerInfo.name, cache);
|
||||
}
|
||||
},
|
||||
|
||||
images: function (projection) {
|
||||
const limit = 5;
|
||||
return searchLimited(limit, projection, _vegbilderCache.rtree);
|
||||
},
|
||||
|
||||
|
||||
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 seen = new Set();
|
||||
const line_strings = [];
|
||||
|
||||
for (const {data} of _vegbilderCache.rtree.search(bbox)) {
|
||||
const sequence = _vegbilderCache.image2sequence_map.get(data.key);
|
||||
if (!sequence) continue;
|
||||
const {key, geometry, images} = sequence;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const line = {
|
||||
type: 'LineString',
|
||||
coordinates: geometry.coordinates,
|
||||
key,
|
||||
images
|
||||
};
|
||||
line_strings.push(line);
|
||||
}
|
||||
return line_strings;
|
||||
},
|
||||
|
||||
cachedImage: function (key) {
|
||||
for (const {points} of _vegbilderCache.wfslayers.values()) {
|
||||
if (points.has(key)) return points.get(key);
|
||||
}
|
||||
},
|
||||
|
||||
getSequenceForImage: function (image) {
|
||||
return _vegbilderCache?.image2sequence_map.get(image?.key);
|
||||
},
|
||||
|
||||
loadImages: async function (context, margin) {
|
||||
if (!_vegbilderCache) {
|
||||
await this.reset();
|
||||
}
|
||||
margin ??= 1;
|
||||
const wfslayers = filterAvailableLayers(context.photos());
|
||||
loadWFSLayers(context.projection, margin, wfslayers);
|
||||
},
|
||||
|
||||
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];
|
||||
|
||||
if (!nextImage) return;
|
||||
|
||||
context.map().centerEase(nextImage.loc);
|
||||
this.selectImage(context, nextImage.key, true);
|
||||
};
|
||||
|
||||
const wrap = context.container().select('.photoviewer')
|
||||
.selectAll('.vegbilder-wrapper')
|
||||
.data([0]);
|
||||
|
||||
const wrapEnter = wrap.enter()
|
||||
.append('div')
|
||||
.attr('class', 'photo-wrapper vegbilder-wrapper')
|
||||
.classed('hide', true);
|
||||
|
||||
wrapEnter
|
||||
.append('div')
|
||||
.attr('class', 'photo-attribution fillD');
|
||||
|
||||
const controlsEnter = wrapEnter
|
||||
.append('div')
|
||||
.attr('class', 'photo-controls-wrap')
|
||||
.append('div')
|
||||
.attr('class', 'photo-controls');
|
||||
|
||||
controlsEnter
|
||||
.append('button')
|
||||
.on('click.back', step(-1))
|
||||
.text('◄');
|
||||
|
||||
controlsEnter
|
||||
.append('button')
|
||||
.on('click.forward', step(1))
|
||||
.text('►');
|
||||
|
||||
_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'));
|
||||
});
|
||||
|
||||
return _loadViewerPromise;
|
||||
},
|
||||
|
||||
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); }
|
||||
|
||||
this.setStyles(context, null, true);
|
||||
|
||||
if (!d) return this;
|
||||
|
||||
const wrap = context.container().select('.photoviewer .vegbilder-wrapper');
|
||||
const attribution = wrap.selectAll('.photo-attribution').text('');
|
||||
|
||||
if (d.captured_at) {
|
||||
attribution
|
||||
.append('span')
|
||||
.attr('class', 'captured_at')
|
||||
.text(localeTimestamp(d.captured_at));
|
||||
}
|
||||
|
||||
attribution
|
||||
.append('a')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', 'https://vegvesen.no')
|
||||
.call(t.append('vegbilder.publisher'));
|
||||
|
||||
attribution
|
||||
.append('a')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', `https://vegbilder.atlas.vegvesen.no/?year=${d.captured_at.getFullYear()}&lat=${d.loc[1]}&lng=${d.loc[0]}&view=image&imageId=${d.key}`)
|
||||
.call(t.append('vegbilder.view_on'));
|
||||
|
||||
_currentFrame = d.is_sphere? _pannellumFrame : _planeFrame;
|
||||
|
||||
_currentFrame
|
||||
.selectPhoto(d, keepOrientation)
|
||||
.showPhotoFrame(wrap);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
showViewer: function (context) {
|
||||
const viewer = context.container().select('.photoviewer')
|
||||
.classed('hide', false);
|
||||
|
||||
const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();
|
||||
|
||||
if (isHidden) {
|
||||
viewer
|
||||
.selectAll('.photo-wrapper:not(.vegbilder-wrapper)')
|
||||
.classed('hide', true);
|
||||
|
||||
viewer
|
||||
.selectAll('.photo-wrapper.vegbilder-wrapper')
|
||||
.classed('hide', false);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
hideViewer: function(context) {
|
||||
this.updateUrlImage(null);
|
||||
|
||||
const 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')
|
||||
.classed('currentView', false);
|
||||
|
||||
return this.setStyles(context, null, true);
|
||||
},
|
||||
|
||||
|
||||
// Updates the currently highlighted sequence and selected bubble.
|
||||
// Reset is only necessary when interacting with the viewport because
|
||||
// this implicitly changes the currently selected bubble/sequence
|
||||
setStyles: function (context, hovered, reset) {
|
||||
if (reset) { // reset all layers
|
||||
context.container().selectAll('.viewfield-group')
|
||||
.classed('highlighted', false)
|
||||
.classed('hovered', false)
|
||||
.classed('currentView', false);
|
||||
|
||||
context.container().selectAll('.sequence')
|
||||
.classed('highlighted', false)
|
||||
.classed('currentView', false);
|
||||
}
|
||||
|
||||
const hoveredImageKey = hovered?.key;
|
||||
const hoveredSequence = this.getSequenceForImage(hovered);
|
||||
const hoveredSequenceKey = hoveredSequence?.key;
|
||||
const hoveredImageKeys = hoveredSequence?.images.map(d => d.key) ?? [];
|
||||
|
||||
const viewer = context.container().select('.photoviewer');
|
||||
const selected = viewer.empty() ? undefined : viewer.datum();
|
||||
const selectedImageKey = selected?.key;
|
||||
const selectedSequence = this.getSequenceForImage(selected);
|
||||
const selectedSequenceKey = selectedSequence?.key;
|
||||
const selectedImageKeys = selectedSequence?.images.map(d => d.key) ?? [];
|
||||
|
||||
// highlight sibling viewfields on either the selected or the hovered sequences
|
||||
const highlightedImageKeys = utilArrayUnion(hoveredImageKeys, selectedImageKeys);
|
||||
|
||||
context.container().selectAll('.layer-vegbilder .viewfield-group')
|
||||
.classed('highlighted', d => highlightedImageKeys.indexOf(d.key) !== -1)
|
||||
.classed('hovered', d => d.key === hoveredImageKey)
|
||||
.classed('currentView', d => d.key === selectedImageKey);
|
||||
|
||||
context.container().selectAll('.layer-vegbilder .sequence')
|
||||
.classed('highlighted', d => d.key === hoveredSequenceKey)
|
||||
.classed('currentView', d => d.key === selectedSequenceKey);
|
||||
|
||||
// update viewfields if needed
|
||||
context.container().selectAll('.layer-vegbilder .viewfield-group .viewfield')
|
||||
.attr('d', viewfieldPath);
|
||||
|
||||
function viewfieldPath() {
|
||||
const d = this.parentNode.__data__;
|
||||
if (d.is_sphere && d.key !== selectedImageKey) {
|
||||
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;
|
||||
},
|
||||
|
||||
updateUrlImage: function (key) {
|
||||
if (!window.mocha) {
|
||||
const hash = utilStringQs(window.location.hash);
|
||||
if (key) {
|
||||
hash.photo = 'vegbilder/' + key;
|
||||
} else {
|
||||
delete hash.photo;
|
||||
}
|
||||
window.location.replace('#' + utilQsString(hash, true));
|
||||
}
|
||||
},
|
||||
|
||||
validHere: function(extent) {
|
||||
const bbox = Object.values(extent.bbox());
|
||||
return iso1A2Codes(bbox).includes('NO');
|
||||
},
|
||||
|
||||
|
||||
cache: function () {
|
||||
return _vegbilderCache;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -22,6 +22,7 @@ export { svgPoints } from './points.js';
|
||||
export { svgRelationMemberTags } from './helpers.js';
|
||||
export { svgSegmentWay } from './helpers.js';
|
||||
export { svgStreetside } from './streetside.js';
|
||||
export { svgVegbilder } from './vegbilder';
|
||||
export { svgTagClasses } from './tag_classes.js';
|
||||
export { svgTagPattern } from './tag_pattern.js';
|
||||
export { svgTouch } from './touch.js';
|
||||
|
||||
@@ -301,6 +301,10 @@ export function svgKartaviewImages(projection, context, dispatch) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawImages;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { svgKeepRight } from './keepRight';
|
||||
import { svgImproveOSM } from './improveOSM';
|
||||
import { svgOsmose } from './osmose';
|
||||
import { svgStreetside } from './streetside';
|
||||
import { svgVegbilder} from './vegbilder';
|
||||
import { svgMapillaryImages } from './mapillary_images';
|
||||
import { svgMapillaryPosition } from './mapillary_position';
|
||||
import { svgMapillarySigns } from './mapillary_signs';
|
||||
@@ -38,6 +39,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: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },
|
||||
{ id: 'debug', layer: svgDebug(projection, context, dispatch) },
|
||||
{ id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },
|
||||
{ id: 'touch', layer: svgTouch(projection, context, dispatch) },
|
||||
|
||||
@@ -243,6 +243,10 @@ export function svgMapilioImages(projection, context, dispatch) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawImages;
|
||||
|
||||
@@ -310,6 +310,10 @@ export function svgMapillaryImages(projection, context, dispatch) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawImages;
|
||||
|
||||
@@ -208,6 +208,10 @@ export function svgMapillaryMapFeatures(projection, context, dispatch) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawMapFeatures.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawMapFeatures;
|
||||
|
||||
@@ -157,6 +157,10 @@ export function svgMapillaryPosition(projection, context) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawImages;
|
||||
|
||||
@@ -197,6 +197,10 @@ export function svgMapillarySigns(projection, context, dispatch) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawSigns.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawSigns;
|
||||
|
||||
@@ -379,6 +379,10 @@ export function svgStreetside(projection, context, dispatch) {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return drawImages;
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
import _throttle from 'lodash-es/throttle';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import { svgPath, svgPointTransform } from './helpers';
|
||||
import { services } from '../services';
|
||||
|
||||
|
||||
export function svgVegbilder(projection, context, dispatch) {
|
||||
const throttledRedraw = _throttle(() => dispatch.call('change'), 1000);
|
||||
const minZoom = 14;
|
||||
const minMarkerZoom = 16;
|
||||
const minViewfieldZoom = 18;
|
||||
let layer = d3_select(null);
|
||||
let _viewerYaw = 0;
|
||||
let _vegbilder;
|
||||
|
||||
/**
|
||||
* init().
|
||||
*/
|
||||
function init() {
|
||||
if (svgVegbilder.initialized) return; // run once
|
||||
svgVegbilder.enabled = false;
|
||||
svgVegbilder.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* getService().
|
||||
*/
|
||||
function getService() {
|
||||
if (services.vegbilder && !_vegbilder) {
|
||||
_vegbilder = services.vegbilder;
|
||||
_vegbilder.event
|
||||
.on('viewerChanged.svgVegbilder', viewerChanged)
|
||||
.on('loadedImages.svgVegbilder', throttledRedraw);
|
||||
} else if (!services.vegbilder && _vegbilder) {
|
||||
_vegbilder = null;
|
||||
}
|
||||
|
||||
return _vegbilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* showLayer().
|
||||
*/
|
||||
function showLayer() {
|
||||
const service = getService();
|
||||
if (!service) return;
|
||||
|
||||
editOn();
|
||||
|
||||
layer
|
||||
.style('opacity', 0)
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 1)
|
||||
.on('end', () => dispatch.call('change'));
|
||||
}
|
||||
|
||||
/**
|
||||
* hideLayer().
|
||||
*/
|
||||
function hideLayer() {
|
||||
throttledRedraw.cancel();
|
||||
|
||||
layer
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 0)
|
||||
.on('end', editOff);
|
||||
}
|
||||
|
||||
/**
|
||||
* editOn().
|
||||
*/
|
||||
function editOn() {
|
||||
layer.style('display', 'block');
|
||||
}
|
||||
|
||||
/**
|
||||
* editOff().
|
||||
*/
|
||||
function editOff() {
|
||||
layer.selectAll('.viewfield-group').remove();
|
||||
layer.style('display', 'none');
|
||||
}
|
||||
|
||||
function click(d3_event, d) {
|
||||
const service = getService();
|
||||
if (!service) return;
|
||||
|
||||
service
|
||||
.ensureViewerLoaded(context)
|
||||
.then(() => {
|
||||
service
|
||||
.selectImage(context, d.key)
|
||||
.showViewer(context);
|
||||
});
|
||||
|
||||
context.map().centerEase(d.loc);
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseover().
|
||||
*/
|
||||
function mouseover(d3_event, d) {
|
||||
const service = getService();
|
||||
if (service) service.setStyles(context, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* mouseout().
|
||||
*/
|
||||
function mouseout() {
|
||||
const service = getService();
|
||||
if (service) service.setStyles(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* transform().
|
||||
*/
|
||||
function transform(d, selected) {
|
||||
let t = svgPointTransform(projection)(d);
|
||||
let rot = d.ca;
|
||||
if (d === selected) {
|
||||
rot += _viewerYaw;
|
||||
}
|
||||
if (rot) {
|
||||
t += ' rotate(' + Math.floor(rot) + ',0,0)';
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
function viewerChanged() {
|
||||
const service = getService();
|
||||
if (!service) return;
|
||||
|
||||
const frame = service.photoFrame();
|
||||
|
||||
// update viewfield rotation
|
||||
_viewerYaw = frame.getYaw();
|
||||
|
||||
// avoid updating if the map is currently transformed
|
||||
// e.g. during drags or easing.
|
||||
if (context.map().isTransformed()) return;
|
||||
|
||||
layer.selectAll('.viewfield-group.currentView')
|
||||
.attr('transform', (d) => transform(d, d));
|
||||
}
|
||||
|
||||
function filterImages(images) {
|
||||
const photoContext = context.photos();
|
||||
const fromDateString = photoContext.fromDate();
|
||||
const toDateString = photoContext.toDate();
|
||||
const showsFlat = photoContext.showsFlat();
|
||||
const showsPano = photoContext.showsPanoramic();
|
||||
|
||||
if (fromDateString) {
|
||||
const fromDate = new Date(fromDateString);
|
||||
images = images.filter(image => image.captured_at.getTime() >= fromDate.getTime());
|
||||
}
|
||||
|
||||
if (toDateString) {
|
||||
const toDate = new Date(toDateString);
|
||||
images = images.filter(image => image.captured_at.getTime() <= toDate.getTime());
|
||||
}
|
||||
|
||||
if (!showsPano) {
|
||||
images = images.filter(image => !image.is_sphere);
|
||||
}
|
||||
|
||||
if (!showsFlat) {
|
||||
images = images.filter(image => image.is_sphere);
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
function filterSequences(sequences) {
|
||||
const photoContext = context.photos();
|
||||
const fromDateString = photoContext.fromDate();
|
||||
const toDateString = photoContext.toDate();
|
||||
const showsFlat = photoContext.showsFlat();
|
||||
const showsPano = photoContext.showsPanoramic();
|
||||
|
||||
if (fromDateString) {
|
||||
const fromDate = new Date(fromDateString);
|
||||
sequences = sequences.filter(({images}) => images[0].captured_at.getTime() >= fromDate.getTime());
|
||||
}
|
||||
|
||||
if (toDateString) {
|
||||
const toDate = new Date(toDateString);
|
||||
sequences = sequences.filter(({images}) => images[images.length - 1].captured_at.getTime() <= toDate.getTime());
|
||||
}
|
||||
|
||||
if (!showsPano) {
|
||||
sequences = sequences.filter(({images}) => !images[0].is_sphere);
|
||||
}
|
||||
|
||||
if (!showsFlat) {
|
||||
sequences = sequences.filter(({images}) => images[0].is_sphere);
|
||||
}
|
||||
|
||||
return sequences;
|
||||
}
|
||||
|
||||
/**
|
||||
* update().
|
||||
*/
|
||||
function update() {
|
||||
const viewer = context.container().select('.photoviewer');
|
||||
const selected = viewer.empty() ? undefined : viewer.datum();
|
||||
const z = ~~context.map().zoom();
|
||||
const showMarkers = (z >= minMarkerZoom);
|
||||
const showViewfields = (z >= minViewfieldZoom);
|
||||
const service = getService();
|
||||
let sequences = [];
|
||||
let images = [];
|
||||
|
||||
if (service) {
|
||||
// The WFS-layer for that year or image type may not be loaded after a filter is changed
|
||||
service.loadImages(context);
|
||||
|
||||
sequences = service.sequences(projection);
|
||||
images = showMarkers ? service.images(projection) : [];
|
||||
images = filterImages(images);
|
||||
sequences = filterSequences(sequences);
|
||||
}
|
||||
|
||||
let traces = layer.selectAll('.sequences').selectAll('.sequence')
|
||||
.data(sequences, d => d.key);
|
||||
|
||||
// exit
|
||||
traces.exit()
|
||||
.remove();
|
||||
|
||||
// enter/update
|
||||
traces.enter()
|
||||
.append('path')
|
||||
.attr('class', 'sequence')
|
||||
.merge(traces)
|
||||
.attr('d', svgPath(projection).geojson);
|
||||
|
||||
|
||||
const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
|
||||
.data(images, (d) => d.key);
|
||||
|
||||
// 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((a, b) => {
|
||||
return (a === selected) ? 1
|
||||
: (b === selected) ? -1
|
||||
: b.loc[1] - a.loc[1];
|
||||
})
|
||||
.attr('transform', (d) => transform(d, selected))
|
||||
.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 may or may not be drawn...
|
||||
// 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() {
|
||||
const d = this.parentNode.__data__;
|
||||
if (d.is_sphere) {
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* drawImages()
|
||||
* drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.
|
||||
* 'svgStreetside()' is called from index.js
|
||||
*/
|
||||
function drawImages(selection) {
|
||||
const enabled = svgVegbilder.enabled;
|
||||
const service = getService();
|
||||
|
||||
layer = selection.selectAll('.layer-vegbilder')
|
||||
.data(service ? [0] : []);
|
||||
|
||||
layer.exit()
|
||||
.remove();
|
||||
|
||||
const layerEnter = layer.enter()
|
||||
.append('g')
|
||||
.attr('class', 'layer-vegbilder')
|
||||
.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(context);
|
||||
} else {
|
||||
editOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* drawImages.enabled().
|
||||
*/
|
||||
drawImages.enabled = function (_) {
|
||||
if (!arguments.length) return svgVegbilder.enabled;
|
||||
svgVegbilder.enabled = _;
|
||||
if (svgVegbilder.enabled) {
|
||||
showLayer();
|
||||
context.photos().on('change.vegbilder', update);
|
||||
} else {
|
||||
hideLayer();
|
||||
context.photos().on('change.vegbilder', null);
|
||||
}
|
||||
dispatch.call('change');
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* drawImages.supported().
|
||||
*/
|
||||
drawImages.supported = function () {
|
||||
return !!getService();
|
||||
};
|
||||
|
||||
drawImages.rendered = function(zoom) {
|
||||
return zoom >= minZoom;
|
||||
};
|
||||
|
||||
drawImages.validHere = function(extent, zoom) {
|
||||
return zoom >= (minZoom - 2)
|
||||
&& getService().validHere(extent);
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
return drawImages;
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export function uiPhotoviewer(context) {
|
||||
if (services.mapillary) { services.mapillary.hideViewer(context); }
|
||||
if (services.kartaview) { services.kartaview.hideViewer(context); }
|
||||
if (services.mapilio) { services.mapilio.hideViewer(context); }
|
||||
if (services.vegbilder) { services.vegbilder.hideViewer(context); }
|
||||
})
|
||||
.append('div')
|
||||
.call(svgIcon('#iD-icon-close'));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _debounce from 'lodash-es/debounce';
|
||||
import {
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
@@ -33,7 +34,14 @@ export function uiSectionPhotoOverlays(context) {
|
||||
function drawPhotoItems(selection) {
|
||||
var photoKeys = context.photos().overlayLayerIDs();
|
||||
var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });
|
||||
var data = photoLayers.filter(function(obj) { return obj.layer.supported(); });
|
||||
var data = photoLayers.filter(function(obj) {
|
||||
if (!obj.layer.supported()) return false;
|
||||
if (layerEnabled(obj)) return true;
|
||||
if (typeof obj.layer.validHere === 'function') {
|
||||
return obj.layer.validHere(context.map().extent(), context.map().zoom());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
function layerSupported(d) {
|
||||
return d.layer && d.layer.supported();
|
||||
@@ -41,6 +49,9 @@ export function uiSectionPhotoOverlays(context) {
|
||||
function layerEnabled(d) {
|
||||
return layerSupported(d) && d.layer.enabled();
|
||||
}
|
||||
function layerRendered(d) {
|
||||
return d.layer.rendered?.(context.map().zoom()) ?? true;
|
||||
}
|
||||
|
||||
var ul = selection
|
||||
.selectAll('.layer-list-photos')
|
||||
@@ -77,7 +88,13 @@ export function uiSectionPhotoOverlays(context) {
|
||||
else titleID = d.id.replace(/-/g, '_') + '.tooltip';
|
||||
d3_select(this)
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append(titleID))
|
||||
.title(() => {
|
||||
if (!layerRendered(d)) {
|
||||
return t.append('street_side.minzoom_tooltip');
|
||||
} else {
|
||||
return t.append(titleID);
|
||||
}
|
||||
})
|
||||
.placement('top')
|
||||
);
|
||||
});
|
||||
@@ -100,6 +117,7 @@ export function uiSectionPhotoOverlays(context) {
|
||||
.merge(liEnter)
|
||||
.classed('active', layerEnabled)
|
||||
.selectAll('input')
|
||||
.property('disabled', d => !layerRendered(d))
|
||||
.property('checked', layerEnabled);
|
||||
}
|
||||
|
||||
@@ -320,5 +338,13 @@ export function uiSectionPhotoOverlays(context) {
|
||||
context.layers().on('change.uiSectionPhotoOverlays', section.reRender);
|
||||
context.photos().on('change.uiSectionPhotoOverlays', section.reRender);
|
||||
|
||||
context.map()
|
||||
.on('move.background_list',
|
||||
_debounce(function() {
|
||||
// layers in-view may have changed due to map move
|
||||
window.requestIdleCallback(section.reRender);
|
||||
}, 1000)
|
||||
);
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
"clean": "shx rm -f dist/esbuild.json dist/*.js dist/*.map dist/*.css dist/img/*.svg",
|
||||
"dist": "run-p dist:**",
|
||||
"dist:mapillary": "shx mkdir -p dist/mapillary-js && shx cp -R node_modules/mapillary-js/dist/* dist/mapillary-js/",
|
||||
"dist:pannellum": "shx mkdir -p dist/pannellum-streetside && shx cp -R node_modules/pannellum/build/* dist/pannellum-streetside/",
|
||||
"dist:pannellum": "shx mkdir -p dist/pannellum && shx cp -R node_modules/pannellum/build/* dist/pannellum/",
|
||||
"dist:min": "node config/esbuild.config.min.mjs",
|
||||
"dist:svg:iD": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"iD-%s\" --symbol-sprite dist/img/iD-sprite.svg \"svg/iD-sprite/**/*.svg\"",
|
||||
"dist:svg:community": "svg-sprite --symbol --symbol-dest . --shape-id-generator \"community-%s\" --symbol-sprite dist/img/community-sprite.svg node_modules/osm-community-index/dist/img/*.svg",
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
describe('iD.serviceVegbilder', function() {
|
||||
const dimensions = [64, 64];
|
||||
const testImages = [{
|
||||
loc: [5.7, 58.90001],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.11.000000_EV00001_S001D1_m00001',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar1.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 1,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.11Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 58.90002],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.12.000000_EV00001_S001D1_m00002',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar2.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 2,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.12Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 59.90003],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.13.000000_EV00001_S002D1_m00003',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar3.jpg',
|
||||
road_reference: 'EV1 S2D1',
|
||||
metering: 3,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.13Z'),
|
||||
is_sphere: false
|
||||
}];
|
||||
const stacedImages = [{
|
||||
loc: [5.7, 58.9],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.11.000000_EV00001_S001D1_m00001',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar1.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 1,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.11Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 58.9],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.12.000000_EV00001_S001D1_m00002',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar2.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 2,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.12Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 58.9],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.13.000000_EV00001_S001D1_m00003',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar3.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 3,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.13Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 58.9],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.14.000000_EV00001_S001D1_m00004',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar4.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 4,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.14Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 58.9],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.15.000000_EV00001_S001D1_m00005',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar5.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 5,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.15Z'),
|
||||
is_sphere: false
|
||||
}, {
|
||||
loc: [5.7, 58.9],
|
||||
key: 'Vegbilder_2021.2021-01-01T11.11.16.000000_EV00001_S001D1_m00006',
|
||||
ca: 90,
|
||||
image_path: 'https://s3vegbilder.atlas.vegvesen.no/foo/bar6.jpg',
|
||||
road_reference: 'EV1 S1D1',
|
||||
metering: 6,
|
||||
lane_code: '1K',
|
||||
captured_at: new Date('2021-01-01T11.11.16Z'),
|
||||
is_sphere: false
|
||||
}];
|
||||
|
||||
let context, vegbilder;
|
||||
|
||||
function asFeature(images) {
|
||||
return images.map(image => ({
|
||||
minX: image.loc[0],
|
||||
minY: image.loc[1],
|
||||
maxX: image.loc[0],
|
||||
maxY: image.loc[1],
|
||||
data: image
|
||||
}));
|
||||
}
|
||||
|
||||
before(function() {
|
||||
iD.services.vegbilder = iD.serviceVegbilder;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
delete iD.services.vegbilder;
|
||||
});
|
||||
|
||||
beforeEach(async function() {
|
||||
context = iD.coreContext().assetPath('../dist/').init();
|
||||
// bbox maxX: 5.705423355102539 maxY: 58.900168239328906 minX: 5.699930191040039 minY: 58.8973307343531
|
||||
context.projection
|
||||
.scale(iD.geoZoomToScale(14))
|
||||
.translate([-66409, 853915])
|
||||
.clipExtent([[0,0], dimensions]);
|
||||
|
||||
vegbilder = iD.services.vegbilder;
|
||||
await vegbilder.reset();
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
describe('#init', function() {
|
||||
it('Initializes cache one time', function() {
|
||||
const cache = vegbilder.cache();
|
||||
expect(cache).to.have.property('wfslayers');
|
||||
expect(cache).to.have.property('rtree');
|
||||
expect(cache).to.have.property('image2sequence_map');
|
||||
|
||||
vegbilder.init();
|
||||
const cache2 = vegbilder.cache();
|
||||
expect(cache).to.equal(cache2);
|
||||
});
|
||||
|
||||
it('fetches available layers', function() {
|
||||
const availableLayers = vegbilder.cache().wfslayers;
|
||||
expect(availableLayers).to.have.key('vegbilder_1_0:Vegbilder_2020');
|
||||
expect(availableLayers).to.not.have.key('not_matched_layer:Vegbilder_2020');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#reset', function() {
|
||||
it('resets cache', async function() {
|
||||
vegbilder.cache().foo = 'bar';
|
||||
await vegbilder.reset();
|
||||
expect(vegbilder.cache()).to.not.have.property('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadImages', function () {
|
||||
it('fires loadedImages when images are loaded', function() {
|
||||
const respons = {
|
||||
'type': 'FeatureCollection',
|
||||
'features': [
|
||||
{
|
||||
'type': 'Feature',
|
||||
'id': 'Vegbilder_2021.2021-05-05T08.42.47.315227_EV00039_S100D1_m14966_KD1_m00319',
|
||||
'geometry': {'type': 'Point', 'coordinates': [5.686, 58.901]},
|
||||
'properties':{
|
||||
'BILDETYPE':'Planar','AAR':2021,'TIDSPUNKT':'2021-05-05T06:42:47Z','FYLKENUMMER':11,'VEGKATEGORI':'E','VEGSTATUS':'V',
|
||||
'VEGNUMMER':39,'STREKNING':100,'HP':null,'DELSTREKNING':1,'ANKERPUNKT':14966,'KRYSSDEL':1,'SIDEANLEGGSDEL':null,
|
||||
'METER':319.0,'FELTKODE':'1K','REFLINKID':null,'REFLINKPOSISJON':null,'RETNING':176.2,
|
||||
'URL':'https://s3vegbilder.atlas.vegvesen.no/foo/bar1.jpg'
|
||||
}
|
||||
}, {
|
||||
'type': 'Feature',
|
||||
'id': 'Vegbilder_2021.2021-05-05T08.42.47.627214_EV00039_S100D1_m14966_KD1_m00320',
|
||||
'geometry': {'type': 'Point', 'coordinates': [5.687, 58.902]},
|
||||
'properties': {
|
||||
'BILDETYPE':'Planar','AAR':2021,'TIDSPUNKT':'2021-05-05T06:42:47Z','FYLKENUMMER':11,'VEGKATEGORI':'E','VEGSTATUS':'V',
|
||||
'VEGNUMMER':39,'STREKNING':100,'HP':null,'DELSTREKNING':1,'ANKERPUNKT':14966,'KRYSSDEL':1,'SIDEANLEGGSDEL':null,
|
||||
'METER':320.0,'FELTKODE':'1K','REFLINKID':null,'REFLINKPOSISJON':null,'RETNING':178.5,
|
||||
'URL':'https://s3vegbilder.atlas.vegvesen.no/foo/bar2.jpg'
|
||||
}
|
||||
}, {
|
||||
'type': 'Feature',
|
||||
'id': 'Vegbilder_2021.2021-05-05T08.42.47.627214_EV00039_S100D1_m14966_KD1_m00321',
|
||||
'geometry': {'type':'Point','coordinates':[5.688, 58.903]},
|
||||
'properties': {
|
||||
'BILDETYPE':'Planar','AAR':2021,'TIDSPUNKT':'2021-05-05T06:42:47Z','FYLKENUMMER':11,'VEGKATEGORI':'E','VEGSTATUS':'V',
|
||||
'VEGNUMMER':39,'STREKNING':100,'HP':null,'DELSTREKNING':1,'ANKERPUNKT':14966,'KRYSSDEL':1,'SIDEANLEGGSDEL':null,
|
||||
'METER':321.0,'FELTKODE':'1K','REFLINKID':null,'REFLINKPOSISJON':null,'RETNING':178.5,
|
||||
'URL':'https://s3vegbilder.atlas.vegvesen.no/foo/bar3.jpg'
|
||||
}
|
||||
}
|
||||
],
|
||||
'totalFeatures': 3
|
||||
};
|
||||
|
||||
fetchMock.mock({
|
||||
url: 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows',
|
||||
query: {
|
||||
service: 'WFS',
|
||||
request: 'GetFeature'
|
||||
}
|
||||
}, respons);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
vegbilder.on('loadedImages', () => {
|
||||
expect(fetchMock.calls().length).to.eql(1);
|
||||
resolve();
|
||||
});
|
||||
|
||||
vegbilder.loadImages(context, 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not load images around null island', async function() {
|
||||
const respons = {
|
||||
'type': 'FeatureCollection',
|
||||
'features': [
|
||||
{
|
||||
'type': 'Feature',
|
||||
'id': 'Vegbilder_2021.2021-01-01T11.11.11.000000_EV00001_S001D1_m00001',
|
||||
'geometry': {'type': 'Point', 'coordinates': [0.0, 0.0]},
|
||||
'properties':{
|
||||
'BILDETYPE':'Planar','AAR':2021,'TIDSPUNKT':'2021-01-01T11.11.11Z','FYLKENUMMER':1,'VEGKATEGORI':'E','VEGSTATUS':'V',
|
||||
'VEGNUMMER':1,'STREKNING':1,'HP':null,'DELSTREKNING':1,'ANKERPUNKT':null,'KRYSSDEL':null,'SIDEANLEGGSDEL':null,
|
||||
'METER':1.0,'FELTKODE':'1K','REFLINKID':null,'REFLINKPOSISJON':null,'RETNING':null,
|
||||
'URL':'https://s3vegbilder.atlas.vegvesen.no/foo/bar.jpg'
|
||||
}
|
||||
}
|
||||
],
|
||||
'totalFeatures': 1
|
||||
};
|
||||
|
||||
fetchMock.mock({
|
||||
url: 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows',
|
||||
query: {
|
||||
service: 'WFS',
|
||||
request: 'GetFeature'
|
||||
}
|
||||
}, respons);
|
||||
|
||||
context.projection.translate([0, 0]);
|
||||
|
||||
const spy = sinon.spy();
|
||||
vegbilder.on('loadedImages', spy);
|
||||
vegbilder.loadImages(context, 0);
|
||||
|
||||
await new Promise((resolve) => { window.setTimeout(resolve, 200); });
|
||||
|
||||
expect(spy).to.have.been.not.called;
|
||||
expect(fetchMock.calls().length).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#images', function() {
|
||||
it('returns images in the visible map area', function() {
|
||||
const features = asFeature(testImages);
|
||||
|
||||
vegbilder.cache().rtree.load(features);
|
||||
const result = vegbilder.images(context.projection);
|
||||
|
||||
expect(result).to.deep.eql(testImages.slice(0, 2));
|
||||
});
|
||||
|
||||
it('limits results no more than 5 stacked images in one spot', function() {
|
||||
const features = asFeature(stacedImages);
|
||||
|
||||
vegbilder.cache().rtree.load(features);
|
||||
const result = vegbilder.images(context.projection);
|
||||
expect(features).to.have.length.of.at.least(6);
|
||||
expect(result).to.have.length.of.at.most(5);
|
||||
});
|
||||
});
|
||||
describe('#sequences', function() {
|
||||
it('returns sequence linestrings in the visible map area', function() {
|
||||
const features = asFeature(testImages);
|
||||
const cache = vegbilder.cache();
|
||||
|
||||
cache.rtree.load(features);
|
||||
|
||||
const sequence = {
|
||||
images: testImages,
|
||||
key: '1',
|
||||
geometry : {
|
||||
type : 'LineString',
|
||||
coordinates : testImages.map(image => image.loc)
|
||||
}};
|
||||
|
||||
for (const image of testImages) {
|
||||
cache.image2sequence_map.set(image.key, sequence);
|
||||
}
|
||||
|
||||
const result = vegbilder.sequences(context.projection);
|
||||
expect(result).to.deep.eql([{
|
||||
type: 'LineString',
|
||||
coordinates: [[5.7, 58.90001], [5.7, 58.90002], [5.7, 59.90003]],
|
||||
key: '1',
|
||||
images: testImages
|
||||
}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -137,5 +137,48 @@ const capabilities = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
fetchMock.sticky('https://www.openstreetmap.org/api/capabilities', capabilities, {sticky: true});
|
||||
fetchMock.sticky('http://www.openstreetmap.org/api/capabilities', capabilities, {sticky: true});
|
||||
|
||||
const vegbilderOwsCapabilities = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<wfs:WFS_Capabilities version="2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.opengis.net/wfs/2.0"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0"
|
||||
xmlns:ows="http://www.opengis.net/ows/1.1"
|
||||
xmlns:gml="http://www.opengis.net/gml/3.2"
|
||||
xmlns:fes="http://www.opengis.net/fes/2.0"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 https://www.vegvesen.no/kart/ogc/schemas/wfs/2.0/wfs.xsd"
|
||||
xmlns:xml="http://www.w3.org/XML/1998/namespace"
|
||||
xmlns:vegbilder_1_0="http://vegbilder_1_0">
|
||||
<ows:ServiceIdentification>
|
||||
<ows:Title>Mock OGC</ows:Title>
|
||||
<ows:ServiceType>WFS</ows:ServiceType>
|
||||
<ows:ServiceTypeVersion>2.0.0</ows:ServiceTypeVersion>
|
||||
</ows:ServiceIdentification>
|
||||
<FeatureTypeList>
|
||||
<FeatureType xmlns:vegbilder_1_0="http://vegbilder_1_0">
|
||||
<Name>vegbilder_1_0:Vegbilder_2020</Name>
|
||||
<Title>Vegbilder_2020</Title>
|
||||
<Abstract>Testlayer</Abstract>
|
||||
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
|
||||
<OtherCRS>urn:ogc:def:crs:EPSG::3857</OtherCRS>
|
||||
</FeatureType>
|
||||
<FeatureType xmlns:vegbilder_1_0="http://vegbilder_1_0">
|
||||
<Name>not_matched_layer:Vegbilder_2020</Name>
|
||||
<Title>Vegbilder_2020_4</Title>
|
||||
<Abstract>Not matched layer</Abstract>
|
||||
<DefaultCRS>urn:ogc:def:crs:EPSG::4326</DefaultCRS>
|
||||
<OtherCRS>urn:ogc:def:crs:EPSG::3857</OtherCRS>
|
||||
</FeatureType>
|
||||
</FeatureTypeList>
|
||||
<fes:Filter_Capabilities/>
|
||||
</wfs:WFS_Capabilities>`;
|
||||
|
||||
fetchMock.sticky({
|
||||
url: 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows',
|
||||
query: {
|
||||
service: 'WFS',
|
||||
request: 'GetCapabilities'
|
||||
}
|
||||
}, vegbilderOwsCapabilities, {sticky: true});
|
||||
fetchMock.config.fallbackToNetwork = true;
|
||||
fetchMock.config.overwriteRoutes = false;
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('iD.svgLayers', function () {
|
||||
it('creates default data layers', function () {
|
||||
container.call(iD.svgLayers(projection, context));
|
||||
var nodes = container.selectAll('svg .data-layer').nodes();
|
||||
expect(nodes.length).to.eql(16);
|
||||
expect(nodes.length).to.eql(17);
|
||||
expect(d3.select(nodes[0]).classed('osm')).to.be.true;
|
||||
expect(d3.select(nodes[1]).classed('notes')).to.be.true;
|
||||
expect(d3.select(nodes[2]).classed('data')).to.be.true;
|
||||
@@ -40,9 +40,10 @@ describe('iD.svgLayers', function () {
|
||||
expect(d3.select(nodes[10]).classed('mapillary-signs')).to.be.true;
|
||||
expect(d3.select(nodes[11]).classed('kartaview')).to.be.true;
|
||||
expect(d3.select(nodes[12]).classed('mapilio')).to.be.true;
|
||||
expect(d3.select(nodes[13]).classed('debug')).to.be.true;
|
||||
expect(d3.select(nodes[14]).classed('geolocate')).to.be.true;
|
||||
expect(d3.select(nodes[15]).classed('touch')).to.be.true;
|
||||
expect(d3.select(nodes[13]).classed('vegbilder')).to.be.true;
|
||||
expect(d3.select(nodes[14]).classed('debug')).to.be.true;
|
||||
expect(d3.select(nodes[15]).classed('geolocate')).to.be.true;
|
||||
expect(d3.select(nodes[16]).classed('touch')).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user