@dataforsyningen/saul 3.6.0 → 4.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/index.js CHANGED
@@ -1,20 +1,9 @@
1
1
  // Import/export modules with their public methods
2
2
 
3
- export {
4
- image2world,
5
- world2image, // Deprecated. Use getImageXY
6
- getWorldXYZ,
7
- getImageXY,
8
- getZ,
9
- iterate // Deprecated. Use getWorldXYZ
10
- } from './modules/saul-core.js'
11
-
12
3
  export {
13
4
  postSTAC,
14
5
  getSTAC,
15
6
  getDHM,
16
- getTerrainGeoTIFF,
17
- getDenmarkGeoTiff,
18
7
  get,
19
8
  post
20
9
  } from './modules/api.js'
@@ -22,9 +11,22 @@ export {
22
11
  export {
23
12
  epsg25832proj,
24
13
  createTranslator
25
- } from './modules/saul-projection.js'
14
+ } from './modules/projection.js'
26
15
 
27
16
  export {
28
17
  getElevation,
29
- visualizeGeotiff
30
- } from './modules/saul-elevation.js'
18
+ visualizeGeotiff,
19
+ getZ,
20
+ getTerrainGeoTIFF,
21
+ getDenmarkGeoTiff,
22
+ getTerrainByBbox
23
+ } from './modules/elevation.js'
24
+
25
+ export {
26
+ getTotalBbox,
27
+ image2world,
28
+ world2image, // Deprecated. Use getImageXY
29
+ getWorldXYZ,
30
+ getImageXY,
31
+ iterate // Deprecated. Use getWorldXYZ
32
+ } from './modules/image.js'
package/modules/api.js CHANGED
@@ -2,8 +2,6 @@
2
2
  * SAUL API utilities
3
3
  */
4
4
 
5
- import { fromArrayBuffer } from 'geotiff'
6
-
7
5
  let error_msg
8
6
  let load_stack = []
9
7
 
@@ -194,77 +192,10 @@ function postSTAC(endpoint, data, auth) {
194
192
  .then((data) => data)
195
193
  }
196
194
 
197
-
198
- /** Converts raw GeoTIFF arrayBuffer to image */
199
- async function consumeGeoTIFF(raw_data) {
200
- const tiff = await fromArrayBuffer(raw_data)
201
- const image = await tiff.getImage()
202
- return image
203
- }
204
-
205
- function calcSizeRatio(sizeX, bbox) {
206
- const ratio = ( bbox[0] - bbox[2] ) / ( bbox[1] - bbox[3] )
207
- return Math.round(sizeX * ratio)
208
- }
209
-
210
-
211
- /** Fetches a GeoTIFF with elevation data matching the bounding box of a STAC API item (image)
212
- * @param {object} stac_item - STAC API item from a featureCollection request
213
- * @param {{API_DHM_WCS_BASEURL: string, API_DHM_TOKENA: string, API_DHM_TOKENB: string}} auth - API autentication data. See ../config.js.example for reference.
214
- * @param {number} [resolution] - Resolution (1 - 0.01). Higher number means more pixels and better precision.
215
- * @returns {object} GeoTiff data
216
- */
217
- function getTerrainGeoTIFF(stac_item, auth, resolution = 0.05, sizeX = 300) {
218
-
219
- const bbox = stac_item.bbox
220
- const sizeY = calcSizeRatio(sizeX, stac_item.bbox)
221
- const width = stac_item.properties ? Math.round( stac_item.properties['proj:shape'][0] * resolution ) : sizeX
222
- const height = stac_item.properties ? Math.round( stac_item.properties['proj:shape'][1] * resolution ) : sizeY
223
-
224
- // GET request for DHM WCS data
225
- let url = auth.API_DHM_WCS_BASEURL
226
- url += '?SERVICE=WCS&COVERAGE=dhm_terraen&RESPONSE_CRS=epsg:25832&CRS=epsg:25832&FORMAT=GTiff&REQUEST=GetCoverage&VERSION=1.0.0'
227
- url += `&username=${ auth.API_DHM_TOKENA }&password=${ auth.API_DHM_TOKENB }`
228
- url += `&height=${ height }`
229
- url += `&width=${ width }`
230
- url += `&bbox=${ bbox[0]},${ bbox[1]},${ bbox[2]},${ bbox[3]}`
231
-
232
- return get(url, {cache: 'force-cache'}, false)
233
- .then((response) => {
234
- return response.arrayBuffer()
235
- })
236
- .then((arrayBuffer) => {
237
- return consumeGeoTIFF(arrayBuffer)
238
- })
239
- }
240
-
241
- /**
242
- * Fetches a geoTIFF with elevation data covering all of Denmark
243
- * @param {Object} options
244
- * @param {Object} options.src - URL to download a pre-generated GeoTiff.
245
- * @param {Object} options.auth - API autentication data. See ../config.js.example for reference.
246
- * @param {Number} options.size - Width of the geoTiff image to return
247
- * @returns GeoTIFF raster with elevation data
248
- */
249
- function getDenmarkGeoTiff(options) {
250
- if (options.src) {
251
- return fetch(options.src)
252
- .then(response => response.arrayBuffer())
253
- .then(arrayBuffer => consumeGeoTIFF(arrayBuffer))
254
- } else {
255
- const auth = options.auth
256
- const size = options.size ? options.size : 1000
257
- return getTerrainGeoTIFF({bbox: [430000,6040000,900000,6413000]}, auth, null, size)
258
- }
259
- }
260
-
261
195
  export {
262
196
  get,
263
197
  post,
264
198
  getSTAC,
265
199
  postSTAC,
266
- getDHM,
267
- getTerrainGeoTIFF,
268
- getDenmarkGeoTiff,
269
- consumeGeoTIFF
200
+ getDHM
270
201
  }
@@ -2,7 +2,7 @@
2
2
  * SAUL photogrammetry utilities
3
3
  */
4
4
 
5
- import { getDHM } from './api.js'
5
+
6
6
  import { getElevation } from './saul-elevation.js'
7
7
 
8
8
  /**
@@ -136,19 +136,6 @@ function radians(degrees) {
136
136
  return degrees * (Math.PI / 180)
137
137
  }
138
138
 
139
- /**
140
- * Fetches a single elevation value based on X,Y coordinates using DHM/Koter endpoint
141
- * @param {number} xcoor - EPSG:25832 X coordinate
142
- * @param {number} ycoor - EPSG:25832 Y coordinate
143
- * @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
144
- * @returns {number} Elevation in meters
145
- */
146
- async function getZ(xcoor, ycoor, auth) {
147
- let zcoor_data = await getDHM(`?geop=POINT(${xcoor} ${ycoor})&elevationmodel=dtm`, auth)
148
- let z = zcoor_data.HentKoterRespons.data[0].kote
149
- return z
150
- }
151
-
152
139
  /** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
153
140
  function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
154
141
 
@@ -239,7 +226,6 @@ export {
239
226
  world2image,
240
227
  getWorldXYZ,
241
228
  getImageXY,
242
- getZ,
243
229
  iterate
244
230
  }
245
231
 
@@ -0,0 +1,203 @@
1
+ import { getDHM, get } from './api.js'
2
+ import { fromArrayBuffer } from 'geotiff'
3
+
4
+ /** Converts raw GeoTIFF arrayBuffer to image */
5
+ async function consumeGeoTIFF(raw_data) {
6
+ const tiff = await fromArrayBuffer(raw_data)
7
+ const image = await tiff.getImage()
8
+ return image
9
+ }
10
+
11
+ function calcSizeRatio(sizeX, bbox) {
12
+ const ratio = ( bbox[0] - bbox[2] ) / ( bbox[1] - bbox[3] )
13
+ return Math.round(sizeX * ratio)
14
+ }
15
+
16
+ /** Fetches a GeoTIFF with elevation data matching the bounding box of a STAC API item (image)
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.
19
+ * @param {number} [resolution] - Resolution (1 - 0.01). Higher number means more pixels and better precision.
20
+ * @returns {object} GeoTiff data
21
+ */
22
+ function getTerrainGeoTIFF(stac_item, auth, resolution = 0.05, sizeX = 300) {
23
+
24
+ const bbox = stac_item.bbox
25
+ const width = stac_item.properties ? Math.round( stac_item.properties['proj:shape'][0] * resolution ) : sizeX
26
+
27
+ return getTerrainByBbox(bbox, auth, width)
28
+ }
29
+
30
+ /**
31
+ * Fetches a geoTIFF with elevation data covering all of Denmark
32
+ * @param {Object} options
33
+ * @param {Object} options.src - URL to download a pre-generated GeoTiff.
34
+ * @param {Object} options.auth - API autentication data. See ../config.js.example for reference.
35
+ * @param {Number} options.size - Width of the geoTiff image to return
36
+ * @returns GeoTIFF raster with elevation data
37
+ */
38
+ function getDenmarkGeoTiff(options) {
39
+ if (options.src) {
40
+ return fetch(options.src)
41
+ .then(response => response.arrayBuffer())
42
+ .then(arrayBuffer => consumeGeoTIFF(arrayBuffer))
43
+ } else {
44
+ const auth = options.auth
45
+ const size = options.size ? options.size : 1000
46
+ return getTerrainByBbox([430000,6040000,900000,6413000], auth, size)
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Maps a coordinate from the one coordinate system to another based on GeoTiff image dimensions.
52
+ * @param {Array} coord - EPSG:25832 X,Y coordinate pair
53
+ * @param {Array} bbox - Bounding box within input coordinate systtem. Consists of 2 EPSG:25832 coordinate pairs. Example: [x1,y1,x2,y2]
54
+ * @param {Array} imageDimensions - Width and height of a GeoTiff image. This will set the bounding box of the output coordinate system. Note: This coordinate system will have 0,0 set in the top right position of the image.
55
+ */
56
+ function convertCoordinate(coord, bbox1, imageDimensions) {
57
+
58
+ // Extract the coordinates from the bounding boxes
59
+ const [x1_min, y1_min, x1_max, y1_max] = bbox1
60
+
61
+ // Extract the coordinate to convert
62
+ const [x1, y1] = coord
63
+
64
+ // Calculate the width and height of both bounding boxes
65
+ const width1 = x1_max - x1_min
66
+ const height1 = y1_max - y1_min
67
+ const width2 = imageDimensions[0]
68
+ const height2 = imageDimensions[1]
69
+
70
+ // Calculate the relative position in the first coordinate system
71
+ const x_relative = (x1 - x1_min) / width1
72
+ const y_relative = (y1 - y1_min) / height1
73
+
74
+ // Calculate the new position in the second coordinate system
75
+ const x2 = Math.abs(x_relative * width2)
76
+ const y2 = height2 - Math.abs(y_relative * height2)
77
+
78
+ return [x2, y2]
79
+ }
80
+
81
+ /**
82
+ * Calculates nearest elevation for given coordinate using geoTIFF terrain data
83
+ * @param {number} x - EPSG:25832 X coordinate
84
+ * @param {number} y - EPSG:25832 Y coordinate
85
+ * @param {object} geoTiff - GeoTiff data output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
86
+ * @returns {number} Elevation in meters
87
+ */
88
+ async function getElevation(x, y, geoTiff) {
89
+
90
+ const bbox = geoTiff.getBoundingBox()
91
+ const height = geoTiff.getHeight()
92
+ const width = geoTiff.getWidth()
93
+
94
+ // Convert lat/lon to geotiff x/y
95
+ const [xPx, yPx] = convertCoordinate([x,y], bbox, [width, height])
96
+
97
+ // Fetch window of raster values around x/y
98
+ const window = [
99
+ Math.floor(xPx),
100
+ Math.floor(yPx),
101
+ Math.ceil(xPx),
102
+ Math.ceil(yPx)
103
+ ]
104
+
105
+ // Fetch elevation value from geoTiff rasters
106
+ const elevation_data = await geoTiff.readRasters({
107
+ window: window,
108
+ fillValue: 0
109
+ })
110
+
111
+ return elevation_data[0][0]
112
+ }
113
+
114
+ /** Creates an ASCII visualization of a GeoTiff with terrain elevation data. Useful for display in a terminal.
115
+ * NOTE: You may want to use a downsmapled GeoTiff to fit within your terminal window. A GeoTiff pixel is represented by 3 characters each.
116
+ * @param {object} gTiff - GeoTiff output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
117
+ */
118
+ async function visualizeGeotiff(gTiff) {
119
+ console.log('--- GTIFF visualization ---')
120
+ const float32Arr = await gTiff.readRasters()
121
+ const tiffWidth = gTiff.getWidth()
122
+ const rasters = float32Arr[0]
123
+ let lines = []
124
+ let line = ''
125
+ for (let i = 0; i < rasters.length; i++) {
126
+ if (i % tiffWidth === 0) {
127
+ lines.push(line)
128
+ line = ''
129
+ }
130
+ line += zeroPadNumber(rasters[i])
131
+ }
132
+ for (const l of lines) {
133
+ console.log(l)
134
+ }
135
+ }
136
+
137
+ /** Converts numbers to strings in a 3-letter format. Ex. `002`, `0-3` (for negative numbers), or `123` */
138
+ function zeroPadNumber(input) {
139
+ if (input === 0) {
140
+ return "..."
141
+ } else if (input < 0) {
142
+ return `0${input.toFixed(0)}`
143
+ } else if (input < 10) {
144
+ return `00${Math.floor(input)}`
145
+ } else if (input < 100) {
146
+ return `0${Math.floor(input)}`
147
+ } else {
148
+ return `${Math.floor(input)}`
149
+ }
150
+ }
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
158
+ */
159
+ 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
163
+ }
164
+
165
+ /** Fetches a GeoTIFF with elevation data matching a bounding box of EPSG:25832 coordinates
166
+ * @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.
168
+ * @param {number} [sizeLimit] - Optional size limiter. Will request GeoTIFF with width that does not exceed this number. Default is 500
169
+ * @returns {object} GeoTiff data
170
+ */
171
+ function getTerrainByBbox(bbox, auth, sizeLimit = 500) {
172
+
173
+ const width = sizeLimit
174
+ const height = Math.round(calcSizeRatio(sizeLimit, bbox))
175
+
176
+ // GET request for DHM WCS data
177
+ let url = auth.API_DHM_WCS_BASEURL
178
+ 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 }`
180
+ url += `&height=${ height }`
181
+ url += `&width=${ width }`
182
+ 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
183
+
184
+ return get(url, {cache: 'force-cache'}, false)
185
+ .then((response) => {
186
+ return response.arrayBuffer()
187
+ })
188
+ .then((arrayBuffer) => {
189
+ return consumeGeoTIFF(arrayBuffer)
190
+ })
191
+ .catch(error => {
192
+ return error
193
+ })
194
+ }
195
+
196
+ export {
197
+ getElevation,
198
+ visualizeGeotiff,
199
+ getZ,
200
+ getTerrainGeoTIFF,
201
+ getDenmarkGeoTiff,
202
+ getTerrainByBbox
203
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * SAUL photogrammetry utilities
3
+ */
4
+
5
+ import { getElevation, getZ } from './elevation.js'
6
+
7
+ /**
8
+ * Converts x,y coordinates from an image to real world lat,lon coordinates
9
+ * @param {object} image_data - skraafoto-stac-api image data
10
+ * @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.
11
+ * @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.
12
+ * @param {number} [Z] - Elevation (geoide)
13
+ * @returns {array} [longitude, latitude, elevation]
14
+ */
15
+ function image2world(image_data, col, row, Z = 0) {
16
+
17
+ // constants pulled from image_data
18
+ const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
19
+ const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
20
+ const ci = image_data.properties['pers:interior_orientation'].focal_length
21
+ const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
22
+ const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
23
+ const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
24
+ const X0 = image_data.properties['pers:perspective_center'][0]
25
+ const Y0 = image_data.properties['pers:perspective_center'][1]
26
+ const Z0 = image_data.properties['pers:perspective_center'][2]
27
+ const Ome = image_data.properties['pers:omega']
28
+ const Phi = image_data.properties['pers:phi']
29
+ const Kap = image_data.properties['pers:kappa']
30
+
31
+ // recalc values
32
+ var c = ci*(-1)
33
+ var dimX = dimXi*pix/2*(-1)
34
+ var dimY = dimYi*pix/2*(-1)
35
+
36
+ // ... Do calculations ...
37
+ var o = radians(Ome)
38
+ var p = radians(Phi)
39
+ var k = radians(Kap)
40
+ var D11 = Math.cos(p) * Math.cos(k)
41
+ var D12 = - Math.cos(p) * Math.sin(k)
42
+ var D13 = Math.sin(p)
43
+ var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
44
+ var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
45
+ var D23 = - Math.sin(o) * Math.cos(p)
46
+ var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
47
+ var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
48
+ var D33 = Math.cos(o) * Math.cos(p)
49
+
50
+ var x_dot = ((col*pix)-dimX*-1)-xx0
51
+ var y_dot = ((row*pix)-dimY*-1)-yy0
52
+
53
+ var kx = (D11*x_dot + D12*y_dot + D13*c)/(D31*x_dot + D32*y_dot + D33*c)
54
+ var ky = (D21*x_dot + D22*y_dot + D23*c)/(D31*x_dot + D32*y_dot + D33*c)
55
+
56
+ var X = (Z-Z0)*kx + X0
57
+ var Y = (Z-Z0)*ky + Y0
58
+
59
+ return[
60
+ Math.round(X * 100) / 100,
61
+ Math.round(Y * 100) / 100,
62
+ Z
63
+ ]
64
+ }
65
+
66
+ /**
67
+ * Converts world lat,lon coordinates to x,y coordinates within a specific image
68
+ * @param {Object} image_data - skraafoto-stac-api image data
69
+ * @param {Number} Y - northing
70
+ * @param {Number} X - easting
71
+ * @param {Number} [Z] - elevation (geoide)
72
+ * @returns {array} [x,y] Column/row image coordinate
73
+ */
74
+ function getImageXY(image_data, X, Y, Z = 0) {
75
+
76
+ // constants pulled from image_data
77
+ const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
78
+ const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
79
+ const ci = image_data.properties['pers:interior_orientation'].focal_length
80
+ const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
81
+ const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
82
+ const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
83
+ const X0 = image_data.properties['pers:perspective_center'][0]
84
+ const Y0 = image_data.properties['pers:perspective_center'][1]
85
+ const Z0 = image_data.properties['pers:perspective_center'][2]
86
+ const Ome = image_data.properties['pers:omega']
87
+ const Phi = image_data.properties['pers:phi']
88
+ const Kap = image_data.properties['pers:kappa']
89
+
90
+ // recalc values
91
+ var c = ci*(-1)
92
+ var dimX = dimXi*pix/2*(-1)
93
+ var dimY = dimYi*pix/2*(-1)
94
+
95
+ // ... Do calculations ...
96
+ var o = radians(Ome)
97
+ var p = radians(Phi)
98
+ var k = radians(Kap)
99
+ var D11 = Math.cos(p) * Math.cos(k)
100
+ var D12 = - Math.cos(p) * Math.sin(k)
101
+ var D13 = Math.sin(p)
102
+ var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
103
+ var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
104
+ var D23 = - Math.sin(o) * Math.cos(p)
105
+ var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
106
+ var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
107
+ var D33 = Math.cos(o) * Math.cos(p)
108
+
109
+ var x_dot = (-1)*c*((D11*(X-X0)+D21*(Y-Y0)+D31*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
110
+ var y_dot = (-1)*c*((D12*(X-X0)+D22*(Y-Y0)+D32*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
111
+
112
+ var col = ((x_dot-xx0)+(dimX))*(-1)/pix
113
+ var row = ((y_dot-yy0)+(dimY))*(-1)/pix
114
+
115
+ return [
116
+ Math.round(col),
117
+ Math.round(row)
118
+ ]
119
+ }
120
+
121
+ /**
122
+ * (DEPRECATED - Use getImageXY) Converts lat,lon coordinates to x,y coordinates within a specific image
123
+ * @param {Object} image_data - skraafoto-stac-api image data
124
+ * @param {Number} Y - northing
125
+ * @param {Number} X - easting
126
+ * @param {Number} [Z] - elevation (geoide)
127
+ * @returns {array} [x,y] Column/row image coordinate
128
+ */
129
+ function world2image(image_data, X, Y, Z = 0) {
130
+ return getImageXY(image_data, X, Y, Z)
131
+ }
132
+
133
+ /** Converts degress to radians */
134
+ function radians(degrees) {
135
+ return degrees * (Math.PI / 180)
136
+ }
137
+
138
+ /** (DEPRECATED - Use getWorldXYZ) Iterates guessing at a world coordinate using image coordinates and elevation info */
139
+ function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
140
+
141
+ // Get coordinates based on current z value
142
+ const worldcoor = image2world(image_data,col,row,z)
143
+
144
+ i--
145
+ if (i <= 0) {
146
+ // max iterations reached
147
+ return [worldcoor, 0, count]
148
+ }
149
+
150
+ // Get new z value from coordinates
151
+ return getZ(worldcoor[0],worldcoor[1], auth).then((new_z) => {
152
+
153
+ // How big is the different between z and new z?
154
+ const delta = Math.abs(new_z - z)
155
+
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
162
+ return [worldcoor, delta, count]
163
+ }
164
+ })
165
+ }
166
+
167
+ /**
168
+ * (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.
169
+ * @param {object} image_data - image item data from STAC API
170
+ * @param {number} col - image x coordinate (from left to right)
171
+ * @param {number} row - image y coordinate (from top to bottom)
172
+ * @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
173
+ * @param {number} [limit] - result may be inaccurate within this limit. Default is 1.
174
+ * @returns {array} [world coordindates (array), elevation discrepancy, calculation iterations]
175
+ */
176
+ function iterate(image_data, col, row, auth, limit = 1) {
177
+ let i = 20 // Maximum number of iterations allowed
178
+ return iterateRecursive(image_data, col, row, 0.5, 0, limit, auth, i)
179
+ }
180
+
181
+ async function compareZ(options) {
182
+
183
+ // Get coordinates based on current z value
184
+ const world_xyz = image2world(options.image, options.xy[0], options.xy[1], options.z)
185
+ // Get new z value from coordinates
186
+ world_xyz[2] = await getElevation(world_xyz[0], world_xyz[1], options.terrain)
187
+ // How big is the different between z and new z?
188
+ const delta = Math.abs(world_xyz[2] - options.z)
189
+
190
+ // Check delta vs. limit. If the difference is too big, try building coordinates with the new z
191
+ // Make sure the recursive loop ends after 20 iterations even if no precise z is found
192
+ if (options.iteration < 20 && delta > options.limit) {
193
+ options.iteration++
194
+ options.z = world_xyz[2]
195
+ return compareZ(options)
196
+ } else {
197
+ return world_xyz
198
+ }
199
+ }
200
+
201
+ /** 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.
202
+ * @param {object} options
203
+ * @param {array} options.xy - image coordinates
204
+ * @param {object} options.image - STAC API item
205
+ * @param {object} options.terrain - GeoTIFF object from getTerrainGeoTIFF() output
206
+ * @param {number} precision - Precision in meters for "good enough"
207
+ * @returns {array} Array with world coordinates (indexes 0 and 1) and elevation (index 2). Coordinates are EPSG:25832 format.
208
+ */
209
+ async function getWorldXYZ(options, precision = 0.3) {
210
+
211
+ const best_world_xyz = compareZ({
212
+ image: options.image,
213
+ terrain: options.terrain,
214
+ xy: options.xy,
215
+ z: 0,
216
+ limit: precision,
217
+ iteration: 1
218
+ })
219
+
220
+ return best_world_xyz
221
+ }
222
+
223
+
224
+ /**
225
+ * Iterates over an array of STAC API image items
226
+ * and returns a total bounding box for all the images
227
+ */
228
+ function getTotalBbox(images) {
229
+ const initialBbox = images[0].features[0].bbox
230
+
231
+ return images.reduce((bbox, image) => {
232
+ const imageBbox = image.features[0].bbox
233
+ if (imageBbox[0] < bbox[0]) {
234
+ bbox[0] = imageBbox[0]
235
+ }
236
+ if (imageBbox[1] < bbox[1]) {
237
+ bbox[1] = imageBbox[1]
238
+ }
239
+ if (imageBbox[2] > bbox[2]) {
240
+ bbox[2] = imageBbox[2]
241
+ }
242
+ if (imageBbox[3] > bbox[3]) {
243
+ bbox[3] = imageBbox[3]
244
+ }
245
+ return bbox
246
+ }, initialBbox)
247
+ }
248
+
249
+ export {
250
+ image2world,
251
+ world2image,
252
+ getWorldXYZ,
253
+ getImageXY,
254
+ iterate,
255
+ getTotalBbox
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataforsyningen/saul",
3
- "version": "3.6.0",
3
+ "version": "4.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",
@@ -3,7 +3,7 @@
3
3
  import auth from '../config.js'
4
4
 
5
5
  import assert from 'assert'
6
- import { getElevation, get, getTerrainGeoTIFF, getZ, getWorldXYZ, world2image } from '../index.js'
6
+ import { getElevation, get, getTerrainGeoTIFF, getZ, getWorldXYZ, world2image, getTerrainByBbox } from '../index.js'
7
7
 
8
8
  console.log('---------------')
9
9
  console.log('Elevation tests')
@@ -107,3 +107,12 @@ get(url_stac)
107
107
  .catch((err) => {
108
108
  console.log('THE ERROR', err)
109
109
  })
110
+
111
+ // Testing getTerrainByBbox()
112
+ try {
113
+ const gtiff = await getTerrainByBbox([542929.6729020511, 6153925.479277819, 544463.7189110086, 6155242.76541582], auth)
114
+ assert(gtiff.fileDirectory.ImageWidth === 500, 'GeoTIFF has weird dimensions')
115
+ console.log('Test getTerrainByBbox() => OK')
116
+ } catch(error) {
117
+ console.error(error)
118
+ }
package/example.html DELETED
@@ -1,58 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width">
6
- <title>Saul modules example page</title>
7
- </head>
8
- <body>
9
-
10
- <h1>Saul modules example</h1>
11
-
12
- <p>Just opening this example.html file might not work becuase of CORS restrictions when trying to access STAC API.</p>
13
- <p>You should run this HTML file from a simple server like Python's <code>http.server</code></p>
14
-
15
- <h2>This is the result</h2>
16
- <div id="result"></div>
17
-
18
- <script type="module">
19
-
20
- // You should add your own config.js with proper tokens, etc.
21
- // Check config.js.example for info on how to set it up
22
- import auth from './config.js'
23
-
24
- import {image2world, world2image, getZ, iterate} from './modules/saul-core.js'
25
- import {getSTAC} from './modules/api.js'
26
-
27
- let xcoor = 579782.75
28
- let ycoor = 6131096.03
29
-
30
- const result_display_element = document.getElementById('result')
31
- let apidata = await getSTAC('/collections/skraafotos2019/items/2019_83_37_2_0046_00001113', auth)
32
-
33
- // Let's call an imported method
34
- let zcoor = await getZ(xcoor, ycoor, auth)
35
-
36
- let image_coor = world2image(apidata, xcoor, ycoor, zcoor)
37
-
38
- let col = image_coor[0]
39
- let row = image_coor[1]
40
-
41
- let world_coor = image2world(apidata, col, row, zcoor)
42
-
43
- col = 7375
44
- row = 5382
45
- let ite = await iterate(apidata, col, row, auth, 0.05)
46
-
47
- // Write out result for the user to see
48
- if (image_coor) {
49
- result.innerText = xcoor + ' ' + ycoor + ' ' + zcoor + '\n' + image_coor[0] + ' ' + image_coor[1] + '\n' +ite
50
- } else {
51
- result.innerText = 'There was no result'
52
- }
53
-
54
-
55
- </script>
56
-
57
- </body>
58
- </html>
@@ -1,106 +0,0 @@
1
- /**
2
- * Maps a coordinate from the one coordinate system to another based on GeoTiff image dimensions.
3
- * @param {Array} coord - EPSG:25832 X,Y coordinate pair
4
- * @param {Array} bbox - Bounding box within input coordinate systtem. Consists of 2 EPSG:25832 coordinate pairs. Example: [x1,y1,x2,y2]
5
- * @param {Array} imageDimensions - Width and height of a GeoTiff image. This will set the bounding box of the output coordinate system. Note: This coordinate system will have 0,0 set in the top right position of the image.
6
- */
7
- function convertCoordinate(coord, bbox1, imageDimensions) {
8
-
9
- // Extract the coordinates from the bounding boxes
10
- const [x1_min, y1_min, x1_max, y1_max] = bbox1
11
-
12
- // Extract the coordinate to convert
13
- const [x1, y1] = coord
14
-
15
- // Calculate the width and height of both bounding boxes
16
- const width1 = x1_max - x1_min
17
- const height1 = y1_max - y1_min
18
- const width2 = imageDimensions[0]
19
- const height2 = imageDimensions[1]
20
-
21
- // Calculate the relative position in the first coordinate system
22
- const x_relative = (x1 - x1_min) / width1
23
- const y_relative = (y1 - y1_min) / height1
24
-
25
- // Calculate the new position in the second coordinate system
26
- const x2 = Math.abs(x_relative * width2)
27
- const y2 = height2 - Math.abs(y_relative * height2)
28
-
29
- return [x2, y2]
30
- }
31
-
32
- /**
33
- * Calculates nearest elevation for given coordinate using geoTIFF terrain data
34
- * @param {number} x - EPSG:25832 X coordinate
35
- * @param {number} y - EPSG:25832 Y coordinate
36
- * @param {object} geoTiff - GeoTiff data output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
37
- * @returns {number} Elevation in meters
38
- */
39
- async function getElevation(x, y, geoTiff) {
40
-
41
- const bbox = geoTiff.getBoundingBox()
42
- const height = geoTiff.getHeight()
43
- const width = geoTiff.getWidth()
44
-
45
- // Convert lat/lon to geotiff x/y
46
- const [xPx, yPx] = convertCoordinate([x,y], bbox, [width, height])
47
-
48
- // Fetch window of raster values around x/y
49
- const window = [
50
- Math.floor(xPx),
51
- Math.floor(yPx),
52
- Math.ceil(xPx),
53
- Math.ceil(yPx)
54
- ]
55
-
56
- // Fetch elevation value from geoTiff rasters
57
- const elevation_data = await geoTiff.readRasters({
58
- window: window,
59
- fillValue: 0
60
- })
61
-
62
- return elevation_data[0][0]
63
- }
64
-
65
- /** Creates an ASCII visualization of a GeoTiff with terrain elevation data. Useful for display in a terminal.
66
- * NOTE: You may want to use a downsmapled GeoTiff to fit within your terminal window. A GeoTiff pixel is represented by 3 characters each.
67
- * @param {object} gTiff - GeoTiff output from getTerrainGeoTIFF() or getDenmarkGeoTiff() method
68
- */
69
- async function visualizeGeotiff(gTiff) {
70
- console.log('--- GTIFF visualization ---')
71
- const float32Arr = await gTiff.readRasters()
72
- const tiffWidth = gTiff.getWidth()
73
- const rasters = float32Arr[0]
74
- let lines = []
75
- let line = ''
76
- for (let i = 0; i < rasters.length; i++) {
77
- if (i % tiffWidth === 0) {
78
- lines.push(line)
79
- line = ''
80
- }
81
- line += zeroPadNumber(rasters[i])
82
- }
83
- for (const l of lines) {
84
- console.log(l)
85
- }
86
- }
87
-
88
- /** Converts numbers to strings in a 3-letter format. Ex. `002`, `0-3` (for negative numbers), or `123` */
89
- function zeroPadNumber(input) {
90
- if (input === 0) {
91
- return "..."
92
- } else if (input < 0) {
93
- return `0${input.toFixed(0)}`
94
- } else if (input < 10) {
95
- return `00${Math.floor(input)}`
96
- } else if (input < 100) {
97
- return `0${Math.floor(input)}`
98
- } else {
99
- return `${Math.floor(input)}`
100
- }
101
- }
102
-
103
- export {
104
- getElevation,
105
- visualizeGeotiff
106
- }
File without changes