|
2 | 2 | // SPDX-License-Identifier: MIT
|
3 | 3 | // Copyright (c) vis.gl contributors
|
4 | 4 |
|
| 5 | +import maplibregl from 'maplibre-gl'; |
5 | 6 | import {Deck} from '@deck.gl/core';
|
6 |
| -import {PathLayer} from '@deck.gl/layers'; |
7 |
| -import {Tile3DLayer, TripsLayer} from '@deck.gl/geo-layers'; |
8 |
| -import {colorContinuous, fetchMap} from '@deck.gl/carto'; |
9 |
| -import {MapboxOverlay} from '@deck.gl/mapbox'; |
10 |
| -import EdgedPathLayer, {ADDITIVE_BLEND_PARAMETERS} from './EdgedPathLayer'; |
| 7 | +import {fetchMap} from '@deck.gl/carto'; |
11 | 8 |
|
12 |
| -const cartoMapId = '72d126eb-c77e-4786-bd16-4acceeae72ba'; |
13 |
| -const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line |
14 |
| - |
15 |
| -function deg2rad(deg) { |
16 |
| - return deg * (Math.PI / 180); |
17 |
| -} |
18 |
| - |
19 |
| -function distanceBetweenPoints([lon1, lat1, lon2, lat2]) { |
20 |
| - const R = 6371000; // Radius of the earth in m |
21 |
| - const dLat = deg2rad(lat2 - lat1); |
22 |
| - const dLon = deg2rad(lon2 - lon1); |
23 |
| - const a = |
24 |
| - Math.sin(dLat / 2) * Math.sin(dLat / 2) + |
25 |
| - Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); |
26 |
| - return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
27 |
| -} |
28 |
| - |
29 |
| -function addAltitude(geometry) { |
30 |
| - // Convert to 3D data |
31 |
| - const {featureIds, numericProps, positions, properties} = geometry; |
32 |
| - const n = featureIds.value.length; |
33 |
| - |
34 |
| - // Need to get the total length of each line |
35 |
| - const distanceAlong = new Float32Array(n); |
36 |
| - const segmentLengths = {}; |
37 |
| - for (let i = 0; i < n; i++) { |
38 |
| - const delta = distanceBetweenPoints(positions.value.subarray(2 * i - 2, 2 * i + 2)); |
39 |
| - distanceAlong[i] = delta + distanceAlong[i - 1]; |
40 |
| - const featureId = featureIds.value[i]; |
41 |
| - if (!(featureId in segmentLengths)) { |
42 |
| - segmentLengths[featureId] = 0; |
43 |
| - distanceAlong[i] = 0; |
44 |
| - } |
45 |
| - segmentLengths[featureId] = distanceAlong[i]; |
46 |
| - } |
47 |
| - |
48 |
| - // Normalize |
49 |
| - for (let i = 0; i < distanceAlong.length; i++) { |
50 |
| - const featureId = featureIds.value[i]; |
51 |
| - const segmentLength = segmentLengths[featureId]; |
52 |
| - distanceAlong[i] = distanceAlong[i] / segmentLength; |
53 |
| - } |
54 |
| - |
55 |
| - // Parse JSON |
56 |
| - properties.forEach(property => { |
57 |
| - property.ticks = JSON.parse(property.ticks)[0]; |
58 |
| - }); |
59 |
| - |
60 |
| - // Vertex colors |
61 |
| - const getAltitudeColor = colorContinuous({ |
62 |
| - attr: d => d.altitude, |
63 |
| - domain: [1000, 1500, 2000, 2500, 3000, 3500, 4000], |
64 |
| - colors: 'TealRose' |
65 |
| - }); |
66 |
| - const getSpeedColor = colorContinuous({ |
67 |
| - attr: d => d.altitude, |
68 |
| - domain: [5, 7, 9, 11, 13, 15], |
69 |
| - colors: 'OrYel' |
70 |
| - }); |
71 |
| - geometry.attributes = { |
72 |
| - ...geometry.attributes, |
73 |
| - getColor: {value: new Uint8Array(4 * n), size: 4, normalized: true}, |
74 |
| - getTimestamps: {value: new Float32Array(n), size: 1} |
75 |
| - }; |
76 |
| - |
77 |
| - function interpolatePoints(f, point, nextPoint) { |
78 |
| - const propertyNames = Object.keys(point); |
79 |
| - let interpolatedProperties = {}; |
80 |
| - if (!nextPoint) { |
81 |
| - interpolatedProperties = point; |
82 |
| - } else { |
83 |
| - const g = (f - point.f) / (nextPoint.f - point.f); |
84 |
| - for (const propertyName of propertyNames) { |
85 |
| - const value = point[propertyName]; |
86 |
| - const nextValue = nextPoint[propertyName]; |
87 |
| - const interpolatedValue = value * (1 - g) + nextValue * g; |
88 |
| - interpolatedProperties[propertyName] = interpolatedValue; |
89 |
| - } |
90 |
| - } |
91 |
| - |
92 |
| - return interpolatedProperties; |
93 |
| - } |
94 |
| - |
95 |
| - function interpolateProperties(vertexId) { |
96 |
| - const f = distanceAlong[vertexId]; |
97 |
| - const featureId = featureIds.value[vertexId]; |
98 |
| - const points = properties[featureId].ticks; |
99 |
| - let point = points[0]; |
100 |
| - let p = 0; |
101 |
| - |
102 |
| - // TODO more efficent search? |
103 |
| - while (points[p]?.f <= f) { |
104 |
| - point = points[p++]; |
105 |
| - } |
106 |
| - const nextPoint = points[p]; |
107 |
| - const interpolatedProperties = interpolatePoints(f, point, nextPoint); |
108 |
| - |
109 |
| - // DEBUG: Sanity |
110 |
| - // const seconds_min = numericProps.seconds_min[vertexId]; |
111 |
| - // const seconds_max = numericProps.seconds_max[vertexId]; |
112 |
| - // const {seconds} = point; |
113 |
| - // if (seconds_min > seconds || seconds_max < seconds) { |
114 |
| - // debugger; |
115 |
| - // } |
116 |
| - |
117 |
| - if (Math.abs(point.f - f) > 0.1) debugger; |
118 |
| - |
119 |
| - return interpolatedProperties; |
120 |
| - } |
121 |
| - |
122 |
| - // Interpolate properties to match vertex data |
123 |
| - let interpolatedProperties = []; |
124 |
| - for (let i = 0; i < n; i++) { |
125 |
| - interpolatedProperties[i] = interpolateProperties(i); |
126 |
| - } |
127 |
| - |
128 |
| - // Collect segments |
129 |
| - const chunks = []; |
130 |
| - const segmentCount = geometry.pathIndices.value.length - 1; |
131 |
| - for (let c = 0; c < segmentCount; c++) { |
132 |
| - const startIndex = geometry.pathIndices.value[c]; |
133 |
| - // Remove last vertex to avoid double vertices in line |
134 |
| - // TODO this is wrong, we can have two disjoint segments in a single tile |
135 |
| - // for example when the line comes back later |
136 |
| - let endIndex = geometry.pathIndices.value[c + 1]; |
137 |
| - const startT = interpolatedProperties[startIndex].t; |
138 |
| - const endT = interpolatedProperties[endIndex - 1].t; // TODO is this correct? |
139 |
| - const reverse = startT > endT; |
140 |
| - if (reverse) continue; // Skip reversed segments for now, no idea why they are there?? |
141 |
| - |
142 |
| - const {value, size} = geometry.positions; |
143 |
| - const TypedArray = value.constructor; |
144 |
| - const range = []; |
145 |
| - |
146 |
| - let segment = value.subarray(startIndex * size, endIndex * size); |
147 |
| - let segmentProperties = interpolatedProperties.slice(startIndex, endIndex); |
148 |
| - if (reverse) { |
149 |
| - const segmentReversed = new TypedArray(segment.length); |
150 |
| - for (let v = 0; v < segment.length / size; v++) { |
151 |
| - segmentReversed.set( |
152 |
| - segment.subarray(v * size, (v + 1) * size), |
153 |
| - segment.length - (v + 1) * size |
154 |
| - ); |
155 |
| - } |
156 |
| - segment = segmentReversed; |
157 |
| - segmentProperties.reverse(); |
158 |
| - } |
159 |
| - chunks.push({ |
160 |
| - segment, |
161 |
| - segmentProperties, |
162 |
| - timestamp: startT, |
163 |
| - range: [startIndex, endIndex], |
164 |
| - reverse |
165 |
| - }); |
166 |
| - } |
167 |
| - chunks.sort(({timestamp: a}, {timestamp: b}) => a - b); |
168 |
| - |
169 |
| - //chunks[chunks.length - 1].range[1] += 1; |
170 |
| - if (chunks[chunks.length - 1].reverse) debugger; // Shouldn't happen? |
171 |
| - |
172 |
| - function sortChunks(chunks) { |
173 |
| - const {value, size} = geometry.positions; |
174 |
| - const TypedArray = value.constructor; |
175 |
| - const newPositions = new TypedArray(value.length); |
176 |
| - let newInterpolatedProperties = []; |
177 |
| - let offset = 0; |
178 |
| - const newPathIndices = []; |
179 |
| - for (const chunk of chunks) { |
180 |
| - let { |
181 |
| - segment, |
182 |
| - segmentProperties, |
183 |
| - range: [startIndex, endIndex] |
184 |
| - } = chunk; |
185 |
| - |
186 |
| - try { |
187 |
| - // If last vertex in previous segment match first vertex in current segment, stitch line together |
188 |
| - let stitch = true; |
189 |
| - if (offset === 0) { |
190 |
| - stitch = false; |
191 |
| - } else { |
192 |
| - const lastVertex = newPositions.subarray((offset - 1) * size); |
193 |
| - for (let s = 0; s < size; s++) { |
194 |
| - if (segment[s] !== lastVertex[s]) stitch = false; |
195 |
| - } |
196 |
| - } |
197 |
| - if (stitch) { |
198 |
| - startIndex += 1; |
199 |
| - segment = segment.subarray(size); |
200 |
| - segmentProperties = segmentProperties.slice(1); |
201 |
| - } else { |
202 |
| - newPathIndices.push(offset); |
203 |
| - } |
204 |
| - |
205 |
| - newPositions.set(segment, offset * size); |
206 |
| - newInterpolatedProperties = newInterpolatedProperties.concat(segmentProperties); |
207 |
| - } catch (e) { |
208 |
| - debugger; |
209 |
| - } |
210 |
| - offset += endIndex - startIndex; |
211 |
| - } |
212 |
| - |
213 |
| - geometry.positions.value = newPositions.subarray(0, offset * size); |
214 |
| - interpolatedProperties = newInterpolatedProperties; |
215 |
| - newPathIndices.push(interpolatedProperties.length); |
216 |
| - geometry.pathIndices.value = new Uint32Array(newPathIndices); |
217 |
| - } |
218 |
| - |
219 |
| - // Also need to sort properties |
220 |
| - sortChunks(chunks); |
221 |
| - |
222 |
| - const stitchedN = interpolatedProperties.length; |
223 |
| - const size = 3; |
224 |
| - const value = new Float64Array(size * stitchedN); |
225 |
| - for (let i = 0; i < stitchedN; i++) { |
226 |
| - const {altitude, t} = interpolatedProperties[i]; |
227 |
| - |
228 |
| - // Populate 3D point data |
229 |
| - value[size * i] = geometry.positions.value[geometry.positions.size * i]; |
230 |
| - value[size * i + 1] = geometry.positions.value[geometry.positions.size * i + 1]; |
231 |
| - value[size * i + 2] = altitude; |
232 |
| - |
233 |
| - // Add altitude color per vertex |
234 |
| - // const color = getFillColor({altitude}); |
235 |
| - // geometry.attributes.getColor.value.set(color, 4 * i); |
236 |
| - geometry.attributes.getTimestamps.value[i] = t; |
237 |
| - } |
238 |
| - geometry.positions = {value, size}; |
239 |
| - |
240 |
| - let previousPosition; |
241 |
| - let maxS = 0; |
242 |
| - let previousSpeed = 0; |
243 |
| - for (let i = 0; i < stitchedN; i++) { |
244 |
| - // Calculate speed (only in 2D for now) |
245 |
| - let speed = 0; |
246 |
| - if (i < stitchedN) { |
247 |
| - const start = i === stitchedN - 1 ? i - 1 : i; |
248 |
| - const [lon1, lat1, alt1, lon2, lat2, alt2] = geometry.positions.value.subarray( |
249 |
| - 3 * start, |
250 |
| - 3 * start + 6 |
251 |
| - ); |
252 |
| - const deltaP = distanceBetweenPoints([lon1, lat1, lon2, lat2]); |
253 |
| - const [t1, t2] = geometry.attributes.getTimestamps.value.subarray(start, start + 2); |
254 |
| - let deltaT = t2 - t1; |
255 |
| - speed = deltaT > 0 ? deltaP / deltaT : 0; |
256 |
| - |
257 |
| - if (deltaT === 0) { |
258 |
| - // rounding gives 0 time delta, use previous speed value or hard code half second |
259 |
| - speed = previousSpeed; |
260 |
| - } |
261 |
| - |
262 |
| - maxS = Math.max(speed, maxS); |
263 |
| - previousSpeed = speed; |
264 |
| - } |
265 |
| - if (speed === 0) { |
266 |
| - console.log(speed, 'speed'); |
267 |
| - speed = 100; |
268 |
| - } |
269 |
| - // Add speed color per vertex |
270 |
| - const color = getSpeedColor({altitude: speed}); |
271 |
| - // color[3] = speed < 1 || speed > 50 ? 255 : 50; |
272 |
| - geometry.attributes.getColor.value.set(color, 4 * i); |
273 |
| - } |
274 |
| - |
275 |
| - return geometry; |
276 |
| -} |
| 9 | +const cartoMapId = 'ff6ac53f-741a-49fb-b615-d040bc5a96b8'; |
277 | 10 |
|
278 | 11 | // Get map info from CARTO and update deck
|
279 |
| -fetchMap({cartoMapId}).then(({initialViewState, basemap, layers: _baseLayers}) => { |
280 |
| - const showTrips = false; |
281 |
| - const props = { |
282 |
| - getWidth: 4, |
283 |
| - widthUnits: 'pixels', |
284 |
| - opacity: 0.8, // Best to use full opacity to hide segment joins |
285 |
| - billboard: true, |
286 |
| - jointRounded: true, |
287 |
| - capRounded: true, |
288 |
| - _pathType: 'open' |
289 |
| - // parameters: ADDITIVE_BLEND_PARAMETERS |
290 |
| - }; |
291 |
| - const layers = [ |
292 |
| - _baseLayers[3].clone({ |
293 |
| - dataTransform: d => { |
294 |
| - addAltitude(d.lines); |
295 |
| - return d; |
296 |
| - }, |
297 |
| - // tileSize: 256, |
298 |
| - zRange: [0, 4000], |
299 |
| - // refinementStrategy: 'no-overlap', |
300 |
| - _subLayerProps: { |
301 |
| - linestrings: showTrips |
302 |
| - ? { |
303 |
| - type: TripsLayer, |
304 |
| - getTimestamps: (d, info) => info.index, |
305 |
| - fadeTrail: true, |
306 |
| - currentTime: 20500, |
307 |
| - trailLength: 600, |
308 |
| - ...props |
309 |
| - } |
310 |
| - : { |
311 |
| - type: EdgedPathLayer, |
312 |
| - ...props |
313 |
| - } |
314 |
| - } |
315 |
| - }) |
316 |
| - ]; |
317 |
| - |
318 |
| - const {bearing, latitude, longitude, pitch, zoom} = initialViewState; |
| 12 | +fetchMap({cartoMapId}).then(({initialViewState, basemap, layers}) => { |
| 13 | + const deck = new Deck({canvas: 'deck-canvas', controller: true, initialViewState, layers}); |
319 | 14 |
|
320 | 15 | // Add Mapbox GL for the basemap. It's not a requirement if you don't need a basemap.
|
321 |
| - // const map = new maplibregl.Map({container: 'map', ...basemap?.props, interactive: false}); |
322 |
| - mapboxgl.accessToken = MAPBOX_TOKEN; |
323 |
| - const map = new mapboxgl.Map({ |
324 |
| - container: 'map', |
325 |
| - style: 'mapbox://styles/mapbox/satellite-streets-v12', |
326 |
| - interactive: true, // Have Mapbox control control for better 3D orientation |
327 |
| - |
328 |
| - // View state |
329 |
| - bearing, |
330 |
| - center: [longitude, latitude], |
331 |
| - pitch, |
332 |
| - zoom, |
333 |
| - boxZoom: false, |
334 |
| - maxPitch: 50 |
335 |
| - }); |
336 |
| - map.on('style.load', () => { |
337 |
| - map.addSource('mapbox-dem', { |
338 |
| - type: 'raster-dem', |
339 |
| - url: 'mapbox://mapbox.mapbox-terrain-dem-v1', |
340 |
| - tileSize: 512, |
341 |
| - maxzoom: 14 |
342 |
| - }); |
343 |
| - // Do not exaggerate, so deck layers match elevation |
344 |
| - map.setTerrain({source: 'mapbox-dem', exaggeration: 1}); |
345 |
| - }); |
346 |
| - |
347 |
| - const overlay = new MapboxOverlay({ |
348 |
| - layers: layers, |
349 |
| - onAfterRender: () => { |
350 |
| - const isLoading = layers.every(layer => layer.isLoaded); |
351 |
| - document.getElementById('loading-indicator').style.transition = isLoading ? 'opacity 1s' : ''; |
352 |
| - document.getElementById('loading-indicator').style.opacity = isLoading ? 0 : 1; |
| 16 | + const map = new maplibregl.Map({container: 'map', ...basemap?.props, interactive: false}); |
| 17 | + deck.setProps({ |
| 18 | + onViewStateChange: ({viewState}) => { |
| 19 | + const {longitude, latitude, ...rest} = viewState; |
| 20 | + map.jumpTo({center: [longitude, latitude], ...rest}); |
353 | 21 | }
|
354 | 22 | });
|
355 |
| - map.addControl(overlay); |
356 | 23 | });
|
0 commit comments