@@ -7,11 +7,15 @@ import { ServiceError } from "./ServiceError";
77import { math } from "./math" ;
88import {
99 TileJSON ,
10+ canParsePixelData ,
1011 getBufferToPixelDataParser ,
1112 getTileCache ,
1213} from "../tiledecoding" ;
1314
14- const terrainTileJsonURL = "tiles/terrain-rgb-v2/tiles.json" ;
15+ const TERRAIN_TILESET = "terrain-rgb-v2" ;
16+ const API_BATCH_SIZE = 50 ;
17+ const API_WARN_SIZE = 1000 ;
18+
1519let terrainTileJson : TileJSON = null ;
1620
1721export type ElevationAtOptions = {
@@ -21,10 +25,17 @@ export type ElevationAtOptions = {
2125 apiKey ?: string ;
2226
2327 /**
24- * Zoom level to use for the terrain RGB tileset.
25- * If not provided, the highest zoom level will be used
28+ * Zoom level to use for the terrain tileset in `client` mode .
29+ * If not provided, the highest zoom level will be used.
2630 */
2731 zoom ?: number ;
32+
33+ /**
34+ * If set to `client`, the elevation will be computed from the terrain tiles on the client side.
35+ * If set to `server`, the elevation will be obtained from the MapTiler Elevation API.
36+ * Defaults to `server` for `at`, `batch`, and `client` for `fromLineString`, `fromMultiLineString` (in browser envs).
37+ */
38+ computeOn ?: "client" | "server" ;
2839} ;
2940
3041/**
@@ -42,110 +53,72 @@ const customMessages = {
4253 403 : "Key is missing, invalid or restricted" ,
4354} ;
4455
45- async function fetchTerrainTileJson ( apiKey : string ) : Promise < TileJSON > {
46- const endpoint = new URL ( terrainTileJsonURL , defaults . maptilerApiURL ) ;
47- endpoint . searchParams . set ( "key" , apiKey ) ;
48- const urlWithParams = endpoint . toString ( ) ;
49-
50- const res = await callFetch ( urlWithParams ) ;
51- if ( res . ok ) {
52- terrainTileJson = ( await res . json ( ) ) as TileJSON ;
53- return terrainTileJson ;
54- } else {
55- if ( ! res . ok ) {
56- throw new ServiceError ( res , customMessages [ res . status ] ?? "" ) ;
57- }
58- }
59- }
60-
61- /**
62- * Get the elevation at a given position.
63- * The returned position is of form [longitude, latitude, altitude]
64- */
65- async function at (
66- /**
67- * Wgs84 position as [longitude, latitude]
68- */
69- position : Position ,
70- /**
71- * Options
72- */
73- options : ElevationAtOptions = { } ,
74- ) : Promise < Position > {
75- const apiKey = options . apiKey ?? config . apiKey ;
76-
77- if ( ! terrainTileJson ) {
78- await fetchTerrainTileJson ( apiKey ) ;
79- }
80-
81- const maxZoom = terrainTileJson . maxzoom ;
82- let zoom = ~ ~ ( options . zoom ?? maxZoom ) ;
83- if ( zoom > maxZoom || zoom < 0 ) {
84- zoom = maxZoom ;
85- }
86- const tileIndex = math . wgs84ToTileIndex ( position , zoom , false ) ;
87-
88- const tileX = ~ ~ tileIndex [ 0 ] ;
89- const tileY = ~ ~ tileIndex [ 1 ] ;
90-
91- if ( ! terrainTileJson . tiles . length ) {
92- throw new Error ( "Terrain tileJSON tile list is empty." ) ;
56+ async function computeOnServer (
57+ positions : Position [ ] ,
58+ apiKey : string ,
59+ ) : Promise < Position [ ] > {
60+ if ( positions . length > API_WARN_SIZE ) {
61+ console . warn (
62+ "Computing elevation for complex geometries is discouraged - simplify the geometry before proceeding" ,
63+ ) ;
9364 }
9465
95- const tileID = `terrain_${ zoom . toString ( ) } _${ tileX . toString ( ) } _${ tileY . toString ( ) } ` ;
96- let tilePixelData ;
97-
98- const cache = getTileCache ( ) ;
99-
100- if ( cache . has ( tileID ) ) {
101- tilePixelData = cache . get ( tileID ) ;
102- } else {
103- const tileURL = terrainTileJson . tiles [ 0 ]
104- . replace ( "{x}" , tileX . toString ( ) )
105- . replace ( "{y}" , tileY . toString ( ) )
106- . replace ( "{z}" , zoom . toString ( ) ) ;
107-
108- const tileRes = await callFetch ( tileURL ) ;
109-
110- if ( ! tileRes . ok ) {
111- throw new ServiceError ( tileRes , customMessages [ tileRes . status ] ?? "" ) ;
112- }
113-
114- const tileBuff = await tileRes . arrayBuffer ( ) ;
115- const tileParser = getBufferToPixelDataParser ( ) ;
116- tilePixelData = await tileParser ( tileBuff ) ;
117- cache . set ( tileID , tilePixelData ) ;
118- }
66+ const parts = Math . ceil ( positions . length / API_BATCH_SIZE ) ;
67+ const respPromises = Array . from ( { length : parts } , ( ) => null ) . map (
68+ ( _ , part ) => {
69+ const startPos = part * API_BATCH_SIZE ;
70+ const batch = positions . slice ( startPos , startPos + API_BATCH_SIZE ) ;
71+ const batchEncoded = batch . map ( ( pos ) => pos . join ( "," ) ) . join ( ";" ) ;
72+ const endpoint = new URL (
73+ `elevation/${ batchEncoded } .json` ,
74+ defaults . maptilerApiURL ,
75+ ) ;
76+ endpoint . searchParams . set ( "key" , apiKey ) ;
77+ return callFetch ( endpoint . toString ( ) ) ;
78+ } ,
79+ ) ;
11980
120- const pixelX = ~ ~ ( tilePixelData . width * ( tileIndex [ 0 ] % 1 ) ) ;
121- const pixelY = ~ ~ ( tilePixelData . height * ( tileIndex [ 1 ] % 1 ) ) ;
122- const pixelDataIndex =
123- ( pixelY * tilePixelData . width + pixelX ) * tilePixelData . components ;
124- const R = tilePixelData . pixels [ pixelDataIndex ] ;
125- const G = tilePixelData . pixels [ pixelDataIndex + 1 ] ;
126- const B = tilePixelData . pixels [ pixelDataIndex + 2 ] ;
127- const elevation = - 10000 + ( R * 256 * 256 + G * 256 + B ) * 0.1 ;
81+ const resps = await Promise . allSettled ( respPromises ) ;
82+ const jsons = await Promise . all (
83+ resps . map ( async ( resp ) => {
84+ if ( resp . status === "rejected" ) {
85+ throw new Error (
86+ `Some segments could not be fetched, error: ${ resp . reason } ` ,
87+ ) ;
88+ }
89+ if ( ! resp . value . ok ) {
90+ throw new Error (
91+ `Some segments could not be fetched, response: ${
92+ resp . value . status
93+ } ${ await resp . value . text ( ) } , url: ${ resp . value . url } `,
94+ ) ;
95+ }
96+ return resp . value . json ( ) ;
97+ } ) ,
98+ ) ;
12899
129- return [ position [ 0 ] , position [ 1 ] , elevation ] ;
100+ return jsons . flat ( ) ;
130101}
131102
132- /**
133- * Perform a batch elevation request
134- */
135- async function batch (
136- /**
137- * Wgs84 positions as [[lng0, lat0], [lng1, lat1], [lng2, lat2], ...]
138- */
103+ async function computeOnClient (
139104 positions : Position [ ] ,
140- /**
141- * Options
142- */
143- options : ElevationBatchOptions = { } ,
105+ apiKey : string ,
106+ zoom ?: number ,
144107) : Promise < Position [ ] > {
145- const apiKey = options . apiKey ?? config . apiKey ;
146-
108+ // Fetch terrain TileJSON
147109 if ( ! terrainTileJson ) {
148- await fetchTerrainTileJson ( apiKey ) ;
110+ const endpoint = new URL (
111+ `tiles/${ TERRAIN_TILESET } /tiles.json` ,
112+ defaults . maptilerApiURL ,
113+ ) ;
114+ endpoint . searchParams . set ( "key" , apiKey ) ;
115+ const urlWithParams = endpoint . toString ( ) ;
116+ const res = await callFetch ( urlWithParams ) ;
117+ if ( res . ok ) {
118+ terrainTileJson = ( await res . json ( ) ) as TileJSON ;
119+ } else {
120+ throw new ServiceError ( res , customMessages [ res . status ] ?? "" ) ;
121+ }
149122 }
150123
151124 // Better throw about not bein able to parse tiles before fetching them
@@ -154,20 +127,20 @@ async function batch(
154127 const cache = getTileCache ( ) ;
155128
156129 const maxZoom = terrainTileJson . maxzoom ;
157- let zoom = ~ ~ ( options . zoom ?? maxZoom ) ;
158- if ( zoom > maxZoom || zoom < 0 ) {
159- zoom = maxZoom ;
130+ let usedZoom = ~ ~ ( zoom ?? maxZoom ) ;
131+ if ( usedZoom > maxZoom || usedZoom < 0 ) {
132+ usedZoom = maxZoom ;
160133 }
161134 const tileIndicesFloats = positions . map ( ( position ) =>
162- math . wgs84ToTileIndex ( position , zoom , false ) ,
135+ math . wgs84ToTileIndex ( position , usedZoom , false ) ,
163136 ) ;
164137 const tileIndicesInteger = tileIndicesFloats . map ( ( index ) => [
165138 ~ ~ index [ 0 ] ,
166139 ~ ~ index [ 1 ] ,
167140 ] ) ;
168141 const tileIDs = tileIndicesInteger . map (
169142 ( index ) =>
170- `terrain_${ zoom . toString ( ) } _${ index [ 0 ] . toString ( ) } _${ index [ 1 ] . toString ( ) } ` ,
143+ `terrain_${ usedZoom . toString ( ) } _${ index [ 0 ] . toString ( ) } _${ index [ 1 ] . toString ( ) } ` ,
171144 ) ;
172145
173146 // unique tiles to fetch (excluding those already in cache and the doublons)
@@ -247,6 +220,49 @@ async function batch(
247220 return [ position [ 0 ] , position [ 1 ] , ~ ~ ( elevation * 1000 ) / 1000 ] ;
248221 } ) ;
249222
223+ return elevatedPositions ;
224+ }
225+
226+ /**
227+ * Get the elevation at a given position.
228+ * The returned position is of form [longitude, latitude, altitude]
229+ */
230+ async function at (
231+ /**
232+ * Wgs84 position as [longitude, latitude]
233+ */
234+ position : Position ,
235+ /**
236+ * Options
237+ */
238+ options : ElevationAtOptions = { } ,
239+ ) : Promise < Position > {
240+ const elevatedPositions = await batch ( [ position ] , options ) ;
241+ return elevatedPositions [ 0 ] ;
242+ }
243+
244+ /**
245+ * Perform a batch elevation request
246+ */
247+ async function batch (
248+ /**
249+ * Wgs84 positions as [[lng0, lat0], [lng1, lat1], [lng2, lat2], ...]
250+ */
251+ positions : Position [ ] ,
252+ /**
253+ * Options
254+ */
255+ options : ElevationBatchOptions = { } ,
256+ ) : Promise < Position [ ] > {
257+ if ( positions . length === 0 ) return [ ] ;
258+
259+ const apiKey = options . apiKey ?? config . apiKey ;
260+
261+ const elevatedPositions =
262+ options . computeOn === "client"
263+ ? await computeOnClient ( positions , apiKey , options . zoom )
264+ : await computeOnServer ( positions , apiKey ) ;
265+
250266 // Smoothing
251267 if ( options . smoothingKernelSize ) {
252268 // make sure the kernel is of an odd size
@@ -286,6 +302,7 @@ async function fromLineString(
286302 throw new Error ( "The provided object is not a GeoJSON LineString" ) ;
287303 }
288304
305+ options . computeOn ??= canParsePixelData ( ) ? "client" : "server" ;
289306 const clone = structuredClone ( ls ) as LineString ;
290307 const elevatedPositions = await batch ( clone . coordinates , options ) ;
291308 clone . coordinates = elevatedPositions ;
@@ -311,11 +328,12 @@ async function fromMultiLineString(
311328 throw new Error ( "The provided object is not a GeoJSON MultiLineString" ) ;
312329 }
313330
331+ options . computeOn ??= canParsePixelData ( ) ? "client" : "server" ;
314332 const clone = structuredClone ( ls ) as MultiLineString ;
315333 const multiLengths = clone . coordinates . map ( ( poss ) => poss . length ) ;
316334
317- // This is equivalent to a batch of batch, so we makes the multilinestring a unique
318- // line string to prevent batch to fetch multiple times the same tile
335+ // This is equivalent to a batch of batch, so we makes the multilinestring
336+ // a unique line string to reduce number of requests
319337 const flattenPositions = clone . coordinates . flat ( ) ;
320338 const flattenPositionsElevated = await batch ( flattenPositions , options ) ;
321339
0 commit comments