index.js (7445B)
1 import L from 'leaflet'; 2 import {fetch} from '~/lib/xhr-promise'; 3 import config from '~/config'; 4 import './style.css'; 5 import {CloseButtonMixin, Events} from '../common'; 6 7 function getCoverageLayer(options) { 8 return L.tileLayer(config.mapillaryRasterTilesUrl, L.extend({ 9 tileSize: 1024, 10 zoomOffset: -2, 11 minNativeZoom: 0, 12 }, options)); 13 } 14 15 async function getMapillary() { 16 const [mapillary] = await Promise.all([ 17 import( 18 /* webpackChunkName: "mapillary" */ 19 'mapillary-js' 20 ), 21 import( 22 /* webpackChunkName: "mapillary" */ 23 'mapillary-js/dist/mapillary.css' 24 ), 25 ]); 26 return mapillary; 27 } 28 async function getPanoramaAtPos(latlng, searchRadiusMeters) { 29 function radiusToBbox(latlng, radiusInMeters) { 30 const center = L.CRS.EPSG3857.project(latlng); 31 const metersPerMapUnit = L.CRS.EPSG3857.unproject(L.point(center.x, center.y + 1)).distanceTo(latlng); 32 const radiusInMapUnits = radiusInMeters / metersPerMapUnit; 33 return L.latLngBounds( 34 L.CRS.EPSG3857.unproject(L.point(center.x - radiusInMapUnits, center.y - radiusInMapUnits)), 35 L.CRS.EPSG3857.unproject(L.point(center.x + radiusInMapUnits, center.y + radiusInMapUnits)) 36 ); 37 } 38 39 function isCloser(target, a, b) { 40 const d1 = target.distanceTo(a); 41 const d2 = target.distanceTo(b); 42 if (d1 < d2) { 43 return -1; 44 } else if (d1 === d2) { 45 return 0; 46 } 47 return 1; 48 } 49 50 const searchBbox = radiusToBbox(latlng, searchRadiusMeters); 51 const precision = 6; 52 const searchBboxStr = [ 53 searchBbox.getWest().toFixed(precision), 54 searchBbox.getSouth().toFixed(precision), 55 searchBbox.getEast().toFixed(precision), 56 searchBbox.getNorth().toFixed(precision), 57 ].join(','); 58 const url = `https://graph.mapillary.com/images?access_token=${config.mapillary4}&bbox=${searchBboxStr}&limit=100`; 59 const resp = await fetch(url, {responseType: 'json', timeout: 20000}); 60 if (resp.responseJSON.data.length) { 61 const points = resp.responseJSON.data.map((it) => ({ 62 id: it.id, 63 lat: it.geometry.coordinates[1], 64 lng: it.geometry.coordinates[0], 65 })); 66 points.sort((p1, p2) => isCloser(latlng, p1, p2)); 67 return {found: true, data: points[0].id}; 68 } 69 return {found: false}; 70 } 71 72 const Viewer = L.Evented.extend({ 73 includes: [CloseButtonMixin], 74 75 initialize: function(mapillary, container) { 76 const id = `container-${L.stamp(container)}`; 77 container.id = id; 78 this.viewer = new mapillary.Viewer( 79 { 80 container: id, 81 accessToken: config.mapillary4, 82 component: {cover: false, bearing: false, cache: true, zoom: false, trackResize: true}, 83 }); 84 this.createCloseButton(container); 85 this.invalidateSize = L.Util.throttle(this._invalidateSize, 100, this); 86 this._updateHandler = null; 87 this._currentImage = null; 88 this._zoom = null; 89 this._centerX = null; 90 this._centerY = null; 91 this._bearing = null; 92 this._yawPitchZoomChangeTimer = null; 93 }, 94 95 showPano: function(imageId) { 96 this.viewer.moveTo(imageId) 97 .then(() => { 98 if (!this._updateHandler) { 99 this._updateHandler = setInterval(this.watchMapillaryStateChange.bind(this), 50); 100 } 101 }) 102 .catch(() => { 103 // ignore error 104 }); 105 }, 106 107 watchMapillaryStateChange: function() { 108 Promise.all([ 109 this.viewer.getImage(), 110 this.viewer.getCenter(), 111 this.viewer.getZoom(), 112 this.viewer.getBearing(), 113 ]).then(([image, center, zoom, bearing]) => { 114 if (this._currentImage?.id !== image.id) { 115 this._currentImage = image; 116 const lngLat = image.originalLngLat; 117 this.fire(Events.ImageChange, {latlng: L.latLng(lngLat.lat, lngLat.lng)}); 118 } 119 const [centerX, centerY] = center; 120 if (centerX !== this._centerX || centerY !== this._centerY || zoom !== this._zoom) { 121 this._zoom = zoom; 122 this._centerX = centerX; 123 this._centerY = centerY; 124 if (this._yawPitchZoomChangeTimer !== null) { 125 clearTimeout(this._yawPitchZoomChangeTimer); 126 this._yawPitchZoomChangeTimer = null; 127 } 128 this._yawPitchZoomChangeTimer = setTimeout(() => { 129 this.fire(Events.YawPitchZoomChangeEnd); 130 }, 120); 131 } 132 bearing -= this.getBearingCorrection(); 133 if (bearing !== this._bearing) { 134 this._bearing = bearing; 135 this.fire(Events.BearingChange, {bearing: bearing}); 136 } 137 }).catch(() => { 138 // ignore error 139 }); 140 }, 141 142 getBearingCorrection: function() { 143 if (this._currentImage && 'computedCompassAngle' in this._currentImage) { 144 return (this._currentImage.computedCompassAngle - this._currentImage.originalCompassAngle); 145 } 146 return 0; 147 }, 148 149 deactivate: function() { 150 this._currentImage = null; 151 this._bearing = null; 152 this._zoom = null; 153 this._centerX = null; 154 this._centerY = null; 155 clearInterval(this._updateHandler); 156 this._updateHandler = null; 157 this.viewer.setCenter([0.5, 0.5]); 158 this.viewer.setZoom(0); 159 }, 160 161 activate: function() { 162 this.viewer.resize(); 163 }, 164 165 getState: function() { 166 if ( 167 this._currentImage === null || 168 this._zoom === null || 169 this._centerX === null || 170 this._centerY === null 171 ) { 172 return []; 173 } 174 return [ 175 this._currentImage.id, 176 this._centerX.toFixed(6), 177 this._centerY.toFixed(6), 178 this._zoom.toFixed(2) 179 ]; 180 }, 181 182 setState: function(state) { 183 const imageId = state[0]; 184 const center0 = parseFloat(state[1]); 185 const center1 = parseFloat(state[2]); 186 const zoom = parseFloat(state[3]); 187 if (imageId && !isNaN(center0) && !isNaN(center1) && !isNaN(zoom)) { 188 this.showPano(imageId); 189 this.viewer.setCenter([center0, center1]); 190 this.viewer.setZoom(zoom); 191 return true; 192 } 193 return false; 194 }, 195 196 _invalidateSize: function() { 197 this.viewer.resize(); 198 } 199 } 200 ); 201 202 async function getViewer(container) { 203 const mapillary = await getMapillary(); 204 return new Viewer(mapillary, container); 205 } 206 207 const mapillaryProvider = {getCoverageLayer, getPanoramaAtPos, getViewer}; 208 export default mapillaryProvider;