@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.
@@ -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@v3
12
- - uses: actions/setup-node@v3
15
+ - uses: actions/checkout@v6
16
+ - uses: actions/setup-node@v6
13
17
  with:
14
- node-version: 16
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@v3
25
- - uses: actions/setup-node@v3
28
+ - uses: actions/checkout@v6
29
+ - uses: actions/setup-node@v6
26
30
  with:
27
- node-version: 16
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
@@ -0,0 +1,13 @@
1
+ FROM node:26-alpine
2
+
3
+ # Set the working directory inside the container
4
+ WORKDIR /saul
5
+
6
+ # Copy files to the container
7
+ COPY . .
8
+
9
+ # Install dependencies
10
+ RUN npm install
11
+
12
+ # Run the Vite dev server
13
+ CMD ["npm", "run", "test"]
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 `/example/config.js.example`
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://services.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&username=xxx&password=xxx&height=1000&width=1260&bbox=430000,6040000,900000,6413000
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
@@ -0,0 +1,10 @@
1
+ services:
2
+ saul:
3
+ build: .
4
+ volumes:
5
+ - .:/saul # Local source of Saul library
6
+ - /saul/node_modules # Do not mount over node_modules
7
+ environment:
8
+ - NODE_ENV=development
9
+ deploy:
10
+ mode: replicated_job
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 and auth can be aquired from https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/koter/
8
- API_DHM_WCS_BASEURL: "https://services.datafordeler.dk/DHMNedboer/dhm_wcs/1.0.0/WCS",
9
- API_DHM_BASEURL: "https://services.datafordeler.dk/DHMTerraen/DHMKoter/1.0.0/GEOREST/HentKoter",
10
- API_DHM_TOKENA: "", // DHM API service username
11
- API_DHM_TOKENB: "" // DHM API service password
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
- getDHM,
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
- /** Returns response data */
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 responsee from API
84
- * @param {string} url - API service URL, including endpoint paths and query parameters.
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
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
- * API method to fetch data from DHM
155
- * @param {string} query - DHM API query. Find details at https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/koter/
156
- * @param {{API_DHM_BASEURL: string, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
157
- * @returns {object} Response data
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 = `&username=${auth.API_DHM_TOKENA}&password=${auth.API_DHM_TOKENB}`
161
- return get(encodeURI(auth.API_DHM_BASEURL + query + auth_params), {cache: 'force-cache'})
162
- .then((data) => data)
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
  /**
@@ -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, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
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
- * 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
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
- let zcoor_data = await getDHM(`?geop=POINT(${xcoor} ${ycoor})&elevationmodel=dtm`, auth)
161
- let z = zcoor_data.HentKoterRespons.data[0].kote
162
- return z
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, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
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 += `&username=${ auth.API_DHM_TOKENA }&password=${ auth.API_DHM_TOKENB }`
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 >= 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
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": "4.2.0",
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/SDFIdk/saul.git"
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": "Styrelsen for Dataforsyning og Infrastruktur",
22
- "email": "support@sdfi.dk",
23
- "url": "https://sdfi.dk/"
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/SDFIdk/saul/issues"
27
+ "url": "https://github.com/Klimadatastyrelsen/saul/issues"
28
28
  },
29
- "homepage": "https://github.com/SDFIdk/saul#readme",
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
+ }
@@ -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 assert from 'assert'
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
- function is_equalIsh(num1, num2, deviation = 0.05) {
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(is_equalIsh(elevation, world_elevation), "getZ fail: This is not the Z value we were hoping for.")
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(is_equalIsh(ite[0][0], world_x), "iterate fail: The coordinate values are not equal") //test if the values are equal
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(is_equalIsh(xy[0], image_x, 1), `world2image fail: The x coordinate values ${xy[0]} ${image_x} are not equal`)
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(is_equalIsh(coords[0], world_x), "image2world fail: The longitude values are not equal")
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(is_equalIsh(coords1[0], world_x), "world2image > image2world fail: The longitude values are not equal")
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 coords2 = iterate(item, xy2[0], xy2[1], auth)
113
-
114
- assert(is_equalIsh(coords2[0], world_x), "world2image > iterate fail: The longitude values are not equal")
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 assert from 'assert'
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(is_equalIsh(image_coords[0], xy[0], 2), `Image x coordinates ${image_coords[0]} / ${xy[0]} do not match`)
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(is_equalIsh(getz_e, elevation, 2.7), `Elevations ${elevation} / ${getz_e} at ${x} ${y} are way apart`)
73
- console.log(`Elevation at ${ x } ${ y } with delta ${Math.abs(elevation - getz_e).toFixed(2)} OK`)
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 === 500, 'GeoTIFF has weird dimensions')
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
-