@dataforsyningen/saul 3.0.2 → 3.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 +8 -2
- package/modules/api.js +14 -7
- package/modules/saul-core.js +96 -3
- package/modules/saul-elevation.js +68 -0
- package/package.json +3 -2
- package/test/saul-elevation.test.js +80 -0
package/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
export {
|
|
4
4
|
image2world,
|
|
5
5
|
world2image,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
getWorldXYZ,
|
|
7
|
+
getZ, // Deprecated. Use getElevation
|
|
8
|
+
iterate // Deprecated. Use getWorldXYZ
|
|
8
9
|
} from './modules/saul-core.js'
|
|
9
10
|
|
|
10
11
|
export {
|
|
@@ -19,3 +20,8 @@ export {
|
|
|
19
20
|
epsg25832proj,
|
|
20
21
|
createTranslator
|
|
21
22
|
} from './modules/saul-projection.js'
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
getTerrainGeoTIFF,
|
|
26
|
+
getElevation
|
|
27
|
+
} from './modules/saul-elevation.js'
|
package/modules/api.js
CHANGED
|
@@ -62,24 +62,31 @@ function getErrorMsg() {
|
|
|
62
62
|
return error_msg
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** Returns
|
|
66
|
-
function HttpResponseHandler(response) {
|
|
65
|
+
/** Returns response data */
|
|
66
|
+
function HttpResponseHandler(response, is_json) {
|
|
67
67
|
if (!response.ok) {
|
|
68
68
|
error_msg = response.status
|
|
69
69
|
interruptLoading()
|
|
70
|
-
throw new Error(`HTTP error!
|
|
70
|
+
throw new Error(`HTTP error! ${ response.status }`)
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if (is_json) {
|
|
73
|
+
// We assume the returned data is JSON
|
|
74
|
+
return response.json()
|
|
75
|
+
} else {
|
|
76
|
+
// Return whatever and let someone else worry about parsing it
|
|
77
|
+
return response
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
/**
|
|
77
83
|
* GET HTTP responsee from API
|
|
78
84
|
* @param {string} url - API service URL, including endpoint paths and query parameters.
|
|
79
85
|
* @param {object} [config] - Custom request configs. See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options
|
|
86
|
+
* @param {boolean} [is_json] - `true` if requested output is JSON
|
|
80
87
|
* @returns {object} response object
|
|
81
88
|
*/
|
|
82
|
-
function get(url, config = {}) {
|
|
89
|
+
function get(url, config = {}, is_json = true) {
|
|
83
90
|
if (!url) {
|
|
84
91
|
console.error('Could not fetch data. Missing API URL')
|
|
85
92
|
} else {
|
|
@@ -89,7 +96,7 @@ function get(url, config = {}) {
|
|
|
89
96
|
method: 'GET'
|
|
90
97
|
})
|
|
91
98
|
.then((response) => {
|
|
92
|
-
return HttpResponseHandler(response)
|
|
99
|
+
return HttpResponseHandler(response, is_json)
|
|
93
100
|
})
|
|
94
101
|
.then((response) => {
|
|
95
102
|
// Finally, return the parsed JSON response
|
package/modules/saul-core.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getDHM } from './api.js'
|
|
6
|
+
import { getElevation } from './saul-elevation.js'
|
|
7
|
+
|
|
8
|
+
let iterations
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* Converts x,y coordinates from an image to real world lat,lon coordinates
|
|
@@ -117,7 +120,7 @@ function radians(degrees) {
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/**
|
|
120
|
-
* Fetches elevation based on X,Y coordinates using DHM/Koter endpoint
|
|
123
|
+
* (DEPRECATED - Use getElevation) Fetches elevation based on X,Y coordinates using DHM/Koter endpoint
|
|
121
124
|
* @param {number} xcoor - EPSG:25832 X coordinate
|
|
122
125
|
* @param {number} ycoor - EPSG:25832 Y coordinate
|
|
123
126
|
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
@@ -129,7 +132,7 @@ async function getZ(xcoor, ycoor, auth) {
|
|
|
129
132
|
return z
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
/** Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
135
|
+
/** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
133
136
|
function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
134
137
|
|
|
135
138
|
// Get coordinates based on current z value
|
|
@@ -159,7 +162,7 @@ function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
/**
|
|
162
|
-
* Tries to guess world coordinate for a pixel position within an image using STAC API image data and requests to DHM elevation data.
|
|
165
|
+
* (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.
|
|
163
166
|
* @param {object} image_data - image item data from STAC API
|
|
164
167
|
* @param {number} col - image x coordinate (from left to right)
|
|
165
168
|
* @param {number} row - image y coordinate (from top to bottom)
|
|
@@ -172,8 +175,98 @@ function iterate(image_data, col, row, auth, limit = 1) {
|
|
|
172
175
|
return iterateRecursive(image_data, col, row, 0.5, 0, limit, auth, i)
|
|
173
176
|
}
|
|
174
177
|
|
|
178
|
+
/** 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.
|
|
179
|
+
* @param {object} options
|
|
180
|
+
* @param {array} options.xy - image coordinates
|
|
181
|
+
* @param {object} options.image - STAC API item
|
|
182
|
+
* @param {object} options.terrain - GeoTIFF object from getTerrainGeoTIFF() output
|
|
183
|
+
* @returns {array} Array with world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
|
|
184
|
+
*/
|
|
185
|
+
async function getWorldXYZ(options) {
|
|
186
|
+
|
|
187
|
+
iterations = 1
|
|
188
|
+
|
|
189
|
+
const world_xyz = image2world(options.image, options.xy[0], options.xy[1], 0)
|
|
190
|
+
const better_world_xyz = await compareXYZ({
|
|
191
|
+
xy: options.xy,
|
|
192
|
+
image: options.image,
|
|
193
|
+
terrain: options.terrain,
|
|
194
|
+
world_xyz: Array.from(world_xyz),
|
|
195
|
+
limit: 0.5
|
|
196
|
+
})
|
|
197
|
+
return better_world_xyz
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Adjusts elevation value if values are too far apart
|
|
202
|
+
* @param {number} z - elevation
|
|
203
|
+
* @param {number} delta_y - discrepancy value
|
|
204
|
+
* @returns {number} Adjusted elevation number
|
|
205
|
+
*/
|
|
206
|
+
function adjustZ(z, delta_y) {
|
|
207
|
+
let new_z
|
|
208
|
+
if (delta_y < 0) {
|
|
209
|
+
new_z = z + 0.1
|
|
210
|
+
} else {
|
|
211
|
+
new_z = z - 0.1
|
|
212
|
+
}
|
|
213
|
+
console.log('z', new_z)
|
|
214
|
+
return new_z
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Compares differences i x and y values
|
|
219
|
+
* @param {number} delta_x - x difference
|
|
220
|
+
* @param {number} delta_y - y difference
|
|
221
|
+
* @param {number} limit - difference may be within this limit
|
|
222
|
+
* @returns {boolean} Returns true if differences are within accepted limit
|
|
223
|
+
*/
|
|
224
|
+
function compareXY(delta_x, delta_y, limit) {
|
|
225
|
+
if (Math.abs(delta_x) > limit || Math.abs(delta_y) > limit) {
|
|
226
|
+
return false
|
|
227
|
+
} else {
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Iterates world coordinate calculations in order to get a more precise result
|
|
234
|
+
* @param {object} options
|
|
235
|
+
* @param {array} options.xy - image coordinates
|
|
236
|
+
* @param {object} options.image - STAC API item
|
|
237
|
+
* @param {object} options.terrain - GeoTIFF object from getTerrainGeoTIFF() output
|
|
238
|
+
* @param {array} options.world_xy - Array with (currently guessed) world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
|
|
239
|
+
* @param {number} options.limit - Return when two guesses are within this limit
|
|
240
|
+
* @returns {array} Array with world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
|
|
241
|
+
*/
|
|
242
|
+
async function compareXYZ(options) {
|
|
243
|
+
|
|
244
|
+
iterations++
|
|
245
|
+
if (iterations > 50) {
|
|
246
|
+
console.error('too many iterations')
|
|
247
|
+
return false
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const z = await getElevation(options.xy[0], options.xy[1], options.terrain)
|
|
251
|
+
const world_xyz = image2world(options.image, options.xy[0], options.xy[1], z)
|
|
252
|
+
const delta_x = world_xyz[0] - options.world_xyz[0]
|
|
253
|
+
const delta_y = world_xyz[1] - options.world_xyz[1]
|
|
254
|
+
|
|
255
|
+
if (!compareXY(delta_x, delta_y, options.limit)) {
|
|
256
|
+
|
|
257
|
+
const new_options = Object.assign({},options)
|
|
258
|
+
new_options.world_xyz = Array.from(world_xyz)
|
|
259
|
+
new_options.world_xyz[2] = adjustZ(world_xyz[2], delta_x, delta_y)
|
|
260
|
+
return await compareXYZ(new_options)
|
|
261
|
+
|
|
262
|
+
} else {
|
|
263
|
+
return world_xyz
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
175
267
|
export {
|
|
176
268
|
image2world,
|
|
269
|
+
getWorldXYZ,
|
|
177
270
|
world2image,
|
|
178
271
|
getZ,
|
|
179
272
|
iterate
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fromArrayBuffer } from 'geotiff'
|
|
2
|
+
import { get } from './api.js'
|
|
3
|
+
|
|
4
|
+
/** Fetches a GeoTIFF with elevation data matching the bounding box of a STAC API item (image)
|
|
5
|
+
* @param {object} stac_item - STAC API item from a featureCollection request
|
|
6
|
+
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
7
|
+
* @param {number} [fidelity] - Resolution fidelity. Higher number means more pixels and better precision. Between 1 and 0.01.
|
|
8
|
+
* @returns {object} GeoTiff data
|
|
9
|
+
*/
|
|
10
|
+
function getTerrainGeoTIFF(stac_item, auth, fidelity = 0.05) {
|
|
11
|
+
|
|
12
|
+
const bbox = stac_item.bbox
|
|
13
|
+
const width = Math.round( stac_item.properties['proj:shape'][0] * fidelity )
|
|
14
|
+
const height = Math.round( stac_item.properties['proj:shape'][1] * fidelity )
|
|
15
|
+
|
|
16
|
+
// DHM data endpoint
|
|
17
|
+
let url = 'https://services.datafordeler.dk/DHMNedboer/dhm_wcs/1.0.0/WCS'
|
|
18
|
+
url += '?SERVICE=WCS&COVERAGE=dhm_terraen&RESPONSE_CRS=epsg:25832&CRS=epsg:25832&FORMAT=GTiff&REQUEST=GetCoverage&VERSION=1.0.0'
|
|
19
|
+
url += `&username=${ auth.API_DHM_USERNAME }&password=${ auth.API_DHM_PASSWORD }` // TODO: Should be auth stuff
|
|
20
|
+
url += `&height=${ height }`
|
|
21
|
+
url += `&width=${ width }`
|
|
22
|
+
url += `&bbox=${ bbox[0]},${ bbox[1]},${ bbox[2]},${ bbox[3]}`
|
|
23
|
+
|
|
24
|
+
return get(url, {}, false)
|
|
25
|
+
.then((response) => {
|
|
26
|
+
return response.arrayBuffer()
|
|
27
|
+
})
|
|
28
|
+
.then((arrayBuffer) => {
|
|
29
|
+
return consumeGeoTIFF(arrayBuffer)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function consumeGeoTIFF(raw_data) {
|
|
34
|
+
const tiff = await fromArrayBuffer(raw_data)
|
|
35
|
+
const image = await tiff.getImage()
|
|
36
|
+
return image
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Calculates nearest elevation for given coordinate using geoTIFF terrain data
|
|
40
|
+
* Heavily inspired by https://towardsdatascience.com/geotiff-coordinate-querying-with-javascript-5e6caaaf88cf
|
|
41
|
+
* @param {number} x - EPSG:25832 X coordinate
|
|
42
|
+
* @param {number} y - EPSG:25832 Y coordinate
|
|
43
|
+
* @param {object} geoTiff - GeoTiff data output from getTerrainGeoTIFF() method
|
|
44
|
+
* @returns {number} Elevation in meters
|
|
45
|
+
*/
|
|
46
|
+
async function getElevation(x, y, geoTiff) {
|
|
47
|
+
|
|
48
|
+
const bbox = geoTiff.getBoundingBox()
|
|
49
|
+
const height = geoTiff.getHeight()
|
|
50
|
+
const width = geoTiff.getWidth()
|
|
51
|
+
const bboxWidth = bbox[2] - bbox[0]
|
|
52
|
+
const bboxHeight = bbox[3] - bbox[1]
|
|
53
|
+
|
|
54
|
+
const widthPct = ( x - bbox[0] ) / bboxWidth
|
|
55
|
+
const heightPct = ( y - bbox[1] ) / bboxHeight
|
|
56
|
+
const xPx = Math.floor( width * widthPct )
|
|
57
|
+
const yPx = Math.floor( height * ( 1 - heightPct ) )
|
|
58
|
+
|
|
59
|
+
const window = [ xPx, yPx, xPx + 1, yPx + 1 ]
|
|
60
|
+
const elevation_data = await geoTiff.readRasters( {window} )
|
|
61
|
+
|
|
62
|
+
return Math.round( elevation_data[0][0] * 10) / 10
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
getTerrainGeoTIFF,
|
|
67
|
+
getElevation
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataforsyningen/saul",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "This is a collection of stand-alone methods for photogrammetry calculations and API interactions for 'Danske Skråfotos'.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "echo 'TESTS WILL ONLY RUN UNDER NODE v18' && node ./test/saul-projection.test.js && node ./test/saul-core.test.js",
|
|
8
|
+
"test": "echo 'TESTS WILL ONLY RUN UNDER NODE v18' && node ./test/saul-projection.test.js && node ./test/saul-core.test.js && node ./test/saul-elevation.test.js",
|
|
9
9
|
"docs": "jsdoc ./modules/*.js --destination docs"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"jsdoc": "^3.6.10"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"geotiff": "^2.0.5",
|
|
30
31
|
"proj4": "^2.8.0"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// You should add your own config.js with proper tokens, passwords, etc.
|
|
2
|
+
// Check config.js.example for info on how to set it up
|
|
3
|
+
import auth from '../config.js'
|
|
4
|
+
|
|
5
|
+
import assert from 'assert'
|
|
6
|
+
import { getTerrainGeoTIFF, getElevation } from '../modules/saul-elevation.js'
|
|
7
|
+
import { get } from '../modules/api.js'
|
|
8
|
+
import { getZ, getWorldXYZ } from '../modules/saul-core.js'
|
|
9
|
+
|
|
10
|
+
// Vars
|
|
11
|
+
const stac_item = '2021_83_29_2_0019_00003995'
|
|
12
|
+
const fidelity = 0.05 // Higher number means more points and better precision
|
|
13
|
+
const max_deviation = 0.5
|
|
14
|
+
|
|
15
|
+
// STAC API endpoint
|
|
16
|
+
let url_stac = 'https://api.dataforsyningen.dk/skraafotoapi_test/search?limit=1&crs=http://www.opengis.net/def/crs/EPSG/0/25832&token=9b554b6c854184c3b0f377ffc7481585'
|
|
17
|
+
url_stac += `&ids=${ stac_item }`
|
|
18
|
+
|
|
19
|
+
function is_equalIsh(num1, num2) {
|
|
20
|
+
if (Math.abs(num1 - num2) > max_deviation) {
|
|
21
|
+
return false
|
|
22
|
+
} else {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getRandomCoordinate(bbox) {
|
|
28
|
+
const x = bbox[0] + Math.random() * (bbox[2] - bbox[0])
|
|
29
|
+
const y = bbox[1] + Math.random() * (bbox[3] - bbox[1])
|
|
30
|
+
return [x,y]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function testDataAnumberOfTimes(data, bbox, times) {
|
|
34
|
+
for (let i = 1; i <= times; i++) {
|
|
35
|
+
const world1 = getRandomCoordinate(bbox)
|
|
36
|
+
compareElevations(world1[0], world1[1], data)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function compareElevations(x,y,geotiff) {
|
|
41
|
+
getElevation(x, y, geotiff)
|
|
42
|
+
.then(elevation => {
|
|
43
|
+
getZ(x, y, auth)
|
|
44
|
+
.then(getz_e => {
|
|
45
|
+
assert(is_equalIsh(getz_e, elevation), `Elevations ${elevation} / ${getz_e} at ${x} ${y} do not match`)
|
|
46
|
+
console.log(`Elevation at ${ x } ${ y } with delta ${Math.abs(elevation - getz_e).toFixed(2)} OK`)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Test getTerrainGeoTIFF and getElevation with a STAC API item
|
|
52
|
+
get(url_stac)
|
|
53
|
+
.then((json) => {
|
|
54
|
+
|
|
55
|
+
console.log('Testing getTerrainGeoTIFF and getElevation')
|
|
56
|
+
|
|
57
|
+
const width = Math.round( json.features[0].properties['proj:shape'][0] * fidelity )
|
|
58
|
+
const height = Math.round( json.features[0].properties['proj:shape'][1] * fidelity )
|
|
59
|
+
|
|
60
|
+
console.info('fetching', width * height, 'data points as GeoTiff image')
|
|
61
|
+
|
|
62
|
+
getTerrainGeoTIFF(json.features[0], auth, fidelity)
|
|
63
|
+
.then(data => {
|
|
64
|
+
|
|
65
|
+
testDataAnumberOfTimes(data, json.features[0].bbox, 5)
|
|
66
|
+
|
|
67
|
+
getWorldXYZ({
|
|
68
|
+
xy: [1,1],
|
|
69
|
+
image: json.features[0],
|
|
70
|
+
terrain: data
|
|
71
|
+
}).then(world_xy => {
|
|
72
|
+
compareElevations(world_xy[0], world_xy[1], data) // lower left corner of image
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
})
|
|
78
|
+
.catch((err) => {
|
|
79
|
+
console.log('THE ERROR', err)
|
|
80
|
+
})
|