@dataforsyningen/saul 3.0.2 → 3.1.1
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 +21 -3
- package/modules/saul-elevation.js +86 -0
- package/package.json +3 -2
- package/test/saul-elevation.test.js +107 -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,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getDHM } from './api.js'
|
|
6
|
+
import { getElevation } from './saul-elevation.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Converts x,y coordinates from an image to real world lat,lon coordinates
|
|
@@ -117,7 +118,7 @@ function radians(degrees) {
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
|
-
* Fetches elevation based on X,Y coordinates using DHM/Koter endpoint
|
|
121
|
+
* (DEPRECATED - Use getElevation) Fetches elevation based on X,Y coordinates using DHM/Koter endpoint
|
|
121
122
|
* @param {number} xcoor - EPSG:25832 X coordinate
|
|
122
123
|
* @param {number} ycoor - EPSG:25832 Y coordinate
|
|
123
124
|
* @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 +130,7 @@ async function getZ(xcoor, ycoor, auth) {
|
|
|
129
130
|
return z
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
/** Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
133
|
+
/** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
133
134
|
function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
134
135
|
|
|
135
136
|
// Get coordinates based on current z value
|
|
@@ -159,7 +160,7 @@ function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
* Tries to guess world coordinate for a pixel position within an image using STAC API image data and requests to DHM elevation data.
|
|
163
|
+
* (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
164
|
* @param {object} image_data - image item data from STAC API
|
|
164
165
|
* @param {number} col - image x coordinate (from left to right)
|
|
165
166
|
* @param {number} row - image y coordinate (from top to bottom)
|
|
@@ -172,8 +173,25 @@ function iterate(image_data, col, row, auth, limit = 1) {
|
|
|
172
173
|
return iterateRecursive(image_data, col, row, 0.5, 0, limit, auth, i)
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
/** 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.
|
|
177
|
+
* @param {object} options
|
|
178
|
+
* @param {array} options.xy - image coordinates
|
|
179
|
+
* @param {object} options.image - STAC API item
|
|
180
|
+
* @param {object} options.terrain - GeoTIFF object from getTerrainGeoTIFF() output
|
|
181
|
+
* @returns {array} Array with world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
|
|
182
|
+
*/
|
|
183
|
+
async function getWorldXYZ(options) {
|
|
184
|
+
|
|
185
|
+
const world_xyz = image2world(options.image, options.xy[0], options.xy[1], 0)
|
|
186
|
+
world_xyz[2] = await getElevation(world_xyz[0], world_xyz[1], options.terrain)
|
|
187
|
+
const better_world_xyz = image2world(options.image, options.xy[0], options.xy[1], world_xyz[2])
|
|
188
|
+
|
|
189
|
+
return better_world_xyz
|
|
190
|
+
}
|
|
191
|
+
|
|
175
192
|
export {
|
|
176
193
|
image2world,
|
|
194
|
+
getWorldXYZ,
|
|
177
195
|
world2image,
|
|
178
196
|
getZ,
|
|
179
197
|
iterate
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
function constrainToBbox(bbox, x, y) {
|
|
40
|
+
let new_x = x
|
|
41
|
+
let new_y = y
|
|
42
|
+
if (x < bbox[0]) {
|
|
43
|
+
new_x = bbox[0] + 1
|
|
44
|
+
} else if (bbox[2] < x) {
|
|
45
|
+
new_x = bbox[2] - 1
|
|
46
|
+
}
|
|
47
|
+
if (y < bbox[1]) {
|
|
48
|
+
new_y = bbox[1] + 1
|
|
49
|
+
} else if (bbox[3] < y) {
|
|
50
|
+
new_y = bbox[3] - 1
|
|
51
|
+
}
|
|
52
|
+
return [new_x, new_y]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Calculates nearest elevation for given coordinate using geoTIFF terrain data
|
|
56
|
+
* Heavily inspired by https://towardsdatascience.com/geotiff-coordinate-querying-with-javascript-5e6caaaf88cf
|
|
57
|
+
* @param {number} x - EPSG:25832 X coordinate
|
|
58
|
+
* @param {number} y - EPSG:25832 Y coordinate
|
|
59
|
+
* @param {object} geoTiff - GeoTiff data output from getTerrainGeoTIFF() method
|
|
60
|
+
* @returns {number} Elevation in meters
|
|
61
|
+
*/
|
|
62
|
+
async function getElevation(x, y, geoTiff) {
|
|
63
|
+
|
|
64
|
+
const bbox = geoTiff.getBoundingBox()
|
|
65
|
+
const height = geoTiff.getHeight()
|
|
66
|
+
const width = geoTiff.getWidth()
|
|
67
|
+
const bboxWidth = bbox[2] - bbox[0]
|
|
68
|
+
const bboxHeight = bbox[3] - bbox[1]
|
|
69
|
+
|
|
70
|
+
const xy = constrainToBbox(bbox, x, y)
|
|
71
|
+
|
|
72
|
+
const widthPct = ( xy[0] - bbox[0] ) / bboxWidth
|
|
73
|
+
const heightPct = ( xy[1] - bbox[1] ) / bboxHeight
|
|
74
|
+
const xPx = Math.floor( width * widthPct )
|
|
75
|
+
const yPx = Math.floor( height * ( 1 - heightPct ) )
|
|
76
|
+
|
|
77
|
+
const window = [ xPx, yPx, xPx + 1, yPx + 1 ]
|
|
78
|
+
const elevation_data = await geoTiff.readRasters( {window} )
|
|
79
|
+
|
|
80
|
+
return Math.round( elevation_data[0][0] * 10) / 10
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
getTerrainGeoTIFF,
|
|
85
|
+
getElevation
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataforsyningen/saul",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
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,107 @@
|
|
|
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, world2image } 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, deviation = max_deviation) {
|
|
20
|
+
if (Math.abs(num1 - num2) > 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 testGetElevationAnumberOfTimes(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 getRandomImageXY(image) {
|
|
41
|
+
const shapex = image.properties['proj:shape'][0]
|
|
42
|
+
const shapey = image.properties['proj:shape'][1]
|
|
43
|
+
const x = Math.floor(shapex - Math.random() * shapex)
|
|
44
|
+
const y = Math.floor(shapey - Math.random() * shapey)
|
|
45
|
+
return [x,y]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function testGetWorldXYZAnumberOfTimes(item, terrain, times) {
|
|
49
|
+
for (let i = 1; i <= times; i++) {
|
|
50
|
+
const xy = getRandomImageXY(item)
|
|
51
|
+
getWorldXYZ({
|
|
52
|
+
xy: xy,
|
|
53
|
+
image: item,
|
|
54
|
+
terrain: terrain
|
|
55
|
+
}).then(world_xy => {
|
|
56
|
+
compareElevations(world_xy[0], world_xy[1], terrain) // lower left corner of image
|
|
57
|
+
const image_coords = world2image(item, world_xy[0], world_xy[1], world_xy[2])
|
|
58
|
+
assert(is_equalIsh(image_coords[0], xy[0], 0.75), `Image x coordinates ${image_coords[0]} / ${xy[0]} do not match`)
|
|
59
|
+
assert(is_equalIsh(image_coords[1], xy[1], 0.75), `Image y coordinates ${image_coords[1]} / ${xy[1]} do not match`)
|
|
60
|
+
console.log('getWorldXYZ => world2image OK')
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compareElevations(x,y,geotiff) {
|
|
66
|
+
getElevation(x, y, geotiff)
|
|
67
|
+
.then(elevation => {
|
|
68
|
+
getZ(x, y, auth)
|
|
69
|
+
.then(getz_e => {
|
|
70
|
+
assert(is_equalIsh(getz_e, elevation), `Elevations ${elevation} / ${getz_e} at ${x} ${y} do not match`)
|
|
71
|
+
console.log(`Elevation at ${ x } ${ y } with delta ${Math.abs(elevation - getz_e).toFixed(2)} OK`)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Test getTerrainGeoTIFF and getElevation with a STAC API item
|
|
77
|
+
get(url_stac)
|
|
78
|
+
.then((json) => {
|
|
79
|
+
|
|
80
|
+
const width = Math.round( json.features[0].properties['proj:shape'][0] * fidelity )
|
|
81
|
+
const height = Math.round( json.features[0].properties['proj:shape'][1] * fidelity )
|
|
82
|
+
|
|
83
|
+
console.info('fetching', width * height, 'data points as GeoTiff image')
|
|
84
|
+
|
|
85
|
+
getTerrainGeoTIFF(json.features[0], auth, fidelity)
|
|
86
|
+
.then(data => {
|
|
87
|
+
|
|
88
|
+
// Testing getElevation
|
|
89
|
+
try {
|
|
90
|
+
testGetElevationAnumberOfTimes(data, json.features[0].bbox, 5)
|
|
91
|
+
} catch(error) {
|
|
92
|
+
console.error(error)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Testing getWorldXYZ
|
|
96
|
+
try {
|
|
97
|
+
testGetWorldXYZAnumberOfTimes(json.features[0], data, 10)
|
|
98
|
+
} catch(error) {
|
|
99
|
+
console.error(error)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
})
|
|
105
|
+
.catch((err) => {
|
|
106
|
+
console.log('THE ERROR', err)
|
|
107
|
+
})
|