@dataforsyningen/saul 4.2.0 → 5.0.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/.github/workflows/release-package.yml +11 -9
- package/Dockerfile +13 -0
- package/README.md +7 -2
- package/compose.yaml +10 -0
- package/config.js.example +6 -5
- package/index.js +2 -4
- package/modules/api.js +22 -15
- package/modules/elevation.js +53 -12
- package/modules/image.js +5 -10
- package/package.json +8 -8
- package/test/assert.js +48 -0
- package/test/saul-core.test.js +12 -21
- package/test/saul-elevation.test.js +13 -19
- package/modules/core.js +0 -231
|
@@ -4,14 +4,18 @@ on:
|
|
|
4
4
|
release:
|
|
5
5
|
types: [created]
|
|
6
6
|
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write # Required for OIDC
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
7
11
|
jobs:
|
|
8
12
|
build:
|
|
9
13
|
runs-on: ubuntu-latest
|
|
10
14
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
12
|
-
- uses: actions/setup-node@
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
- uses: actions/setup-node@v6
|
|
13
17
|
with:
|
|
14
|
-
node-version:
|
|
18
|
+
node-version: 26
|
|
15
19
|
- run: npm ci
|
|
16
20
|
# - run: npm test
|
|
17
21
|
|
|
@@ -21,11 +25,9 @@ jobs:
|
|
|
21
25
|
needs: build
|
|
22
26
|
runs-on: ubuntu-latest
|
|
23
27
|
steps:
|
|
24
|
-
- uses: actions/checkout@
|
|
25
|
-
- uses: actions/setup-node@
|
|
28
|
+
- uses: actions/checkout@v6
|
|
29
|
+
- uses: actions/setup-node@v6
|
|
26
30
|
with:
|
|
27
|
-
node-version:
|
|
31
|
+
node-version: 26
|
|
28
32
|
registry-url: https://registry.npmjs.org
|
|
29
|
-
- run: npm publish --access public
|
|
30
|
-
env:
|
|
31
|
-
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
33
|
+
- run: npm stage publish --access public
|
package/Dockerfile
ADDED
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ You can use these as building blocks for your own web application that wants to
|
|
|
11
11
|
npm i --save @dataforsyningen/saul
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
2. Create a configuration file or Javascript object with proper values and make sure to load it into your application before using any of the SAUL utilities. This package ships with an example configuration file at
|
|
14
|
+
2. Create a configuration file or Javascript object with proper values and make sure to load it into your application before using any of the SAUL utilities. This package ships with an example configuration file at [`config.js.example`](config.js.example)
|
|
15
15
|
|
|
16
16
|
## How to use
|
|
17
17
|
|
|
@@ -39,11 +39,16 @@ From within `node_modules/@dataforsyningen/saul` you can run the `test` command
|
|
|
39
39
|
npm run test
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Or if you have Docker, you may run the test in a container:
|
|
43
|
+
```shell
|
|
44
|
+
docker compose run --rm saul
|
|
45
|
+
```
|
|
46
|
+
|
|
42
47
|
## Updating local terrain model
|
|
43
48
|
|
|
44
49
|
The Saul package includes a terrain elevation model hosted as a single GeoTiff file for events where terrain data from Datafordeler service is unavailable. To update the terrain model, simply download a new GeoTiff from Datafordeler with the following URL (supply your own username/password) and save it to `/assets/dk-terrain.tiff` in the package directory.
|
|
45
50
|
|
|
46
|
-
https://
|
|
51
|
+
https://wcs.datafordeler.dk/DHMNedboer/dhm_wcs/1.0.0/WCS?SERVICE=WCS&COVERAGE=dhm_terraen&RESPONSE_CRS=epsg:25832&CRS=epsg:25832&FORMAT=GTiff&REQUEST=GetCoverage&VERSION=1.0.0&apikey=xxx&height=1000&width=1260&bbox=430000,6040000,900000,6413000
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
## Acknowledgements
|
package/compose.yaml
ADDED
package/config.js.example
CHANGED
|
@@ -4,9 +4,10 @@ export default {
|
|
|
4
4
|
API_STAC_TOKEN: "", // Dataforsyningen token
|
|
5
5
|
API_STAC_BASEURL: "https://api.dataforsyningen.dk/rest/skraafoto_api/v2",
|
|
6
6
|
|
|
7
|
-
// DHM URL
|
|
8
|
-
API_DHM_WCS_BASEURL: "https://
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
// DHM WCS URL can be aquired from https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/dhm-wcs/
|
|
8
|
+
API_DHM_WCS_BASEURL: "https://wcs.datafordeler.dk/DHMNedboer/dhm_wcs/1.0.0/WCS",
|
|
9
|
+
// DHM WMS URL can be aquired from https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/dhm-wms/
|
|
10
|
+
API_DHM_WMS_BASEURL: "https://wms.datafordeler.dk/DHMNedboer/dhm/1.0.0/WMS",
|
|
11
|
+
// DHM auth is described at https://confluence.kds.dk/pages/viewpage.action?pageId=187105503#Autentifikationsmetoderp%C3%A5DatafordelerAdministration-API-Key
|
|
12
|
+
API_DHM_KEY: "", // DHM API service key
|
|
12
13
|
}
|
package/index.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// Import/export modules with their public methods
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
|
-
postSTAC,
|
|
5
4
|
getSTAC,
|
|
6
|
-
|
|
7
|
-
get,
|
|
8
|
-
post
|
|
5
|
+
get
|
|
9
6
|
} from './modules/api.js'
|
|
10
7
|
|
|
11
8
|
export {
|
|
@@ -17,6 +14,7 @@ export {
|
|
|
17
14
|
getElevation,
|
|
18
15
|
visualizeGeotiff,
|
|
19
16
|
getZ,
|
|
17
|
+
DHMParseError,
|
|
20
18
|
getTerrainGeoTIFF,
|
|
21
19
|
getDenmarkGeoTiff,
|
|
22
20
|
getTerrainByBbox,
|
package/modules/api.js
CHANGED
|
@@ -62,7 +62,12 @@ function getErrorMsg() {
|
|
|
62
62
|
return error_msg
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Returns response data
|
|
67
|
+
* @param {Response} response
|
|
68
|
+
* @param {boolean} is_json Whether to extract JSON data from the response body.
|
|
69
|
+
* @returns {(Response|Promise.<object>)} The response object or a Promise with the JavaScript representation of the response body JSON.
|
|
70
|
+
*/
|
|
66
71
|
function HttpResponseHandler(response, is_json) {
|
|
67
72
|
if (!response.ok) {
|
|
68
73
|
error_msg = response.status
|
|
@@ -79,12 +84,12 @@ function HttpResponseHandler(response, is_json) {
|
|
|
79
84
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
/**
|
|
83
|
-
* GET HTTP
|
|
84
|
-
* @param {string} url
|
|
85
|
-
* @param {object} [config]
|
|
86
|
-
* @param {boolean} [is_json]
|
|
87
|
-
* @returns {object} response object
|
|
87
|
+
/**
|
|
88
|
+
* GET HTTP response from API
|
|
89
|
+
* @param {string} url API service URL, including endpoint paths and query parameters.
|
|
90
|
+
* @param {object} [config] Custom request config, see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options.
|
|
91
|
+
* @param {boolean} [is_json=true] `true` (default) if the response body JSON shall be converted to JavaScript representation and returned, `false` if the response object shall be returned.
|
|
92
|
+
* @returns {Promise<(Response|object)>} A Promise with the response object or with the JavaScript representation of the response body JSON.
|
|
88
93
|
*/
|
|
89
94
|
function get(url, config = {}, is_json = true) {
|
|
90
95
|
if (!url) {
|
|
@@ -150,16 +155,18 @@ function post(url, requestbody, token) {
|
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
155
|
-
* @param {string} query
|
|
156
|
-
* @param {
|
|
157
|
-
* @
|
|
158
|
+
/**
|
|
159
|
+
* Fetch data from DHM
|
|
160
|
+
* @param {string} query DHM API query. Find details at https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/dhm-wms/
|
|
161
|
+
* @param {object} auth API autentication data. See ../config.js.example for reference.
|
|
162
|
+
* @param {string} auth.API_DHM_WMS_BASEURL The base URL of the DHM WMS endpoint.
|
|
163
|
+
* @param {string} auth.API_DHM_KEY Your API key to access services on Datafordeler.
|
|
164
|
+
* @returns {Promise.<string>} Eventually the response body text.
|
|
158
165
|
*/
|
|
159
166
|
function getDHM(query, auth) {
|
|
160
|
-
const auth_params = `&
|
|
161
|
-
return get(
|
|
162
|
-
|
|
167
|
+
const auth_params = `&apikey=${encodeURIComponent(auth.API_DHM_KEY)}`
|
|
168
|
+
return get(auth.API_DHM_WMS_BASEURL + '?SERVICE=WMS&VERSION=1.3.0' + query + auth_params, { cache: 'force-cache' }, false)
|
|
169
|
+
.then((response) => response.text())
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
/**
|
package/modules/elevation.js
CHANGED
|
@@ -15,7 +15,7 @@ function calcSizeRatio(sizeX, bbox) {
|
|
|
15
15
|
|
|
16
16
|
/** Fetches a GeoTIFF with elevation data matching the bounding box of a STAC API item (image)
|
|
17
17
|
* @param {object} stac_item - STAC API item from a featureCollection request
|
|
18
|
-
* @param {{API_DHM_WCS_BASEURL: string,
|
|
18
|
+
* @param {{API_DHM_WCS_BASEURL: string, API_DHM_KEY: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
19
19
|
* @param {number} [resolution] - Resolution (1 - 0.01). Higher number means more pixels and better precision.
|
|
20
20
|
* @returns {object} GeoTiff data
|
|
21
21
|
*/
|
|
@@ -149,22 +149,62 @@ function zeroPadNumber(input) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/**
|
|
153
|
-
*
|
|
154
|
-
* @
|
|
155
|
-
* @
|
|
156
|
-
* @
|
|
157
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Exception thrown, when the response from DHM cannot be parsed.
|
|
154
|
+
* @extends Error
|
|
155
|
+
* @property {string} [response] The given response text from DHM.
|
|
156
|
+
* @property {RexExp} [pattern] The given pattern used to parse the response text.
|
|
157
|
+
*/
|
|
158
|
+
class DHMParseError extends Error {
|
|
159
|
+
/**
|
|
160
|
+
* Create a parse exception. All arguments are forwarded to Error.
|
|
161
|
+
* @param {string} message Description of exception.
|
|
162
|
+
* @param {object} [options] Standard options parameter to Error, may contain a cause, see Error.
|
|
163
|
+
* @param {string} [options.response] Response text from DHM, that could not be parsed with pattern.
|
|
164
|
+
* @param {RegExp} [options.pattern] The pattern, that did not parse the Response text well.
|
|
165
|
+
*/
|
|
166
|
+
constructor(...params) {
|
|
167
|
+
super(...params);
|
|
168
|
+
if (Error.captureStackTrace) {
|
|
169
|
+
Error.captureStackTrace(this, DHMParseError);
|
|
170
|
+
}
|
|
171
|
+
this.name = "DHMParseError";
|
|
172
|
+
if (params.length > 1 && typeof params[1] == 'object') {
|
|
173
|
+
if (params[1].response)
|
|
174
|
+
this.response = params[1].response;
|
|
175
|
+
if (params[1].pattern)
|
|
176
|
+
this.pattern = params[1].pattern;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetches a single elevation value based on X,Y coordinates using DHM/Koter endpoint.
|
|
183
|
+
* @param {number} xcoor EPSG:25832 X coordinate.
|
|
184
|
+
* @param {number} ycoor EPSG:25832 Y coordinate.
|
|
185
|
+
* @param {object} auth API autentication data. See ../config.js.example for reference.
|
|
186
|
+
* @param {string} auth.API_DHM_WMS_BASEURL The base URL of the DHM WMS endpoint.
|
|
187
|
+
* @param {string} auth.API_DHM_KEY Your API key to access services on Datafordeler.
|
|
188
|
+
* @returns {number} Eventually returns the elevation in meters.
|
|
189
|
+
* @throws {DHMParseError} When unable to parse response from DHM WMS.
|
|
158
190
|
*/
|
|
159
191
|
async function getZ(xcoor, ycoor, auth) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
const request = '&request=GetFeatureInfo&styles=&layers=dhm_terraen&query_layers=dhm_terraen&info_format=text/plain&i=175&j=175&width=351&height=351'
|
|
193
|
+
const bbox = [xcoor - 1, ycoor - 1, xcoor + 1, ycoor + 1]
|
|
194
|
+
const coords = '&crs=EPSG:25832&bbox=' + encodeURIComponent(bbox.join())
|
|
195
|
+
const dhm_text = await getDHM(request + coords, auth)
|
|
196
|
+
const text_begin = /GetFeatureInfo\s+results:\s*Layer\s+'dhm_terraen'\s+Feature\s*0:\s*value_0\s*=\s*'/y
|
|
197
|
+
if (text_begin.test(dhm_text)) {
|
|
198
|
+
const z = parseFloat(dhm_text.slice(text_begin.lastIndex))
|
|
199
|
+
if (z) return z
|
|
200
|
+
throw new DHMParseError('Cannot parse float value in response text from DHM GetFeatureInfo after pattern.', { response: dhm_text, pattern: text_begin })
|
|
201
|
+
}
|
|
202
|
+
throw new DHMParseError('Cannot match response text from DHM GetFeatureInfo with pattern.', { response: dhm_text, pattern: text_begin })
|
|
163
203
|
}
|
|
164
204
|
|
|
165
205
|
/** Fetches a GeoTIFF with elevation data matching a bounding box of EPSG:25832 coordinates
|
|
166
206
|
* @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,
|
|
207
|
+
* @param {{API_DHM_WCS_BASEURL: string, API_DHM_KEY: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
168
208
|
* @param {number} [sizeLimit] - Optional size limiter. Will request GeoTIFF with width that does not exceed this number. Default is 500
|
|
169
209
|
* @returns {object} GeoTiff data
|
|
170
210
|
*/
|
|
@@ -176,7 +216,7 @@ function getTerrainByBbox(bbox, auth, sizeLimit = 500) {
|
|
|
176
216
|
// GET request for DHM WCS data
|
|
177
217
|
let url = auth.API_DHM_WCS_BASEURL
|
|
178
218
|
url += '?SERVICE=WCS&COVERAGE=dhm_terraen&RESPONSE_CRS=epsg:25832&CRS=epsg:25832&FORMAT=GTiff&REQUEST=GetCoverage&VERSION=1.0.0'
|
|
179
|
-
url += `&
|
|
219
|
+
url += `&apikey=${ auth.API_DHM_KEY }`
|
|
180
220
|
url += `&height=${ height }`
|
|
181
221
|
url += `&width=${ width }`
|
|
182
222
|
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
|
|
@@ -197,6 +237,7 @@ export {
|
|
|
197
237
|
getElevation,
|
|
198
238
|
visualizeGeotiff,
|
|
199
239
|
getZ,
|
|
240
|
+
DHMParseError,
|
|
200
241
|
getTerrainGeoTIFF,
|
|
201
242
|
getDenmarkGeoTiff,
|
|
202
243
|
getTerrainByBbox,
|
package/modules/image.js
CHANGED
|
@@ -142,10 +142,6 @@ function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
|
142
142
|
const worldcoor = image2world(image_data,col,row,z)
|
|
143
143
|
|
|
144
144
|
i--
|
|
145
|
-
if (i <= 0) {
|
|
146
|
-
// max iterations reached
|
|
147
|
-
return [worldcoor, 0, count]
|
|
148
|
-
}
|
|
149
145
|
|
|
150
146
|
// Get new z value from coordinates
|
|
151
147
|
return getZ(worldcoor[0],worldcoor[1], auth).then((new_z) => {
|
|
@@ -153,13 +149,12 @@ function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
|
153
149
|
// How big is the different between z and new z?
|
|
154
150
|
const delta = Math.abs(new_z - z)
|
|
155
151
|
|
|
156
|
-
if (delta
|
|
157
|
-
// If the difference is
|
|
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
|
|
152
|
+
if (delta < limit || i <= 0) {
|
|
153
|
+
// If the difference is small or max iterations reached, return coordinates
|
|
162
154
|
return [worldcoor, delta, count]
|
|
155
|
+
} else {
|
|
156
|
+
// If the difference is too big, try building coordinates with the new z
|
|
157
|
+
return iterateRecursive(image_data, col, row, new_z, count + 1, limit, auth, i)
|
|
163
158
|
}
|
|
164
159
|
})
|
|
165
160
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataforsyningen/saul",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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",
|
|
@@ -10,23 +10,23 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "git+https://github.com/
|
|
13
|
+
"url": "git+https://github.com/Klimadatastyrelsen/saul.git"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"skråfoto",
|
|
17
|
-
"SDFI",
|
|
17
|
+
"SDFI", "KDS",
|
|
18
18
|
"saul"
|
|
19
19
|
],
|
|
20
20
|
"author": {
|
|
21
|
-
"name": "
|
|
22
|
-
"email": "support@
|
|
23
|
-
"url": "https://
|
|
21
|
+
"name": "Klimadatastyrelsen",
|
|
22
|
+
"email": "support@kds.dk",
|
|
23
|
+
"url": "https://kds.dk/"
|
|
24
24
|
},
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"bugs": {
|
|
27
|
-
"url": "https://github.com/
|
|
27
|
+
"url": "https://github.com/Klimadatastyrelsen/saul/issues"
|
|
28
28
|
},
|
|
29
|
-
"homepage": "https://github.com/
|
|
29
|
+
"homepage": "https://github.com/Klimadatastyrelsen/saul#readme",
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"jsdoc": "^4.0.2"
|
|
32
32
|
},
|
package/test/assert.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'node:assert'; // https://nodejs.org/api/assert.html
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class Assert
|
|
5
|
+
* @param {AssertOptions} [options] - Optional configuration for assertions, see node:assert.Assert.
|
|
6
|
+
* @throws {ERR_CONSTRUCT_CALL_REQUIRED} If not called with `new`.
|
|
7
|
+
*/
|
|
8
|
+
export class Assert extends assert.Assert {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Test for approximate equality.
|
|
15
|
+
* @param {(number | Array<number>} actual
|
|
16
|
+
* @param {(number | Array<number>} expected
|
|
17
|
+
* @param {number} [tolerance] Allowed deviation (default 0.5).
|
|
18
|
+
* @param {string | Error | MessageFactory} [message]
|
|
19
|
+
* @returns {number} Difference between actual and expected value.
|
|
20
|
+
*/
|
|
21
|
+
almostEqual(actual, expected, tolerance = 0.5, ...message) {
|
|
22
|
+
if (arguments.length < 2) {
|
|
23
|
+
throw new ERR_MISSING_ARGS('actual', 'expected');
|
|
24
|
+
}
|
|
25
|
+
const diff = [];
|
|
26
|
+
if (expected instanceof Array) {
|
|
27
|
+
for (let i = 0; i < expected.length; i++) {
|
|
28
|
+
diff[i] = Math.abs(actual[i] - expected[i]);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
diff[0] = Math.abs(actual - expected);
|
|
32
|
+
}
|
|
33
|
+
const maxdiff = Math.max(...diff);
|
|
34
|
+
if (maxdiff <= tolerance) {
|
|
35
|
+
return maxdiff;
|
|
36
|
+
} else {
|
|
37
|
+
try {
|
|
38
|
+
if (arguments.length < 4) {
|
|
39
|
+
message[0] = 'Expected values to be almost equal:';
|
|
40
|
+
}
|
|
41
|
+
this.deepStrictEqual(actual, expected, ...message);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
err.operator = `tolerance ${tolerance}`;
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/test/saul-core.test.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// Check config.js.example for info on how to set it up
|
|
5
5
|
import auth from '../config.js'
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { Assert } from './assert.js'
|
|
8
8
|
import {getSTAC, world2image, image2world, getZ, iterate} from '../index.js'
|
|
9
9
|
|
|
10
|
+
const assert = new Assert()
|
|
11
|
+
|
|
10
12
|
console.log('---------------')
|
|
11
13
|
console.log(' Core tests ')
|
|
12
14
|
console.log('---------------')
|
|
@@ -27,20 +29,14 @@ const image_x = 41839.698939
|
|
|
27
29
|
const image_y = 3204.326930
|
|
28
30
|
const world_elevation = 38.874336
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
if (Math.abs(num1 - num2) > deviation) {
|
|
32
|
-
return false
|
|
33
|
-
} else {
|
|
34
|
-
return true
|
|
35
|
-
}
|
|
36
|
-
}
|
|
32
|
+
const deviation = 0.05
|
|
37
33
|
|
|
38
34
|
// Test getZ
|
|
39
35
|
try {
|
|
40
36
|
|
|
41
37
|
let elevation = await getZ(world_x, world_y, auth)
|
|
42
38
|
|
|
43
|
-
assert(
|
|
39
|
+
assert.almostEqual(elevation, world_elevation, deviation, 'getZ fail: Elevation is way apart from expected:')
|
|
44
40
|
|
|
45
41
|
console.log("Test getZ OK")
|
|
46
42
|
|
|
@@ -53,8 +49,7 @@ try {
|
|
|
53
49
|
|
|
54
50
|
let ite = await iterate(item, image_x, image_y, auth, 0.05)
|
|
55
51
|
|
|
56
|
-
assert(
|
|
57
|
-
assert(is_equalIsh(ite[0][1], world_y), "iterate fail: The coordinate values are not equal") //test if the values are equal
|
|
52
|
+
assert.almostEqual(ite[0], [world_x, world_y], 0.1, `iterate fail: Coordinates are way apart, ite: ${ite}.`)
|
|
58
53
|
|
|
59
54
|
console.log("Test iterate OK")
|
|
60
55
|
|
|
@@ -67,8 +62,7 @@ try {
|
|
|
67
62
|
|
|
68
63
|
let xy = world2image(item, world_x, world_y, world_elevation)
|
|
69
64
|
|
|
70
|
-
assert(
|
|
71
|
-
assert(is_equalIsh(xy[1], image_y, 1), `world2image fail: The y coordinate values ${xy[1]} ${image_y} are not equal`)
|
|
65
|
+
assert.almostEqual(xy, [image_x, image_y], 1, 'world2image fail: The coordinate values are not as expected:')
|
|
72
66
|
|
|
73
67
|
console.log("Test world2image OK")
|
|
74
68
|
|
|
@@ -81,8 +75,7 @@ try {
|
|
|
81
75
|
|
|
82
76
|
let coords = image2world(item, image_x, image_y, world_elevation)
|
|
83
77
|
|
|
84
|
-
assert(
|
|
85
|
-
assert(is_equalIsh(coords[1], world_y), "image2world fail: The latitude values are not equal")
|
|
78
|
+
assert.almostEqual(coords, [world_x, world_y], deviation, 'image2world fail: The longitude or latitude are not equal to expected:')
|
|
86
79
|
|
|
87
80
|
console.log("Test image2world OK")
|
|
88
81
|
|
|
@@ -96,8 +89,7 @@ try {
|
|
|
96
89
|
let xy1 = world2image(item, world_x, world_y, world_elevation)
|
|
97
90
|
let coords1 = image2world(item, xy1[0], xy1[1], world_elevation)
|
|
98
91
|
|
|
99
|
-
assert(
|
|
100
|
-
assert(is_equalIsh(coords1[1], world_y), "world2image > image2world fail: The latitude values are not equal")
|
|
92
|
+
assert.almostEqual(coords1, [world_x, world_y], deviation, 'world2image > image2world fail: The longitude or latitude values are not equal to expected:')
|
|
101
93
|
|
|
102
94
|
console.log("Test world2image > image2world OK")
|
|
103
95
|
|
|
@@ -109,10 +101,9 @@ try {
|
|
|
109
101
|
try {
|
|
110
102
|
|
|
111
103
|
let xy2 = world2image(item, world_x, world_y, world_elevation)
|
|
112
|
-
let
|
|
113
|
-
|
|
114
|
-
assert(
|
|
115
|
-
assert(is_equalIsh(coords2[1], world_y), "world2image > iterate fail: The latitude values are not equal")
|
|
104
|
+
let iterate_result = await iterate(item, xy2[0], xy2[1], auth, 0.1)
|
|
105
|
+
let coords2 = iterate_result[0]
|
|
106
|
+
assert.almostEqual(coords2, [world_x, world_y], 0.1, `world2image > iterate fail: The longitude or latitude values are not equal, iterate result: ${iterate_result}.`)
|
|
116
107
|
|
|
117
108
|
console.log("Test world2image > iterate OK")
|
|
118
109
|
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// Check config.js.example for info on how to set it up
|
|
3
3
|
import auth from '../config.js'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { Assert } from './assert.js'
|
|
6
6
|
import { getElevation, get, getTerrainGeoTIFF, getZ, getWorldXYZ, world2image, getTerrainByBbox } from '../index.js'
|
|
7
7
|
|
|
8
|
+
const assert = new Assert()
|
|
9
|
+
|
|
8
10
|
console.log('---------------')
|
|
9
11
|
console.log('Elevation tests')
|
|
10
12
|
console.log('---------------')
|
|
@@ -12,20 +14,11 @@ console.log('---------------')
|
|
|
12
14
|
// Vars
|
|
13
15
|
const stac_item = '2021_83_29_2_0019_00003995'
|
|
14
16
|
const fidelity = 0.03 // Higher number means more points and better precision
|
|
15
|
-
const max_deviation = 0.5
|
|
16
17
|
|
|
17
18
|
// STAC API endpoint
|
|
18
19
|
let url_stac = auth.API_STAC_BASEURL + '/search?limit=1&crs=http://www.opengis.net/def/crs/EPSG/0/25832&token=9b554b6c854184c3b0f377ffc7481585'
|
|
19
20
|
url_stac += `&ids=${ stac_item }`
|
|
20
21
|
|
|
21
|
-
function is_equalIsh(num1, num2, deviation = max_deviation) {
|
|
22
|
-
if (Math.abs(num1 - num2) > deviation) {
|
|
23
|
-
return false
|
|
24
|
-
} else {
|
|
25
|
-
return true
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
function getRandomCoordinate(bbox) {
|
|
30
23
|
const x = bbox[0] + Math.random() * (bbox[2] - bbox[0])
|
|
31
24
|
const y = bbox[1] + Math.random() * (bbox[3] - bbox[1])
|
|
@@ -57,22 +50,23 @@ function testGetWorldXYZAnumberOfTimes(item, terrain, times) {
|
|
|
57
50
|
}).then(world_xy => {
|
|
58
51
|
compareElevations(world_xy[0], world_xy[1], terrain)
|
|
59
52
|
const image_coords = world2image(item, world_xy[0], world_xy[1], world_xy[2])
|
|
60
|
-
assert(
|
|
61
|
-
assert(is_equalIsh(image_coords[1], xy[1], 2), `Image y coordinates ${image_coords[1]} / ${xy[1]} do not match`)
|
|
53
|
+
assert.almostEqual(image_coords, xy, 2, `Image coordinates ${image_coords} / ${xy} do not match`)
|
|
62
54
|
console.log('getWorldXYZ => world2image OK')
|
|
63
55
|
})
|
|
56
|
+
.catch(console.error)
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
function compareElevations(x,y,geotiff) {
|
|
68
61
|
getElevation(x, y, geotiff)
|
|
69
|
-
.then(elevation =>
|
|
62
|
+
.then(elevation =>
|
|
70
63
|
getZ(x, y, auth)
|
|
71
|
-
.then(getz_e =>
|
|
72
|
-
assert(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
64
|
+
.then(getz_e =>
|
|
65
|
+
assert.almostEqual(getz_e, elevation, 2.7, `Deviant elevation at (${x}, ${y}):`)
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
.then(delta => console.log(`Elevation deviation ${delta.toFixed(2)} at (${x}, ${y}) OK`))
|
|
69
|
+
.catch(console.error)
|
|
76
70
|
}
|
|
77
71
|
|
|
78
72
|
// Test getTerrainGeoTIFF and getElevation with a STAC API item
|
|
@@ -111,7 +105,7 @@ get(url_stac)
|
|
|
111
105
|
// Testing getTerrainByBbox()
|
|
112
106
|
try {
|
|
113
107
|
const gtiff = await getTerrainByBbox([542929.6729020511, 6153925.479277819, 544463.7189110086, 6155242.76541582], auth)
|
|
114
|
-
assert(gtiff.fileDirectory.ImageWidth
|
|
108
|
+
assert.equal(gtiff.fileDirectory.ImageWidth, 500, 'GeoTIFF has weird dimensions')
|
|
115
109
|
console.log('Test getTerrainByBbox() => OK')
|
|
116
110
|
} catch(error) {
|
|
117
111
|
console.error(error)
|
package/modules/core.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SAUL photogrammetry utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { getElevation } from './saul-elevation.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Converts x,y coordinates from an image to real world lat,lon coordinates
|
|
10
|
-
* @param {object} image_data - skraafoto-stac-api image data
|
|
11
|
-
* @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.
|
|
12
|
-
* @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.
|
|
13
|
-
* @param {number} [Z] - Elevation (geoide)
|
|
14
|
-
* @returns {array} [longitude, latitude, elevation]
|
|
15
|
-
*/
|
|
16
|
-
function image2world(image_data, col, row, Z = 0) {
|
|
17
|
-
|
|
18
|
-
// constants pulled from image_data
|
|
19
|
-
const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
|
|
20
|
-
const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
|
|
21
|
-
const ci = image_data.properties['pers:interior_orientation'].focal_length
|
|
22
|
-
const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
|
|
23
|
-
const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
|
|
24
|
-
const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
|
|
25
|
-
const X0 = image_data.properties['pers:perspective_center'][0]
|
|
26
|
-
const Y0 = image_data.properties['pers:perspective_center'][1]
|
|
27
|
-
const Z0 = image_data.properties['pers:perspective_center'][2]
|
|
28
|
-
const Ome = image_data.properties['pers:omega']
|
|
29
|
-
const Phi = image_data.properties['pers:phi']
|
|
30
|
-
const Kap = image_data.properties['pers:kappa']
|
|
31
|
-
|
|
32
|
-
// recalc values
|
|
33
|
-
var c = ci*(-1)
|
|
34
|
-
var dimX = dimXi*pix/2*(-1)
|
|
35
|
-
var dimY = dimYi*pix/2*(-1)
|
|
36
|
-
|
|
37
|
-
// ... Do calculations ...
|
|
38
|
-
var o = radians(Ome)
|
|
39
|
-
var p = radians(Phi)
|
|
40
|
-
var k = radians(Kap)
|
|
41
|
-
var D11 = Math.cos(p) * Math.cos(k)
|
|
42
|
-
var D12 = - Math.cos(p) * Math.sin(k)
|
|
43
|
-
var D13 = Math.sin(p)
|
|
44
|
-
var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
|
|
45
|
-
var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
|
|
46
|
-
var D23 = - Math.sin(o) * Math.cos(p)
|
|
47
|
-
var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
|
|
48
|
-
var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
|
|
49
|
-
var D33 = Math.cos(o) * Math.cos(p)
|
|
50
|
-
|
|
51
|
-
var x_dot = ((col*pix)-dimX*-1)-xx0
|
|
52
|
-
var y_dot = ((row*pix)-dimY*-1)-yy0
|
|
53
|
-
|
|
54
|
-
var kx = (D11*x_dot + D12*y_dot + D13*c)/(D31*x_dot + D32*y_dot + D33*c)
|
|
55
|
-
var ky = (D21*x_dot + D22*y_dot + D23*c)/(D31*x_dot + D32*y_dot + D33*c)
|
|
56
|
-
|
|
57
|
-
var X = (Z-Z0)*kx + X0
|
|
58
|
-
var Y = (Z-Z0)*ky + Y0
|
|
59
|
-
|
|
60
|
-
return[
|
|
61
|
-
Math.round(X * 100) / 100,
|
|
62
|
-
Math.round(Y * 100) / 100,
|
|
63
|
-
Z
|
|
64
|
-
]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Converts world lat,lon coordinates to x,y coordinates within a specific image
|
|
69
|
-
* @param {Object} image_data - skraafoto-stac-api image data
|
|
70
|
-
* @param {Number} Y - northing
|
|
71
|
-
* @param {Number} X - easting
|
|
72
|
-
* @param {Number} [Z] - elevation (geoide)
|
|
73
|
-
* @returns {array} [x,y] Column/row image coordinate
|
|
74
|
-
*/
|
|
75
|
-
function getImageXY(image_data, X, Y, Z = 0) {
|
|
76
|
-
|
|
77
|
-
// constants pulled from image_data
|
|
78
|
-
const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
|
|
79
|
-
const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
|
|
80
|
-
const ci = image_data.properties['pers:interior_orientation'].focal_length
|
|
81
|
-
const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
|
|
82
|
-
const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
|
|
83
|
-
const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
|
|
84
|
-
const X0 = image_data.properties['pers:perspective_center'][0]
|
|
85
|
-
const Y0 = image_data.properties['pers:perspective_center'][1]
|
|
86
|
-
const Z0 = image_data.properties['pers:perspective_center'][2]
|
|
87
|
-
const Ome = image_data.properties['pers:omega']
|
|
88
|
-
const Phi = image_data.properties['pers:phi']
|
|
89
|
-
const Kap = image_data.properties['pers:kappa']
|
|
90
|
-
|
|
91
|
-
// recalc values
|
|
92
|
-
var c = ci*(-1)
|
|
93
|
-
var dimX = dimXi*pix/2*(-1)
|
|
94
|
-
var dimY = dimYi*pix/2*(-1)
|
|
95
|
-
|
|
96
|
-
// ... Do calculations ...
|
|
97
|
-
var o = radians(Ome)
|
|
98
|
-
var p = radians(Phi)
|
|
99
|
-
var k = radians(Kap)
|
|
100
|
-
var D11 = Math.cos(p) * Math.cos(k)
|
|
101
|
-
var D12 = - Math.cos(p) * Math.sin(k)
|
|
102
|
-
var D13 = Math.sin(p)
|
|
103
|
-
var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
|
|
104
|
-
var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
|
|
105
|
-
var D23 = - Math.sin(o) * Math.cos(p)
|
|
106
|
-
var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
|
|
107
|
-
var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
|
|
108
|
-
var D33 = Math.cos(o) * Math.cos(p)
|
|
109
|
-
|
|
110
|
-
var x_dot = (-1)*c*((D11*(X-X0)+D21*(Y-Y0)+D31*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
|
|
111
|
-
var y_dot = (-1)*c*((D12*(X-X0)+D22*(Y-Y0)+D32*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
|
|
112
|
-
|
|
113
|
-
var col = ((x_dot-xx0)+(dimX))*(-1)/pix
|
|
114
|
-
var row = ((y_dot-yy0)+(dimY))*(-1)/pix
|
|
115
|
-
|
|
116
|
-
return [
|
|
117
|
-
Math.round(col),
|
|
118
|
-
Math.round(row)
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* (DEPRECATED - Use getImageXY) Converts lat,lon coordinates to x,y coordinates within a specific image
|
|
124
|
-
* @param {Object} image_data - skraafoto-stac-api image data
|
|
125
|
-
* @param {Number} Y - northing
|
|
126
|
-
* @param {Number} X - easting
|
|
127
|
-
* @param {Number} [Z] - elevation (geoide)
|
|
128
|
-
* @returns {array} [x,y] Column/row image coordinate
|
|
129
|
-
*/
|
|
130
|
-
function world2image(image_data, X, Y, Z = 0) {
|
|
131
|
-
return getImageXY(image_data, X, Y, Z)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Converts degress to radians */
|
|
135
|
-
function radians(degrees) {
|
|
136
|
-
return degrees * (Math.PI / 180)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
140
|
-
function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
141
|
-
|
|
142
|
-
// Get coordinates based on current z value
|
|
143
|
-
const worldcoor = image2world(image_data,col,row,z)
|
|
144
|
-
|
|
145
|
-
i--
|
|
146
|
-
if (i <= 0) {
|
|
147
|
-
// max iterations reached
|
|
148
|
-
return [worldcoor, 0, count]
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Get new z value from coordinates
|
|
152
|
-
return getZ(worldcoor[0],worldcoor[1], auth).then((new_z) => {
|
|
153
|
-
|
|
154
|
-
// How big is the different between z and new z?
|
|
155
|
-
const delta = Math.abs(new_z - z)
|
|
156
|
-
|
|
157
|
-
if (delta >= limit) {
|
|
158
|
-
// If the difference is too big, try building coordinates with the new z
|
|
159
|
-
count = count + 1
|
|
160
|
-
return iterateRecursive(image_data, col, row, new_z, count, limit, auth, i)
|
|
161
|
-
} else {
|
|
162
|
-
// If the difference is small, return coordinates
|
|
163
|
-
return [worldcoor, delta, count]
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* (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.
|
|
170
|
-
* @param {object} image_data - image item data from STAC API
|
|
171
|
-
* @param {number} col - image x coordinate (from left to right)
|
|
172
|
-
* @param {number} row - image y coordinate (from top to bottom)
|
|
173
|
-
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
174
|
-
* @param {number} [limit] - result may be inaccurate within this limit. Default is 1.
|
|
175
|
-
* @returns {array} [world coordindates (array), elevation discrepancy, calculation iterations]
|
|
176
|
-
*/
|
|
177
|
-
function iterate(image_data, col, row, auth, limit = 1) {
|
|
178
|
-
let i = 20 // Maximum number of iterations allowed
|
|
179
|
-
return iterateRecursive(image_data, col, row, 0.5, 0, limit, auth, i)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function compareZ(options) {
|
|
183
|
-
|
|
184
|
-
// Get coordinates based on current z value
|
|
185
|
-
const world_xyz = image2world(options.image, options.xy[0], options.xy[1], options.z)
|
|
186
|
-
// Get new z value from coordinates
|
|
187
|
-
world_xyz[2] = await getElevation(world_xyz[0], world_xyz[1], options.terrain)
|
|
188
|
-
// How big is the different between z and new z?
|
|
189
|
-
const delta = Math.abs(world_xyz[2] - options.z)
|
|
190
|
-
|
|
191
|
-
// Check delta vs. limit. If the difference is too big, try building coordinates with the new z
|
|
192
|
-
// Make sure the recursive loop ends after 20 iterations even if no precise z is found
|
|
193
|
-
if (options.iteration < 20 && delta > options.limit) {
|
|
194
|
-
options.iteration++
|
|
195
|
-
options.z = world_xyz[2]
|
|
196
|
-
return compareZ(options)
|
|
197
|
-
} else {
|
|
198
|
-
return world_xyz
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** 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.
|
|
203
|
-
* @param {object} options
|
|
204
|
-
* @param {array} options.xy - image coordinates
|
|
205
|
-
* @param {object} options.image - STAC API item
|
|
206
|
-
* @param {object} options.terrain - GeoTIFF object from getTerrainGeoTIFF() output
|
|
207
|
-
* @param {number} precision - Precision in meters for "good enough"
|
|
208
|
-
* @returns {array} Array with world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
|
|
209
|
-
*/
|
|
210
|
-
async function getWorldXYZ(options, precision = 0.3) {
|
|
211
|
-
|
|
212
|
-
const best_world_xyz = compareZ({
|
|
213
|
-
image: options.image,
|
|
214
|
-
terrain: options.terrain,
|
|
215
|
-
xy: options.xy,
|
|
216
|
-
z: 0,
|
|
217
|
-
limit: precision,
|
|
218
|
-
iteration: 1
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
return best_world_xyz
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export {
|
|
225
|
-
image2world,
|
|
226
|
-
world2image,
|
|
227
|
-
getWorldXYZ,
|
|
228
|
-
getImageXY,
|
|
229
|
-
iterate
|
|
230
|
-
}
|
|
231
|
-
|