@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 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,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.0.2",
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
+ })