nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

commit d38a0a30d9eacd87703a2e96ddfa436d1d688e0f
parent d2d671ae55243f5302a6dd2a1cb820879590117c
Author: Sergej Orlov <wladimirych@gmail.com>
Date:   Fri,  6 May 2022 20:45:29 +0200

Panoramas: redesign events

Send event about changed view only when movement ends.

This helps to avoid ui freezes in Chrome when updating hash state.

Additional changes:
- wikimedia: use CloseButton mixin instead of own implementation
- wikimedia: remove redundand wrapper for `showPano`
- mapillary: store image ID instead of coordinates in hash state.
  Search by coordinates is slow.

Diffstat:
Msrc/lib/leaflet.control.panoramas/index.js | 83+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/lib/leaflet.control.panoramas/lib/common/index.js | 8+++++++-
Msrc/lib/leaflet.control.panoramas/lib/google/index.js | 27++++++++++++++++++++-------
Msrc/lib/leaflet.control.panoramas/lib/mapillary/index.js | 169++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/lib/leaflet.control.panoramas/lib/mapycz/index.js | 79+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/lib/leaflet.control.panoramas/lib/wikimedia/coverage-layer.js | 1-
Msrc/lib/leaflet.control.panoramas/lib/wikimedia/index.js | 26+++++++++-----------------
7 files changed, 214 insertions(+), 179 deletions(-)

diff --git a/src/lib/leaflet.control.panoramas/index.js b/src/lib/leaflet.control.panoramas/index.js @@ -4,6 +4,7 @@ import googleProvider from './lib/google'; import '~/lib/leaflet.hashState/leaflet.hashState'; import './style.css'; +import {Events} from './lib/common'; import '~/lib/controls-styles/controls-styles.css'; import {makeButtonWithBar} from '~/lib/leaflet.control.commons'; import mapillaryProvider from './lib/mapillary'; @@ -25,6 +26,22 @@ const PanoMarker = L.Marker.extend({ } ); L.Marker.prototype.initialize.call(this, [0, 0], {icon, interactive: false}); + this._postponeType = null; + this._postponeHeading = null; + }, + + onAdd: function(map) { + L.Marker.prototype.onAdd.call(this, map); + if (this._postponeType !== null) { + this.setType(this._postponeType); + } + if (this._postponeHeading !== null) { + this.setHeading(this._postponeHeading); + } + }, + + onRemove: function(map) { + L.Marker.prototype.onRemove.call(this, map); }, getIcon: function() { @@ -34,11 +51,19 @@ const PanoMarker = L.Marker.extend({ }, setHeading: function(angle) { + this._postponeHeading = angle; + if (!this._map) { + return; + } const markerIcon = this.getIcon(); markerIcon.style.transform = `rotate(${angle || 0}deg)`; }, setType: function(markerType) { + this._postponeType = markerType; + if (!this._map) { + return; + } const className = { slim: 'leaflet-panorama-marker-circle', normal: 'leaflet-panorama-marker-binocular' @@ -112,6 +137,7 @@ L.Control.Panoramas = L.Control.extend({ provider.container = L.DomUtil.create('div', 'panorama-container', this._panoramasContainer); } this.nearbyPoints = []; + this.marker = new PanoMarker(); }, loadSettings: function() { @@ -219,7 +245,7 @@ L.Control.Panoramas = L.Control.extend({ this.updateCoverageVisibility(); this._map.on('click', this.onMapClick, this); L.DomUtil.addClass(this._map._container, 'panoramas-control-active'); - this.notifyChanged(); + this.notifyChange(); }, disableControl: function() { @@ -232,7 +258,7 @@ L.Control.Panoramas = L.Control.extend({ this._map.off('click', this.onMapClick, this); this.hidePanoViewer(); L.DomUtil.removeClass(this._map._container, 'panoramas-control-active'); - this.notifyChanged(); + this.notifyChange(); }, updateCoverageVisibility: function() { @@ -252,7 +278,7 @@ L.Control.Panoramas = L.Control.extend({ } } } - this.notifyChanged(); + this.notifyChange(); }, showPanoramaContainer: function() { @@ -309,12 +335,15 @@ L.Control.Panoramas = L.Control.extend({ setTimeout(() => provider.viewer.showPano(data), 0); } provider.viewer.activate(); - this.notifyChanged(); + this.marker.setType(provider.mapMarkerType); + this.notifyChange(); }, setupViewerEvents: function(provider) { provider.viewer.on({ - change: this.onPanoramaChangeView.bind(this, provider), + [Events.ImageChange]: this.onViewerImageChange, + [Events.BearingChange]: this.onViewerBearingChange, + [Events.YawPitchZoomChangeEnd]: this.onViewerZoomYawPitchChangeEnd, closeclick: this.onPanoramaCloseClick }, this); }, @@ -324,39 +353,29 @@ L.Control.Panoramas = L.Control.extend({ this.hidePano(provider); } L.DomUtil.removeClass(this._panoramasContainer, 'enabled'); - this.hideMarker(); - this.notifyChanged(); + this._map.removeLayer(this.marker); + this.notifyChange(); }, - placeMarker: function(latlng, heading) { - if (!this.panoramaVisible()) { - return; - } - if (!this.marker) { - this.marker = new PanoMarker(); - } - this._map.addLayer(this.marker); - this.marker.setLatLng(latlng); - this.marker.setHeading(heading); + notifyChange: function() { + this.fire('change'); }, - hideMarker: function() { - if (this.marker) { - this._map.removeLayer(this.marker); + onViewerImageChange: function(e) { + if (!this._map.getBounds().pad(-0.05).contains(e.latlng)) { + this._map.panTo(e.latlng); } + this._map.addLayer(this.marker); + this.marker.setLatLng(e.latlng); + this.notifyChange(); }, - notifyChanged: function() { - this.fire('panoramachanged'); + + onViewerBearingChange: function(e) { + this.marker.setHeading(e.bearing); }, - onPanoramaChangeView: function(provider, e) { - if (!this._map.getBounds().pad(-0.05).contains(e.latlng)) { - this._map.panTo(e.latlng); - } - this.placeMarker(e.latlng, e.heading); - this.marker.setType(provider.mapMarkerType); - this.setupNearbyPoints(e.latlngs); - this.notifyChanged(); + onViewerZoomYawPitchChangeEnd: function() { + this.notifyChange(); }, onPanoramaCloseClick: function() { @@ -381,6 +400,8 @@ L.Control.Panoramas = L.Control.extend({ for (let {promise, provider} of promises) { let searchResult = await promise; if (searchResult.found) { + this._map.removeLayer(this.marker); + this.provider = provider; this.showPano(provider, searchResult.data); return; } @@ -448,7 +469,7 @@ L.Control.Panoramas = L.Control.extend({ L.Control.Panoramas.include(L.Mixin.HashState); L.Control.Panoramas.include({ - stateChangeEvents: ['panoramachanged'], + stateChangeEvents: ['change'], serializeState: function() { let state = null; diff --git a/src/lib/leaflet.control.panoramas/lib/common/index.js b/src/lib/leaflet.control.panoramas/lib/common/index.js @@ -28,4 +28,10 @@ const DateLabelMixin = { } }; -export {CloseButtonMixin, DateLabelMixin}; +const Events = { + ImageChange: 'ImageChange', + BearingChange: 'BearingChange', + YawPitchZoomChangeEnd: 'YawPitchZoomChangeEnd', +}; + +export {CloseButtonMixin, DateLabelMixin, Events}; diff --git a/src/lib/leaflet.control.panoramas/lib/google/index.js b/src/lib/leaflet.control.panoramas/lib/google/index.js @@ -1,5 +1,6 @@ import L from 'leaflet'; import getGoogle from '~/lib/googleMapsApi'; +import {Events} from "../common"; function getCoverageLayer(options) { return L.tileLayer( @@ -41,26 +42,38 @@ const Viewer = L.Evented.extend({ motionTrackingControl: false, } ); - panorama.addListener('position_changed', () => this.onPanoramaChangeView()); - panorama.addListener('pov_changed', () => this.onPanoramaChangeView()); + panorama.addListener('position_changed', () => this.onPanoramaPositionChanged()); + panorama.addListener('pov_changed', () => this.onPanoramaPovChanged()); panorama.addListener('closeclick', () => this.onCloseClick()); - this.invalidateSize = L.Util.throttle(this._invalidateSize, 50, this); + this._yawPitchZoomChangeTimer = null; }, showPano: function(data) { this.panorama.setPosition(data.location.latLng); this.panorama.setZoom(1); + const heading = this.panorama.getPov().heading; + this.panorama.setPov({heading, pitch: 0}); }, - onPanoramaChangeView: function() { + onPanoramaPositionChanged: function() { if (!this._active) { return; } - let pos = this.panorama.getPosition(); - pos = L.latLng(pos.lat(), pos.lng()); + const pos = this.panorama.getPosition(); + this.fire(Events.ImageChange, {latlng: L.latLng(pos.lat(), pos.lng())}); + }, + + onPanoramaPovChanged: function() { const pov = this.panorama.getPov(); - this.fire('change', {latlng: pos, heading: pov.heading, zoom: pov.zoom, pitch: pov.pitch}); + this.fire(Events.BearingChange, {bearing: pov.heading}); + if (this._yawPitchZoomChangeTimer !== null) { + clearTimeout(this._yawPitchZoomChangeTimer); + this._yawPitchZoomChangeTimer = null; + } + this._yawPitchZoomChangeTimer = setTimeout(() => { + this.fire(Events.YawPitchZoomChangeEnd); + }, 120); }, onCloseClick: function() { diff --git a/src/lib/leaflet.control.panoramas/lib/mapillary/index.js b/src/lib/leaflet.control.panoramas/lib/mapillary/index.js @@ -2,7 +2,7 @@ import L from 'leaflet'; import {fetch} from '~/lib/xhr-promise'; import config from '~/config'; import './style.css'; -import {CloseButtonMixin, DateLabelMixin} from '../common'; +import {CloseButtonMixin, Events} from '../common'; function getCoverageLayer(options) { return L.tileLayer(config.mapillaryRasterTilesUrl, L.extend({ @@ -75,126 +75,119 @@ const Viewer = L.Evented.extend({ initialize: function(mapillary, container) { const id = `container-${L.stamp(container)}`; container.id = id; - const viewer = this.viewer = new mapillary.Viewer( + this.viewer = new mapillary.Viewer( { container: id, accessToken: config.mapillary4, - component: {cover: false, bearing: false, cache: true, zoom: true}, + component: {cover: false, bearing: false, cache: true, zoom: false, trackResize: true}, }); - viewer.on('image', this.onNodeChanged.bind(this)); this.createCloseButton(container); - this._bearing = 0; - this._zoom = 0; - this._center = [0, 0]; this.invalidateSize = L.Util.throttle(this._invalidateSize, 100, this); + this._updateHandler = null; + this._currentImage = null; + this._zoom = null; + this._centerX = null; + this._centerY = null; + this._bearing = null; + this._yawPitchZoomChangeTimer = null; }, - showPano: function(data) { - this.deactivate(); - this.activate(); - this.viewer.moveTo(data).then(() => { - this.viewer.setZoom(0); - this.updateZoomAndCenter(); - }); + showPano: function(imageId) { + this.viewer.moveTo(imageId) + .then(() => { + if (!this._updateHandler) { + this._updateHandler = setInterval(this.watchMapillaryStateChange.bind(this), 50); + } + }) + .catch(() => { + // ignore error + }); }, - onNodeChanged: function(event) { - if (this.currentImage && (event.image.id === this.currentImage.id)) { - return; - } - this.currentImage = event.image; - this.fireChangeEvent(); + watchMapillaryStateChange: function() { + Promise.all([ + this.viewer.getImage(), + this.viewer.getCenter(), + this.viewer.getZoom(), + this.viewer.getBearing(), + ]).then(([image, center, zoom, bearing]) => { + if (this._currentImage?.id !== image.id) { + this._currentImage = image; + const lngLat = image.originalLngLat; + this.fire(Events.ImageChange, {latlng: L.latLng(lngLat.lat, lngLat.lng)}); + } + const [centerX, centerY] = center; + if (centerX !== this._centerX || centerY !== this._centerY || zoom !== this._zoom) { + this._zoom = zoom; + this._centerX = centerX; + this._centerY = centerY; + if (this._yawPitchZoomChangeTimer !== null) { + clearTimeout(this._yawPitchZoomChangeTimer); + this._yawPitchZoomChangeTimer = null; + } + this._yawPitchZoomChangeTimer = setTimeout(() => { + this.fire(Events.YawPitchZoomChangeEnd); + }, 120); + } + bearing -= this.getBearingCorrection(); + if (bearing !== this._bearing) { + this._bearing = bearing; + this.fire(Events.BearingChange, {bearing: bearing}); + } + }).catch(() => { + // ignore error + }); }, getBearingCorrection: function() { - if (this.currentImage && 'computedCompassAngle' in this.currentImage) { - return (this.currentImage.computedCompassAngle - this.currentImage.originalCompassAngle); + if (this._currentImage && 'computedCompassAngle' in this._currentImage) { + return (this._currentImage.computedCompassAngle - this._currentImage.originalCompassAngle); } return 0; }, - fireChangeEvent: function() { - if (this.currentImage) { - const lnglat = this.currentImage.originalLngLat; - this.fire('change', { - latlng: L.latLng(lnglat.lat, lnglat.lng), - heading: this._bearing, - pitch: this._pitch, - zoom: this._zoom - } - ); - } - }, - deactivate: function() { - this.viewer.activateCover(); - if (this._updateHandler) { - clearInterval(this._updateHandler); - this._updateHandler = null; - } - }, - - updateZoomAndCenter: function() { - this.viewer.getZoom().then((zoom) => { - if (zoom !== this._zoom) { - this._zoom = zoom; - this.fireChangeEvent(); - } - }); - this.viewer.getCenter().then((center) => { - if (center[0] < 0 || center[0] > 1 || center[1] < 0 || center[1] > 1) { - center = [0.5, 0.5]; - } - if (center[0] !== this._center[0] || center[1] !== this._center[1]) { - this._center = center; - this.fireChangeEvent(); - } - }); - this.viewer.getBearing().then((bearing) => { - bearing -= this.getBearingCorrection(); - if (this._bearing !== bearing) { - this._bearing = bearing; - this.fireChangeEvent(); - } - }); + this._currentImage = null; + this._bearing = null; + this._zoom = null; + this._centerX = null; + this._centerY = null; + clearInterval(this._updateHandler); + this._updateHandler = null; + this.viewer.setCenter([0.5, 0.5]); + this.viewer.setZoom(0); }, activate: function() { this.viewer.resize(); - this.viewer.deactivateCover(); - if (!this._updateHandler) { - this._updateHandler = setInterval(() => this.updateZoomAndCenter(), 200); - } }, getState: function() { - if (!this.currentImage) { + if ( + this._currentImage === null || + this._zoom === null || + this._centerX === null || + this._centerY === null + ) { return []; } - const {lat, lng} = this.currentImage.originalLngLat; return [ - lat.toFixed(6), - lng.toFixed(6), - this._center[0].toFixed(4), - this._center[1].toFixed(4), + this._currentImage.id, + this._centerX.toFixed(6), + this._centerY.toFixed(6), this._zoom.toFixed(2) ]; }, setState: function(state) { - const lat = parseFloat(state[0]); - const lng = parseFloat(state[1]); - const center0 = parseFloat(state[2]); - const center1 = parseFloat(state[3]); - const zoom = parseFloat(state[4]); - if (!isNaN(lat) && !isNaN(lng) && !isNaN(center0) && !isNaN(center1) && !isNaN(zoom)) { - getPanoramaAtPos(L.latLng(lat, lng), 0.01).then((res) => { - if (res.found) { - this.viewer.moveTo(res.data); - this.viewer.setCenter([center0, center1]); - this.viewer.setZoom(zoom); - } - }); + const imageId = state[0]; + const center0 = parseFloat(state[1]); + const center1 = parseFloat(state[2]); + const zoom = parseFloat(state[3]); + if (imageId && !isNaN(center0) && !isNaN(center1) && !isNaN(zoom)) { + this.showPano(imageId); + this.viewer.setCenter([center0, center1]); + this.viewer.setZoom(zoom); return true; } return false; diff --git a/src/lib/leaflet.control.panoramas/lib/mapycz/index.js b/src/lib/leaflet.control.panoramas/lib/mapycz/index.js @@ -1,6 +1,6 @@ import L from 'leaflet'; import {getSMap} from './apiLoader'; -import {CloseButtonMixin, DateLabelMixin} from '../common'; +import {CloseButtonMixin, DateLabelMixin, Events} from '../common'; function getCoverageLayer(options) { return L.tileLayer('https://mapserver.mapy.cz/panorama_hybrid-m/{z}-{x}-{y}', options); @@ -36,30 +36,36 @@ const Viewer = L.Evented.extend({ this.createCloseButton(container); window.addEventListener('resize', this.resize.bind(this)); this.invalidateSize = L.Util.throttle(this._invalidateSize, 100, this); + this._updateHandler = null; + this._placeId = null; + this._yaw = null; + this._pitch = null; + this._fov = null; + this._yawPitchZoomChangeTimer = null; }, - showPano: function(data) { - const yaw = this.panorama.getCamera().yaw; - this.panorama.show(data, {yaw}); - this.panorama.setCamera({fov: 1.256637061}); - this.updatePositionAndView(); - this.updateDateLabel(); + showPano: function(place, yaw = null, pitch = 0, fov = 1.256637061) { + if (yaw === null) { + yaw = this.panorama.getCamera().yaw; + } + this.panorama.show(place, {yaw}); + this.panorama.setCamera({fov, pitch}); + if (!this._updateHandler) { + this._updateHandler = setInterval(this.watchMapyStateChange.bind(this), 50); + } }, activate: function() { - this._active = true; this.resize(); - if (!this._updateHandler) { - this._updateHandler = setInterval(() => this.updatePositionAndView(), 200); - } }, deactivate: function() { - this._active = false; - if (this._updateHandler) { - clearInterval(this._updateHandler); - this._updateHandler = null; - } + this._placeId = null; + this._yaw = null; + this._pitch = null; + this._fov = null; + clearInterval(this._updateHandler); + this._updateHandler = null; }, getState: function() { @@ -85,11 +91,9 @@ const Viewer = L.Evented.extend({ const pitch = parseFloat(state[3]); const fov = parseFloat(state[4]); if (!isNaN(lat) && !isNaN(lng) && !isNaN(yaw) && !isNaN(pitch) && !isNaN(fov)) { - getPanoramaAtPos({lat, lng}, 0).then(({data, found}) => { + getPanoramaAtPos({lat, lng}, 0).then(({data: place, found}) => { if (found) { - this.panorama.show(data, {yaw, pitch, fov}); - this.panorama.setCamera({yaw, pitch, fov}); - this.updateDateLabel(); + this.showPano(place, yaw, pitch, fov); } }); return true; @@ -101,26 +105,33 @@ const Viewer = L.Evented.extend({ this.panorama.syncPort(); }, - updatePositionAndView: function() { + watchMapyStateChange: function() { const place = this.panorama.getPlace(); if (!place) { return; } - const oldPlaceId = this._placeId; - const oldHeading = this._heading; - this._placeId = place.getId(); - this._heading = this.panorama.getCamera().yaw; - const placeIdChanged = this._placeId !== oldPlaceId; - const headingChanged = this._heading !== oldHeading; - if (placeIdChanged) { + const placeId = place.getId(); + if (this._placeId !== placeId) { + this._placeId = placeId; + const coords = place.getCoords().toWGS84(); this.updateDateLabel(); + this.fire(Events.ImageChange, {latlng: L.latLng(coords[1], coords[0])}); } - if (placeIdChanged || headingChanged) { - const coords = place.getCoords().toWGS84(); - this.fire('change', { - latlng: L.latLng(coords[1], coords[0]), - heading: (this._heading * 180) / Math.PI, - }); + const camera = this.panorama.getCamera(); + if (this._yaw !== camera.yaw || this._pitch !== camera.pitch || this._fov !== camera.fov) { + if (this._yaw !== camera.yaw) { + this.fire(Events.BearingChange, {bearing: (this._yaw * 180) / Math.PI}); + } + this._yaw = camera.yaw; + this._pitch = camera.pitch; + this._fov = camera.fov; + if (this._yawPitchZoomChangeTimer !== null) { + clearTimeout(this._yawPitchZoomChangeTimer); + this._yawPitchZoomChangeTimer = null; + } + this._yawPitchZoomChangeTimer = setTimeout(() => { + this.fire(Events.YawPitchZoomChangeEnd); + }, 120); } }, diff --git a/src/lib/leaflet.control.panoramas/lib/wikimedia/coverage-layer.js b/src/lib/leaflet.control.panoramas/lib/wikimedia/coverage-layer.js @@ -106,7 +106,6 @@ class WikimediaLoader extends TiledDataLoader { const WikimediaVectorCoverage = L.GridLayer.extend({ options: { tileSize: 256, - // updateWhenIdle: true, color: '#ff00ff', }, diff --git a/src/lib/leaflet.control.panoramas/lib/wikimedia/index.js b/src/lib/leaflet.control.panoramas/lib/wikimedia/index.js @@ -4,6 +4,7 @@ import {fetch} from '~/lib/xhr-promise'; import './style.css'; import '../common/style.css'; import config from '~/config'; +import {CloseButtonMixin, Events} from "../common"; function getCoverageLayer(options) { const url = config.wikimediaCommonsCoverageUrl; @@ -165,8 +166,10 @@ function formatDateTime(dateStr) { } const Viewer = L.Evented.extend({ + includes: [CloseButtonMixin], initialize: function(container) { container = L.DomUtil.create('div', 'wikimedia-viewer-container', container); + this.createCloseButton(container); const mapContainer = this.mapContainer = L.DomUtil.create('div', 'wikimedia-viewer-map-container', container); this.pageButtonContainer = L.DomUtil.create('div', 'wikimedia-viewer-page-buttons-container', container); @@ -178,11 +181,9 @@ const Viewer = L.Evented.extend({ zoomSnap: 0, }); - this.map.on('zoomend', this.notifyChange, this); - this.map.on('moveend', this.notifyChange, this); + this.map.on('zoomend moovend', () => this.fire(Events.YawPitchZoomChangeEnd)); this.infoLabel = L.DomUtil.create('div', 'wikimedia-viewer-info-overlay', mapContainer); - this.closeButton = L.DomUtil.create('div', 'photo-viewer-button-close', container); this.prevPhotoButton = L.DomUtil.create('div', 'wikimedia-viewer-button-prev', mapContainer); this.nextPhotoButton = L.DomUtil.create('div', 'wikimedia-viewer-button-next', mapContainer); L.DomEvent.on(this.prevPhotoButton, 'click', () => { @@ -191,7 +192,6 @@ const Viewer = L.Evented.extend({ L.DomEvent.on(this.nextPhotoButton, 'click', () => { this.switchPhoto(this._imageIdx + 1); }); - L.DomEvent.on(this.closeButton, 'click', this.onCloseClick, this); }, setupPageButtons: function(count) { @@ -273,13 +273,13 @@ const Viewer = L.Evented.extend({ for (let [i, button] of this._buttons.entries()) { ((i === imageIdx) ? L.DomUtil.addClass : L.DomUtil.removeClass)(button, 'active'); } - this.notifyChange(); + this.notifyImageChange(); }, - notifyChange: function() { + notifyImageChange: function() { if (this.images && this._active) { const image = this.images[this._imageIdx]; - this.fire('change', { + this.fire(Events.ImageChange, { latlng: L.latLng(image.lat, image.lng), latlngs: this.images.map((image) => L.latLng(image.lat, image.lng)) } @@ -287,20 +287,12 @@ const Viewer = L.Evented.extend({ } }, - _showPano: function(images, imageIdx = 0, imagePos = null) { + showPano: function(images, imageIdx = 0, imagePos = null) { this.images = images; this.setupPageButtons(images.length); this.switchPhoto(imageIdx, imagePos); }, - showPano: function(images) { - this._showPano(images); - }, - - onCloseClick: function() { - this.fire('closeclick'); - }, - activate: function() { this._active = true; }, @@ -329,7 +321,7 @@ const Viewer = L.Evented.extend({ } } if (imageIdx > -1) { - this._showPano(resp.data, imageIdx, {center: L.latLng(y, x), zoom}); + this.showPano(resp.data, imageIdx, {center: L.latLng(y, x), zoom}); } }); return true;