@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 CHANGED
@@ -3,8 +3,9 @@
3
3
  export {
4
4
  image2world,
5
5
  world2image,
6
- getZ,
7
- iterate
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 JSON response data */
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! Status: ${ response.status }`)
70
+ throw new Error(`HTTP error! ${ response.status }`)
71
71
  }
72
- // We assume the returned data is JSON
73
- return response.json()
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
@@ -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.2",
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
+ })