@dataforsyningen/saul 3.6.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +17 -14
- package/modules/api.js +1 -70
- package/modules/{saul-core.js → core.js} +1 -15
- package/modules/elevation.js +204 -0
- package/modules/image.js +256 -0
- package/package.json +1 -1
- package/test/saul-elevation.test.js +10 -1
- package/example.html +0 -58
- package/modules/saul-elevation.js +0 -106
- /package/modules/{saul-projection.js → projection.js} +0 -0
package/index.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
// Import/export modules with their public methods
|
|
2
2
|
|
|
3
|
-
export {
|
|
4
|
-
image2world,
|
|
5
|
-
world2image, // Deprecated. Use getImageXY
|
|
6
|
-
getWorldXYZ,
|
|
7
|
-
getImageXY,
|
|
8
|
-
getZ,
|
|
9
|
-
iterate // Deprecated. Use getWorldXYZ
|
|
10
|
-
} from './modules/saul-core.js'
|
|
11
|
-
|
|
12
3
|
export {
|
|
13
4
|
postSTAC,
|
|
14
5
|
getSTAC,
|
|
15
6
|
getDHM,
|
|
16
|
-
getTerrainGeoTIFF,
|
|
17
|
-
getDenmarkGeoTiff,
|
|
18
7
|
get,
|
|
19
8
|
post
|
|
20
9
|
} from './modules/api.js'
|
|
@@ -22,9 +11,23 @@ export {
|
|
|
22
11
|
export {
|
|
23
12
|
epsg25832proj,
|
|
24
13
|
createTranslator
|
|
25
|
-
} from './modules/
|
|
14
|
+
} from './modules/projection.js'
|
|
26
15
|
|
|
27
16
|
export {
|
|
28
17
|
getElevation,
|
|
29
|
-
visualizeGeotiff
|
|
30
|
-
|
|
18
|
+
visualizeGeotiff,
|
|
19
|
+
getZ,
|
|
20
|
+
getTerrainGeoTIFF,
|
|
21
|
+
getDenmarkGeoTiff,
|
|
22
|
+
getTerrainByBbox,
|
|
23
|
+
consumeGeoTIFF
|
|
24
|
+
} from './modules/elevation.js'
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
getTotalBbox,
|
|
28
|
+
image2world,
|
|
29
|
+
world2image, // Deprecated. Use getImageXY
|
|
30
|
+
getWorldXYZ,
|
|
31
|
+
getImageXY,
|
|
32
|
+
iterate // Deprecated. Use getWorldXYZ
|
|
33
|
+
} from './modules/image.js'
|
package/modules/api.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* SAUL API utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { fromArrayBuffer } from 'geotiff'
|
|
6
|
-
|
|
7
5
|
let error_msg
|
|
8
6
|
let load_stack = []
|
|
9
7
|
|
|
@@ -194,77 +192,10 @@ function postSTAC(endpoint, data, auth) {
|
|
|
194
192
|
.then((data) => data)
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
/** Converts raw GeoTIFF arrayBuffer to image */
|
|
199
|
-
async function consumeGeoTIFF(raw_data) {
|
|
200
|
-
const tiff = await fromArrayBuffer(raw_data)
|
|
201
|
-
const image = await tiff.getImage()
|
|
202
|
-
return image
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function calcSizeRatio(sizeX, bbox) {
|
|
206
|
-
const ratio = ( bbox[0] - bbox[2] ) / ( bbox[1] - bbox[3] )
|
|
207
|
-
return Math.round(sizeX * ratio)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/** Fetches a GeoTIFF with elevation data matching the bounding box of a STAC API item (image)
|
|
212
|
-
* @param {object} stac_item - STAC API item from a featureCollection request
|
|
213
|
-
* @param {{API_DHM_WCS_BASEURL: string, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
214
|
-
* @param {number} [resolution] - Resolution (1 - 0.01). Higher number means more pixels and better precision.
|
|
215
|
-
* @returns {object} GeoTiff data
|
|
216
|
-
*/
|
|
217
|
-
function getTerrainGeoTIFF(stac_item, auth, resolution = 0.05, sizeX = 300) {
|
|
218
|
-
|
|
219
|
-
const bbox = stac_item.bbox
|
|
220
|
-
const sizeY = calcSizeRatio(sizeX, stac_item.bbox)
|
|
221
|
-
const width = stac_item.properties ? Math.round( stac_item.properties['proj:shape'][0] * resolution ) : sizeX
|
|
222
|
-
const height = stac_item.properties ? Math.round( stac_item.properties['proj:shape'][1] * resolution ) : sizeY
|
|
223
|
-
|
|
224
|
-
// GET request for DHM WCS data
|
|
225
|
-
let url = auth.API_DHM_WCS_BASEURL
|
|
226
|
-
url += '?SERVICE=WCS&COVERAGE=dhm_terraen&RESPONSE_CRS=epsg:25832&CRS=epsg:25832&FORMAT=GTiff&REQUEST=GetCoverage&VERSION=1.0.0'
|
|
227
|
-
url += `&username=${ auth.API_DHM_TOKENA }&password=${ auth.API_DHM_TOKENB }`
|
|
228
|
-
url += `&height=${ height }`
|
|
229
|
-
url += `&width=${ width }`
|
|
230
|
-
url += `&bbox=${ bbox[0]},${ bbox[1]},${ bbox[2]},${ bbox[3]}`
|
|
231
|
-
|
|
232
|
-
return get(url, {cache: 'force-cache'}, false)
|
|
233
|
-
.then((response) => {
|
|
234
|
-
return response.arrayBuffer()
|
|
235
|
-
})
|
|
236
|
-
.then((arrayBuffer) => {
|
|
237
|
-
return consumeGeoTIFF(arrayBuffer)
|
|
238
|
-
})
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Fetches a geoTIFF with elevation data covering all of Denmark
|
|
243
|
-
* @param {Object} options
|
|
244
|
-
* @param {Object} options.src - URL to download a pre-generated GeoTiff.
|
|
245
|
-
* @param {Object} options.auth - API autentication data. See ../config.js.example for reference.
|
|
246
|
-
* @param {Number} options.size - Width of the geoTiff image to return
|
|
247
|
-
* @returns GeoTIFF raster with elevation data
|
|
248
|
-
*/
|
|
249
|
-
function getDenmarkGeoTiff(options) {
|
|
250
|
-
if (options.src) {
|
|
251
|
-
return fetch(options.src)
|
|
252
|
-
.then(response => response.arrayBuffer())
|
|
253
|
-
.then(arrayBuffer => consumeGeoTIFF(arrayBuffer))
|
|
254
|
-
} else {
|
|
255
|
-
const auth = options.auth
|
|
256
|
-
const size = options.size ? options.size : 1000
|
|
257
|
-
return getTerrainGeoTIFF({bbox: [430000,6040000,900000,6413000]}, auth, null, size)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
195
|
export {
|
|
262
196
|
get,
|
|
263
197
|
post,
|
|
264
198
|
getSTAC,
|
|
265
199
|
postSTAC,
|
|
266
|
-
getDHM
|
|
267
|
-
getTerrainGeoTIFF,
|
|
268
|
-
getDenmarkGeoTiff,
|
|
269
|
-
consumeGeoTIFF
|
|
200
|
+
getDHM
|
|
270
201
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* SAUL photogrammetry utilities
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { getElevation } from './saul-elevation.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -136,19 +136,6 @@ function radians(degrees) {
|
|
|
136
136
|
return degrees * (Math.PI / 180)
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/**
|
|
140
|
-
* Fetches a single elevation value based on X,Y coordinates using DHM/Koter endpoint
|
|
141
|
-
* @param {number} xcoor - EPSG:25832 X coordinate
|
|
142
|
-
* @param {number} ycoor - EPSG:25832 Y coordinate
|
|
143
|
-
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
144
|
-
* @returns {number} Elevation in meters
|
|
145
|
-
*/
|
|
146
|
-
async function getZ(xcoor, ycoor, auth) {
|
|
147
|
-
let zcoor_data = await getDHM(`?geop=POINT(${xcoor} ${ycoor})&elevationmodel=dtm`, auth)
|
|
148
|
-
let z = zcoor_data.HentKoterRespons.data[0].kote
|
|
149
|
-
return z
|
|
150
|
-
}
|
|
151
|
-
|
|
152
139
|
/** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
153
140
|
function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
154
141
|
|
|
@@ -239,7 +226,6 @@ export {
|
|
|
239
226
|
world2image,
|
|
240
227
|
getWorldXYZ,
|
|
241
228
|
getImageXY,
|
|
242
|
-
getZ,
|
|
243
229
|
iterate
|
|
244
230
|
}
|
|
245
231
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { getDHM, get } from './api.js'
|
|
2
|
+
import { fromArrayBuffer } from 'geotiff'
|
|
3
|
+
|
|
4
|
+
/** Converts raw GeoTIFF arrayBuffer to image */
|
|
5
|
+
async function consumeGeoTIFF(raw_data) {
|
|
6
|
+
const tiff = await fromArrayBuffer(raw_data)
|
|
7
|
+
const image = await tiff.getImage()
|
|
8
|
+
return image
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function calcSizeRatio(sizeX, bbox) {
|
|
12
|
+
const ratio = ( bbox[0] - bbox[2] ) / ( bbox[1] - bbox[3] )
|
|
13
|
+
return Math.round(sizeX * ratio)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fetches a GeoTIFF with elevation data matching the bounding box of a STAC API item (image)
|
|
17
|
+
* @param {object} stac_item - STAC API item from a featureCollection request
|
|
18
|
+
* @param {{API_DHM_WCS_BASEURL: string, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
19
|
+
* @param {number} [resolution] - Resolution (1 - 0.01). Higher number means more pixels and better precision.
|
|
20
|
+
* @returns {object} GeoTiff data
|
|
21
|
+
*/
|
|
22
|
+
function getTerrainGeoTIFF(stac_item, auth, resolution = 0.05, sizeX = 300) {
|
|
23
|
+
|
|
24
|
+
const bbox = stac_item.bbox
|
|
25
|
+
const width = stac_item.properties ? Math.round( stac_item.properties['proj:shape'][0] * resolution ) : sizeX
|
|
26
|
+
|
|
27
|
+
return getTerrainByBbox(bbox, auth, width)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetches a geoTIFF with elevation data covering all of Denmark
|
|
32
|
+
* @param {Object} options
|
|
33
|
+
* @param {Object} options.src - URL to download a pre-generated GeoTiff.
|
|
34
|
+
* @param {Object} options.auth - API autentication data. See ../config.js.example for reference.
|
|
35
|
+
* @param {Number} options.size - Width of the geoTiff image to return
|
|
36
|
+
* @returns GeoTIFF raster with elevation data
|
|
37
|
+
*/
|
|
38
|
+
function getDenmarkGeoTiff(options) {
|
|
39
|
+
if (options.src) {
|
|
40
|
+
return fetch(options.src)
|
|
41
|
+
.then(response => response.arrayBuffer())
|
|
42
|
+
.then(arrayBuffer => consumeGeoTIFF(arrayBuffer))
|
|
43
|
+
} else {
|
|
44
|
+
const auth = options.auth
|
|
45
|
+
const size = options.size ? options.size : 1000
|
|
46
|
+
return getTerrainByBbox([430000,6040000,900000,6413000], auth, size)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Maps a coordinate from the one coordinate system to another based on GeoTiff image dimensions.
|
|
52
|
+
* @param {Array} coord - EPSG:25832 X,Y coordinate pair
|
|
53
|
+
* @param {Array} bbox - Bounding box within input coordinate systtem. Consists of 2 EPSG:25832 coordinate pairs. Example: [x1,y1,x2,y2]
|
|
54
|
+
* @param {Array} imageDimensions - Width and height of a GeoTiff image. This will set the bounding box of the output coordinate system. Note: This coordinate system will have 0,0 set in the top right position of the image.
|
|
55
|
+
*/
|
|
56
|
+
function convertCoordinate(coord, bbox1, imageDimensions) {
|
|
57
|
+
|
|
58
|
+
// Extract the coordinates from the bounding boxes
|
|
59
|
+
const [x1_min, y1_min, x1_max, y1_max] = bbox1
|
|
60
|
+
|
|
61
|
+
// Extract the coordinate to convert
|
|
62
|
+
const [x1, y1] = coord
|
|
63
|
+
|
|
64
|
+
// Calculate the width and height of both bounding boxes
|
|
65
|
+
const width1 = x1_max - x1_min
|
|
66
|
+
const height1 = y1_max - y1_min
|
|
67
|
+
const width2 = imageDimensions[0]
|
|
68
|
+
const height2 = imageDimensions[1]
|
|
69
|
+
|
|
70
|
+
// Calculate the relative position in the first coordinate system
|
|
71
|
+
const x_relative = (x1 - x1_min) / width1
|
|
72
|
+
const y_relative = (y1 - y1_min) / height1
|
|
73
|
+
|
|
74
|
+
// Calculate the new position in the second coordinate system
|
|
75
|
+
const x2 = Math.abs(x_relative * width2)
|
|
76
|
+
const y2 = height2 - Math.abs(y_relative * height2)
|
|
77
|
+
|
|
78
|
+
return [x2, y2]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculates nearest elevation for given coordinate using geoTIFF terrain data
|
|
83
|
+
* @param {number} x - EPSG:25832 X coordinate
|
|
84
|
+
* @param {number} y - EPSG:25832 Y coordinate
|
|
85
|
+
* @param {object} geoTiff - GeoTiff data output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
|
|
86
|
+
* @returns {number} Elevation in meters
|
|
87
|
+
*/
|
|
88
|
+
async function getElevation(x, y, geoTiff) {
|
|
89
|
+
|
|
90
|
+
const bbox = geoTiff.getBoundingBox()
|
|
91
|
+
const height = geoTiff.getHeight()
|
|
92
|
+
const width = geoTiff.getWidth()
|
|
93
|
+
|
|
94
|
+
// Convert lat/lon to geotiff x/y
|
|
95
|
+
const [xPx, yPx] = convertCoordinate([x,y], bbox, [width, height])
|
|
96
|
+
|
|
97
|
+
// Fetch window of raster values around x/y
|
|
98
|
+
const window = [
|
|
99
|
+
Math.floor(xPx),
|
|
100
|
+
Math.floor(yPx),
|
|
101
|
+
Math.ceil(xPx),
|
|
102
|
+
Math.ceil(yPx)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
// Fetch elevation value from geoTiff rasters
|
|
106
|
+
const elevation_data = await geoTiff.readRasters({
|
|
107
|
+
window: window,
|
|
108
|
+
fillValue: 0
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return elevation_data[0][0]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Creates an ASCII visualization of a GeoTiff with terrain elevation data. Useful for display in a terminal.
|
|
115
|
+
* NOTE: You may want to use a downsmapled GeoTiff to fit within your terminal window. A GeoTiff pixel is represented by 3 characters each.
|
|
116
|
+
* @param {object} gTiff - GeoTiff output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
|
|
117
|
+
*/
|
|
118
|
+
async function visualizeGeotiff(gTiff) {
|
|
119
|
+
console.log('--- GTIFF visualization ---')
|
|
120
|
+
const float32Arr = await gTiff.readRasters()
|
|
121
|
+
const tiffWidth = gTiff.getWidth()
|
|
122
|
+
const rasters = float32Arr[0]
|
|
123
|
+
let lines = []
|
|
124
|
+
let line = ''
|
|
125
|
+
for (let i = 0; i < rasters.length; i++) {
|
|
126
|
+
if (i % tiffWidth === 0) {
|
|
127
|
+
lines.push(line)
|
|
128
|
+
line = ''
|
|
129
|
+
}
|
|
130
|
+
line += zeroPadNumber(rasters[i])
|
|
131
|
+
}
|
|
132
|
+
for (const l of lines) {
|
|
133
|
+
console.log(l)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Converts numbers to strings in a 3-letter format. Ex. `002`, `0-3` (for negative numbers), or `123` */
|
|
138
|
+
function zeroPadNumber(input) {
|
|
139
|
+
if (input === 0) {
|
|
140
|
+
return "..."
|
|
141
|
+
} else if (input < 0) {
|
|
142
|
+
return `0${input.toFixed(0)}`
|
|
143
|
+
} else if (input < 10) {
|
|
144
|
+
return `00${Math.floor(input)}`
|
|
145
|
+
} else if (input < 100) {
|
|
146
|
+
return `0${Math.floor(input)}`
|
|
147
|
+
} else {
|
|
148
|
+
return `${Math.floor(input)}`
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fetches a single elevation value based on X,Y coordinates using DHM/Koter endpoint
|
|
154
|
+
* @param {number} xcoor - EPSG:25832 X coordinate
|
|
155
|
+
* @param {number} ycoor - EPSG:25832 Y coordinate
|
|
156
|
+
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
157
|
+
* @returns {number} Elevation in meters
|
|
158
|
+
*/
|
|
159
|
+
async function getZ(xcoor, ycoor, auth) {
|
|
160
|
+
let zcoor_data = await getDHM(`?geop=POINT(${xcoor} ${ycoor})&elevationmodel=dtm`, auth)
|
|
161
|
+
let z = zcoor_data.HentKoterRespons.data[0].kote
|
|
162
|
+
return z
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Fetches a GeoTIFF with elevation data matching a bounding box of EPSG:25832 coordinates
|
|
166
|
+
* @param {Array} bbox - Bounding box array consisting of two sets of EPSG:25832 coordinates (ie. `[543049, 6153925, 544463, 6155221]`)
|
|
167
|
+
* @param {{API_DHM_WCS_BASEURL: string, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
168
|
+
* @param {number} [sizeLimit] - Optional size limiter. Will request GeoTIFF with width that does not exceed this number. Default is 500
|
|
169
|
+
* @returns {object} GeoTiff data
|
|
170
|
+
*/
|
|
171
|
+
function getTerrainByBbox(bbox, auth, sizeLimit = 500) {
|
|
172
|
+
|
|
173
|
+
const width = sizeLimit
|
|
174
|
+
const height = Math.round(calcSizeRatio(sizeLimit, bbox))
|
|
175
|
+
|
|
176
|
+
// GET request for DHM WCS data
|
|
177
|
+
let url = auth.API_DHM_WCS_BASEURL
|
|
178
|
+
url += '?SERVICE=WCS&COVERAGE=dhm_terraen&RESPONSE_CRS=epsg:25832&CRS=epsg:25832&FORMAT=GTiff&REQUEST=GetCoverage&VERSION=1.0.0'
|
|
179
|
+
url += `&username=${ auth.API_DHM_TOKENA }&password=${ auth.API_DHM_TOKENB }`
|
|
180
|
+
url += `&height=${ height }`
|
|
181
|
+
url += `&width=${ width }`
|
|
182
|
+
url += `&bbox=${ Math.round(bbox[0]) - 200 },${ Math.round(bbox[1]) - 200 },${ Math.round(bbox[2]) + 200},${ Math.round(bbox[3]) + 200}` // Add/subtract 200 meters to bbox to ensure coverage on the edges
|
|
183
|
+
|
|
184
|
+
return get(url, {cache: 'force-cache'}, false)
|
|
185
|
+
.then((response) => {
|
|
186
|
+
return response.arrayBuffer()
|
|
187
|
+
})
|
|
188
|
+
.then((arrayBuffer) => {
|
|
189
|
+
return consumeGeoTIFF(arrayBuffer)
|
|
190
|
+
})
|
|
191
|
+
.catch(error => {
|
|
192
|
+
return error
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
getElevation,
|
|
198
|
+
visualizeGeotiff,
|
|
199
|
+
getZ,
|
|
200
|
+
getTerrainGeoTIFF,
|
|
201
|
+
getDenmarkGeoTiff,
|
|
202
|
+
getTerrainByBbox,
|
|
203
|
+
consumeGeoTIFF
|
|
204
|
+
}
|
package/modules/image.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAUL photogrammetry utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getElevation, getZ } from './elevation.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts x,y coordinates from an image to real world lat,lon coordinates
|
|
9
|
+
* @param {object} image_data - skraafoto-stac-api image data
|
|
10
|
+
* @param {number} col - Image x coordinate (from left to right). Should be a coordinate for the entire image, not just the part displayed in the viewport.
|
|
11
|
+
* @param {number} row - Image y coordinate (from top to bottom). Should be a coordinate for the entire image, not just the part displayed in the viewport.
|
|
12
|
+
* @param {number} [Z] - Elevation (geoide)
|
|
13
|
+
* @returns {array} [longitude, latitude, elevation]
|
|
14
|
+
*/
|
|
15
|
+
function image2world(image_data, col, row, Z = 0) {
|
|
16
|
+
|
|
17
|
+
// constants pulled from image_data
|
|
18
|
+
const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
|
|
19
|
+
const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
|
|
20
|
+
const ci = image_data.properties['pers:interior_orientation'].focal_length
|
|
21
|
+
const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
|
|
22
|
+
const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
|
|
23
|
+
const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
|
|
24
|
+
const X0 = image_data.properties['pers:perspective_center'][0]
|
|
25
|
+
const Y0 = image_data.properties['pers:perspective_center'][1]
|
|
26
|
+
const Z0 = image_data.properties['pers:perspective_center'][2]
|
|
27
|
+
const Ome = image_data.properties['pers:omega']
|
|
28
|
+
const Phi = image_data.properties['pers:phi']
|
|
29
|
+
const Kap = image_data.properties['pers:kappa']
|
|
30
|
+
|
|
31
|
+
// recalc values
|
|
32
|
+
var c = ci*(-1)
|
|
33
|
+
var dimX = dimXi*pix/2*(-1)
|
|
34
|
+
var dimY = dimYi*pix/2*(-1)
|
|
35
|
+
|
|
36
|
+
// ... Do calculations ...
|
|
37
|
+
var o = radians(Ome)
|
|
38
|
+
var p = radians(Phi)
|
|
39
|
+
var k = radians(Kap)
|
|
40
|
+
var D11 = Math.cos(p) * Math.cos(k)
|
|
41
|
+
var D12 = - Math.cos(p) * Math.sin(k)
|
|
42
|
+
var D13 = Math.sin(p)
|
|
43
|
+
var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
|
|
44
|
+
var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
|
|
45
|
+
var D23 = - Math.sin(o) * Math.cos(p)
|
|
46
|
+
var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
|
|
47
|
+
var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
|
|
48
|
+
var D33 = Math.cos(o) * Math.cos(p)
|
|
49
|
+
|
|
50
|
+
var x_dot = ((col*pix)-dimX*-1)-xx0
|
|
51
|
+
var y_dot = ((row*pix)-dimY*-1)-yy0
|
|
52
|
+
|
|
53
|
+
var kx = (D11*x_dot + D12*y_dot + D13*c)/(D31*x_dot + D32*y_dot + D33*c)
|
|
54
|
+
var ky = (D21*x_dot + D22*y_dot + D23*c)/(D31*x_dot + D32*y_dot + D33*c)
|
|
55
|
+
|
|
56
|
+
var X = (Z-Z0)*kx + X0
|
|
57
|
+
var Y = (Z-Z0)*ky + Y0
|
|
58
|
+
|
|
59
|
+
return[
|
|
60
|
+
Math.round(X * 100) / 100,
|
|
61
|
+
Math.round(Y * 100) / 100,
|
|
62
|
+
Z
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Converts world lat,lon coordinates to x,y coordinates within a specific image
|
|
68
|
+
* @param {Object} image_data - skraafoto-stac-api image data
|
|
69
|
+
* @param {Number} Y - northing
|
|
70
|
+
* @param {Number} X - easting
|
|
71
|
+
* @param {Number} [Z] - elevation (geoide)
|
|
72
|
+
* @returns {array} [x,y] Column/row image coordinate
|
|
73
|
+
*/
|
|
74
|
+
function getImageXY(image_data, X, Y, Z = 0) {
|
|
75
|
+
|
|
76
|
+
// constants pulled from image_data
|
|
77
|
+
const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
|
|
78
|
+
const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
|
|
79
|
+
const ci = image_data.properties['pers:interior_orientation'].focal_length
|
|
80
|
+
const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
|
|
81
|
+
const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
|
|
82
|
+
const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
|
|
83
|
+
const X0 = image_data.properties['pers:perspective_center'][0]
|
|
84
|
+
const Y0 = image_data.properties['pers:perspective_center'][1]
|
|
85
|
+
const Z0 = image_data.properties['pers:perspective_center'][2]
|
|
86
|
+
const Ome = image_data.properties['pers:omega']
|
|
87
|
+
const Phi = image_data.properties['pers:phi']
|
|
88
|
+
const Kap = image_data.properties['pers:kappa']
|
|
89
|
+
|
|
90
|
+
// recalc values
|
|
91
|
+
var c = ci*(-1)
|
|
92
|
+
var dimX = dimXi*pix/2*(-1)
|
|
93
|
+
var dimY = dimYi*pix/2*(-1)
|
|
94
|
+
|
|
95
|
+
// ... Do calculations ...
|
|
96
|
+
var o = radians(Ome)
|
|
97
|
+
var p = radians(Phi)
|
|
98
|
+
var k = radians(Kap)
|
|
99
|
+
var D11 = Math.cos(p) * Math.cos(k)
|
|
100
|
+
var D12 = - Math.cos(p) * Math.sin(k)
|
|
101
|
+
var D13 = Math.sin(p)
|
|
102
|
+
var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
|
|
103
|
+
var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
|
|
104
|
+
var D23 = - Math.sin(o) * Math.cos(p)
|
|
105
|
+
var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
|
|
106
|
+
var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
|
|
107
|
+
var D33 = Math.cos(o) * Math.cos(p)
|
|
108
|
+
|
|
109
|
+
var x_dot = (-1)*c*((D11*(X-X0)+D21*(Y-Y0)+D31*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
|
|
110
|
+
var y_dot = (-1)*c*((D12*(X-X0)+D22*(Y-Y0)+D32*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
|
|
111
|
+
|
|
112
|
+
var col = ((x_dot-xx0)+(dimX))*(-1)/pix
|
|
113
|
+
var row = ((y_dot-yy0)+(dimY))*(-1)/pix
|
|
114
|
+
|
|
115
|
+
return [
|
|
116
|
+
Math.round(col),
|
|
117
|
+
Math.round(row)
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* (DEPRECATED - Use getImageXY) Converts lat,lon coordinates to x,y coordinates within a specific image
|
|
123
|
+
* @param {Object} image_data - skraafoto-stac-api image data
|
|
124
|
+
* @param {Number} Y - northing
|
|
125
|
+
* @param {Number} X - easting
|
|
126
|
+
* @param {Number} [Z] - elevation (geoide)
|
|
127
|
+
* @returns {array} [x,y] Column/row image coordinate
|
|
128
|
+
*/
|
|
129
|
+
function world2image(image_data, X, Y, Z = 0) {
|
|
130
|
+
return getImageXY(image_data, X, Y, Z)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Converts degress to radians */
|
|
134
|
+
function radians(degrees) {
|
|
135
|
+
return degrees * (Math.PI / 180)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
139
|
+
function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
140
|
+
|
|
141
|
+
// Get coordinates based on current z value
|
|
142
|
+
const worldcoor = image2world(image_data,col,row,z)
|
|
143
|
+
|
|
144
|
+
i--
|
|
145
|
+
if (i <= 0) {
|
|
146
|
+
// max iterations reached
|
|
147
|
+
return [worldcoor, 0, count]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Get new z value from coordinates
|
|
151
|
+
return getZ(worldcoor[0],worldcoor[1], auth).then((new_z) => {
|
|
152
|
+
|
|
153
|
+
// How big is the different between z and new z?
|
|
154
|
+
const delta = Math.abs(new_z - z)
|
|
155
|
+
|
|
156
|
+
if (delta >= limit) {
|
|
157
|
+
// If the difference is too big, try building coordinates with the new z
|
|
158
|
+
count = count + 1
|
|
159
|
+
return iterateRecursive(image_data, col, row, new_z, count, limit, auth, i)
|
|
160
|
+
} else {
|
|
161
|
+
// If the difference is small, return coordinates
|
|
162
|
+
return [worldcoor, delta, count]
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* (DEPRECATED - Use getWorldXYZ) Tries to guess world coordinate for a pixel position within an image using STAC API image data and requests to DHM elevation data.
|
|
169
|
+
* @param {object} image_data - image item data from STAC API
|
|
170
|
+
* @param {number} col - image x coordinate (from left to right)
|
|
171
|
+
* @param {number} row - image y coordinate (from top to bottom)
|
|
172
|
+
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
173
|
+
* @param {number} [limit] - result may be inaccurate within this limit. Default is 1.
|
|
174
|
+
* @returns {array} [world coordindates (array), elevation discrepancy, calculation iterations]
|
|
175
|
+
*/
|
|
176
|
+
function iterate(image_data, col, row, auth, limit = 1) {
|
|
177
|
+
let i = 20 // Maximum number of iterations allowed
|
|
178
|
+
return iterateRecursive(image_data, col, row, 0.5, 0, limit, auth, i)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function compareZ(options) {
|
|
182
|
+
|
|
183
|
+
// Get coordinates based on current z value
|
|
184
|
+
const world_xyz = image2world(options.image, options.xy[0], options.xy[1], options.z)
|
|
185
|
+
// Get new z value from coordinates
|
|
186
|
+
world_xyz[2] = await getElevation(world_xyz[0], world_xyz[1], options.terrain)
|
|
187
|
+
// How big is the different between z and new z?
|
|
188
|
+
const delta = Math.abs(world_xyz[2] - options.z)
|
|
189
|
+
|
|
190
|
+
// Check delta vs. limit. If the difference is too big, try building coordinates with the new z
|
|
191
|
+
// Make sure the recursive loop ends after 20 iterations even if no precise z is found
|
|
192
|
+
if (options.iteration < 20 && delta > options.limit) {
|
|
193
|
+
options.iteration++
|
|
194
|
+
options.z = world_xyz[2]
|
|
195
|
+
return compareZ(options)
|
|
196
|
+
} else {
|
|
197
|
+
return world_xyz
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Find world coordinate and elevation that correspond to image x,y. Uses image item information from STAC API and a GeoTIFF terrain model from Datafordeler.
|
|
202
|
+
* @param {object} options
|
|
203
|
+
* @param {array} options.xy - image coordinates
|
|
204
|
+
* @param {object} options.image - STAC API item
|
|
205
|
+
* @param {object} options.terrain - GeoTIFF object from getTerrainGeoTIFF() output
|
|
206
|
+
* @param {number} precision - Precision in meters for "good enough"
|
|
207
|
+
* @returns {array} Array with world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
|
|
208
|
+
*/
|
|
209
|
+
async function getWorldXYZ(options, precision = 0.3) {
|
|
210
|
+
|
|
211
|
+
const best_world_xyz = compareZ({
|
|
212
|
+
image: options.image,
|
|
213
|
+
terrain: options.terrain,
|
|
214
|
+
xy: options.xy,
|
|
215
|
+
z: 0,
|
|
216
|
+
limit: precision,
|
|
217
|
+
iteration: 1
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return best_world_xyz
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Iterates over an array of STAC API image items
|
|
226
|
+
* and returns a total bounding box for all the images
|
|
227
|
+
*/
|
|
228
|
+
function getTotalBbox(images) {
|
|
229
|
+
const initialBbox = images[0].features[0].bbox
|
|
230
|
+
|
|
231
|
+
return images.reduce((bbox, image) => {
|
|
232
|
+
const imageBbox = image.features[0].bbox
|
|
233
|
+
if (imageBbox[0] < bbox[0]) {
|
|
234
|
+
bbox[0] = imageBbox[0]
|
|
235
|
+
}
|
|
236
|
+
if (imageBbox[1] < bbox[1]) {
|
|
237
|
+
bbox[1] = imageBbox[1]
|
|
238
|
+
}
|
|
239
|
+
if (imageBbox[2] > bbox[2]) {
|
|
240
|
+
bbox[2] = imageBbox[2]
|
|
241
|
+
}
|
|
242
|
+
if (imageBbox[3] > bbox[3]) {
|
|
243
|
+
bbox[3] = imageBbox[3]
|
|
244
|
+
}
|
|
245
|
+
return bbox
|
|
246
|
+
}, initialBbox)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export {
|
|
250
|
+
image2world,
|
|
251
|
+
world2image,
|
|
252
|
+
getWorldXYZ,
|
|
253
|
+
getImageXY,
|
|
254
|
+
iterate,
|
|
255
|
+
getTotalBbox
|
|
256
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import auth from '../config.js'
|
|
4
4
|
|
|
5
5
|
import assert from 'assert'
|
|
6
|
-
import { getElevation, get, getTerrainGeoTIFF, getZ, getWorldXYZ, world2image } from '../index.js'
|
|
6
|
+
import { getElevation, get, getTerrainGeoTIFF, getZ, getWorldXYZ, world2image, getTerrainByBbox } from '../index.js'
|
|
7
7
|
|
|
8
8
|
console.log('---------------')
|
|
9
9
|
console.log('Elevation tests')
|
|
@@ -107,3 +107,12 @@ get(url_stac)
|
|
|
107
107
|
.catch((err) => {
|
|
108
108
|
console.log('THE ERROR', err)
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
// Testing getTerrainByBbox()
|
|
112
|
+
try {
|
|
113
|
+
const gtiff = await getTerrainByBbox([542929.6729020511, 6153925.479277819, 544463.7189110086, 6155242.76541582], auth)
|
|
114
|
+
assert(gtiff.fileDirectory.ImageWidth === 500, 'GeoTIFF has weird dimensions')
|
|
115
|
+
console.log('Test getTerrainByBbox() => OK')
|
|
116
|
+
} catch(error) {
|
|
117
|
+
console.error(error)
|
|
118
|
+
}
|
package/example.html
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width">
|
|
6
|
-
<title>Saul modules example page</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
|
|
10
|
-
<h1>Saul modules example</h1>
|
|
11
|
-
|
|
12
|
-
<p>Just opening this example.html file might not work becuase of CORS restrictions when trying to access STAC API.</p>
|
|
13
|
-
<p>You should run this HTML file from a simple server like Python's <code>http.server</code></p>
|
|
14
|
-
|
|
15
|
-
<h2>This is the result</h2>
|
|
16
|
-
<div id="result"></div>
|
|
17
|
-
|
|
18
|
-
<script type="module">
|
|
19
|
-
|
|
20
|
-
// You should add your own config.js with proper tokens, etc.
|
|
21
|
-
// Check config.js.example for info on how to set it up
|
|
22
|
-
import auth from './config.js'
|
|
23
|
-
|
|
24
|
-
import {image2world, world2image, getZ, iterate} from './modules/saul-core.js'
|
|
25
|
-
import {getSTAC} from './modules/api.js'
|
|
26
|
-
|
|
27
|
-
let xcoor = 579782.75
|
|
28
|
-
let ycoor = 6131096.03
|
|
29
|
-
|
|
30
|
-
const result_display_element = document.getElementById('result')
|
|
31
|
-
let apidata = await getSTAC('/collections/skraafotos2019/items/2019_83_37_2_0046_00001113', auth)
|
|
32
|
-
|
|
33
|
-
// Let's call an imported method
|
|
34
|
-
let zcoor = await getZ(xcoor, ycoor, auth)
|
|
35
|
-
|
|
36
|
-
let image_coor = world2image(apidata, xcoor, ycoor, zcoor)
|
|
37
|
-
|
|
38
|
-
let col = image_coor[0]
|
|
39
|
-
let row = image_coor[1]
|
|
40
|
-
|
|
41
|
-
let world_coor = image2world(apidata, col, row, zcoor)
|
|
42
|
-
|
|
43
|
-
col = 7375
|
|
44
|
-
row = 5382
|
|
45
|
-
let ite = await iterate(apidata, col, row, auth, 0.05)
|
|
46
|
-
|
|
47
|
-
// Write out result for the user to see
|
|
48
|
-
if (image_coor) {
|
|
49
|
-
result.innerText = xcoor + ' ' + ycoor + ' ' + zcoor + '\n' + image_coor[0] + ' ' + image_coor[1] + '\n' +ite
|
|
50
|
-
} else {
|
|
51
|
-
result.innerText = 'There was no result'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</script>
|
|
56
|
-
|
|
57
|
-
</body>
|
|
58
|
-
</html>
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps a coordinate from the one coordinate system to another based on GeoTiff image dimensions.
|
|
3
|
-
* @param {Array} coord - EPSG:25832 X,Y coordinate pair
|
|
4
|
-
* @param {Array} bbox - Bounding box within input coordinate systtem. Consists of 2 EPSG:25832 coordinate pairs. Example: [x1,y1,x2,y2]
|
|
5
|
-
* @param {Array} imageDimensions - Width and height of a GeoTiff image. This will set the bounding box of the output coordinate system. Note: This coordinate system will have 0,0 set in the top right position of the image.
|
|
6
|
-
*/
|
|
7
|
-
function convertCoordinate(coord, bbox1, imageDimensions) {
|
|
8
|
-
|
|
9
|
-
// Extract the coordinates from the bounding boxes
|
|
10
|
-
const [x1_min, y1_min, x1_max, y1_max] = bbox1
|
|
11
|
-
|
|
12
|
-
// Extract the coordinate to convert
|
|
13
|
-
const [x1, y1] = coord
|
|
14
|
-
|
|
15
|
-
// Calculate the width and height of both bounding boxes
|
|
16
|
-
const width1 = x1_max - x1_min
|
|
17
|
-
const height1 = y1_max - y1_min
|
|
18
|
-
const width2 = imageDimensions[0]
|
|
19
|
-
const height2 = imageDimensions[1]
|
|
20
|
-
|
|
21
|
-
// Calculate the relative position in the first coordinate system
|
|
22
|
-
const x_relative = (x1 - x1_min) / width1
|
|
23
|
-
const y_relative = (y1 - y1_min) / height1
|
|
24
|
-
|
|
25
|
-
// Calculate the new position in the second coordinate system
|
|
26
|
-
const x2 = Math.abs(x_relative * width2)
|
|
27
|
-
const y2 = height2 - Math.abs(y_relative * height2)
|
|
28
|
-
|
|
29
|
-
return [x2, y2]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Calculates nearest elevation for given coordinate using geoTIFF terrain data
|
|
34
|
-
* @param {number} x - EPSG:25832 X coordinate
|
|
35
|
-
* @param {number} y - EPSG:25832 Y coordinate
|
|
36
|
-
* @param {object} geoTiff - GeoTiff data output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
|
|
37
|
-
* @returns {number} Elevation in meters
|
|
38
|
-
*/
|
|
39
|
-
async function getElevation(x, y, geoTiff) {
|
|
40
|
-
|
|
41
|
-
const bbox = geoTiff.getBoundingBox()
|
|
42
|
-
const height = geoTiff.getHeight()
|
|
43
|
-
const width = geoTiff.getWidth()
|
|
44
|
-
|
|
45
|
-
// Convert lat/lon to geotiff x/y
|
|
46
|
-
const [xPx, yPx] = convertCoordinate([x,y], bbox, [width, height])
|
|
47
|
-
|
|
48
|
-
// Fetch window of raster values around x/y
|
|
49
|
-
const window = [
|
|
50
|
-
Math.floor(xPx),
|
|
51
|
-
Math.floor(yPx),
|
|
52
|
-
Math.ceil(xPx),
|
|
53
|
-
Math.ceil(yPx)
|
|
54
|
-
]
|
|
55
|
-
|
|
56
|
-
// Fetch elevation value from geoTiff rasters
|
|
57
|
-
const elevation_data = await geoTiff.readRasters({
|
|
58
|
-
window: window,
|
|
59
|
-
fillValue: 0
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
return elevation_data[0][0]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Creates an ASCII visualization of a GeoTiff with terrain elevation data. Useful for display in a terminal.
|
|
66
|
-
* NOTE: You may want to use a downsmapled GeoTiff to fit within your terminal window. A GeoTiff pixel is represented by 3 characters each.
|
|
67
|
-
* @param {object} gTiff - GeoTiff output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
|
|
68
|
-
*/
|
|
69
|
-
async function visualizeGeotiff(gTiff) {
|
|
70
|
-
console.log('--- GTIFF visualization ---')
|
|
71
|
-
const float32Arr = await gTiff.readRasters()
|
|
72
|
-
const tiffWidth = gTiff.getWidth()
|
|
73
|
-
const rasters = float32Arr[0]
|
|
74
|
-
let lines = []
|
|
75
|
-
let line = ''
|
|
76
|
-
for (let i = 0; i < rasters.length; i++) {
|
|
77
|
-
if (i % tiffWidth === 0) {
|
|
78
|
-
lines.push(line)
|
|
79
|
-
line = ''
|
|
80
|
-
}
|
|
81
|
-
line += zeroPadNumber(rasters[i])
|
|
82
|
-
}
|
|
83
|
-
for (const l of lines) {
|
|
84
|
-
console.log(l)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Converts numbers to strings in a 3-letter format. Ex. `002`, `0-3` (for negative numbers), or `123` */
|
|
89
|
-
function zeroPadNumber(input) {
|
|
90
|
-
if (input === 0) {
|
|
91
|
-
return "..."
|
|
92
|
-
} else if (input < 0) {
|
|
93
|
-
return `0${input.toFixed(0)}`
|
|
94
|
-
} else if (input < 10) {
|
|
95
|
-
return `00${Math.floor(input)}`
|
|
96
|
-
} else if (input < 100) {
|
|
97
|
-
return `0${Math.floor(input)}`
|
|
98
|
-
} else {
|
|
99
|
-
return `${Math.floor(input)}`
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export {
|
|
104
|
-
getElevation,
|
|
105
|
-
visualizeGeotiff
|
|
106
|
-
}
|
|
File without changes
|