@dataforsyningen/saul 3.0.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/.github/workflows/release-package.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +45 -0
- package/config.js.example +11 -0
- package/example.html +58 -0
- package/index.js +21 -0
- package/modules/api.js +186 -0
- package/modules/saul-core.js +181 -0
- package/modules/saul-projection.js +33 -0
- package/package.json +32 -0
- package/test/saul-core.test.js +119 -0
- package/test/saul-projection.test.js +34 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Node.js Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v3
|
|
12
|
+
- uses: actions/setup-node@v3
|
|
13
|
+
with:
|
|
14
|
+
node-version: 16
|
|
15
|
+
- run: npm ci
|
|
16
|
+
# - run: npm test
|
|
17
|
+
|
|
18
|
+
# Important note:
|
|
19
|
+
# This only works in github actions when you've added an "automation token" by the name of "NPM_TOKEN" to "repository secrets"
|
|
20
|
+
publish-npm:
|
|
21
|
+
needs: build
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v3
|
|
25
|
+
- uses: actions/setup-node@v3
|
|
26
|
+
with:
|
|
27
|
+
node-version: 16
|
|
28
|
+
registry-url: https://registry.npmjs.org
|
|
29
|
+
- run: npm publish --access public
|
|
30
|
+
env:
|
|
31
|
+
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 SDFI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# SAUL
|
|
2
|
+
|
|
3
|
+
This is a collection of stand-alone methods for photogrammetry calculations and API interactions that are primarily used by https://github.com/SDFIdk/skraafoto_frontend
|
|
4
|
+
|
|
5
|
+
You can use these as building blocks for your own web application that wants to interface with STAC API for Danske Skråfotos.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
1. Install iva NPM:
|
|
10
|
+
```
|
|
11
|
+
npm i --save @dataforsyningen/saul
|
|
12
|
+
```
|
|
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`
|
|
15
|
+
|
|
16
|
+
## How to use
|
|
17
|
+
|
|
18
|
+
This is an example of how to import and use those utilities in Javascript:
|
|
19
|
+
```
|
|
20
|
+
import auth from './config.js' // This is the path to your configuration file
|
|
21
|
+
import {getSTAC} from '@dataforsyningen/saul'
|
|
22
|
+
|
|
23
|
+
let response = await getSTAC('/some-stac-endpoint', auth)
|
|
24
|
+
console.log(response)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API docs
|
|
28
|
+
|
|
29
|
+
To read the docs, first build the documentation with JSDoc:
|
|
30
|
+
```
|
|
31
|
+
npm run docs
|
|
32
|
+
```
|
|
33
|
+
A `docs` directory will be created with a bunch of HTML files containing the documentation.
|
|
34
|
+
|
|
35
|
+
## How to test
|
|
36
|
+
|
|
37
|
+
From within `node_modules/@dataforsyningen/saul` you can run the `test` command to check that saul core features are still working.
|
|
38
|
+
```
|
|
39
|
+
npm run test
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Acknowledgements
|
|
43
|
+
|
|
44
|
+
SAUL is made available under the MIT license by
|
|
45
|
+
The Skråfoto Team @ [SDFI](https://sdfi.dk/)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
|
|
3
|
+
// STAC TOKEN can be aquired from https://dataforsyningen.dk/
|
|
4
|
+
API_STAC_TOKEN: "",
|
|
5
|
+
API_STAC_BASEURL: "https://api.dataforsyningen.dk/skraafotoapi",
|
|
6
|
+
|
|
7
|
+
// DHM URL and auth can be aquired from https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/koter/
|
|
8
|
+
API_DHM_BASEURL: "https://services.datafordeler.dk/DHMTerraen/DHMKoter/1.0.0/GEOREST/HentKoter",
|
|
9
|
+
API_DHM_USERNAME: "",
|
|
10
|
+
API_DHM_PASSWORD: ""
|
|
11
|
+
}
|
package/example.html
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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, passwords, 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>
|
package/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Import/export modules with their public methods
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
image2world,
|
|
5
|
+
world2image,
|
|
6
|
+
getZ,
|
|
7
|
+
iterate
|
|
8
|
+
} from './modules/saul-core.js'
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
postSTAC,
|
|
12
|
+
getSTAC,
|
|
13
|
+
getDHM,
|
|
14
|
+
get,
|
|
15
|
+
post
|
|
16
|
+
} from './modules/api.js'
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
epsg25832proj,
|
|
20
|
+
createTranslator
|
|
21
|
+
} from './modules/saul-projection.js'
|
package/modules/api.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAUL API utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
let error_msg
|
|
6
|
+
let load_stack = []
|
|
7
|
+
|
|
8
|
+
// These custom events can only be created in a browser environment
|
|
9
|
+
const loadstart = isBrowser() ? new CustomEvent('loadstart') : null
|
|
10
|
+
const loadend = isBrowser() ? new CustomEvent('loadend') : null
|
|
11
|
+
const loaderror = isBrowser() ? new CustomEvent('loaderror', {
|
|
12
|
+
detail: {
|
|
13
|
+
name: getErrorMsg()
|
|
14
|
+
}
|
|
15
|
+
}) : null
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/** Check if code is run in a Browser or Node enviroment */
|
|
19
|
+
function isBrowser() {
|
|
20
|
+
// Check if the environment is Node.js
|
|
21
|
+
if (typeof process === "object" &&
|
|
22
|
+
typeof require === "function") {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
// Check if the environment is
|
|
26
|
+
// a Service worker
|
|
27
|
+
if (typeof importScripts === "function") {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
// Check if the environment is a Browser
|
|
31
|
+
if (typeof window === "object") {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Sends 'loadstart' event if nothing else is currently loading */
|
|
37
|
+
function startLoading() {
|
|
38
|
+
if (isBrowser() && load_stack.length < 1) {
|
|
39
|
+
document.dispatchEvent(loadstart)
|
|
40
|
+
}
|
|
41
|
+
load_stack.push(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Sends 'loadend' event if current loads have finished */
|
|
45
|
+
function endLoading() {
|
|
46
|
+
load_stack.pop()
|
|
47
|
+
if (isBrowser() && load_stack.length < 1) {
|
|
48
|
+
document.dispatchEvent(loadend)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Sends 'loaderror' event and resets load status */
|
|
53
|
+
function interruptLoading() {
|
|
54
|
+
load_stack = []
|
|
55
|
+
if (isBrowser()) {
|
|
56
|
+
document.dispatchEvent(loaderror)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Getter for error messages */
|
|
61
|
+
function getErrorMsg() {
|
|
62
|
+
return error_msg
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns JSON response data */
|
|
66
|
+
function HttpResponseHandler(response) {
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
error_msg = response.status
|
|
69
|
+
interruptLoading()
|
|
70
|
+
throw new Error(`HTTP error! Status: ${ response.status }`)
|
|
71
|
+
}
|
|
72
|
+
// We assume the returned data is JSON
|
|
73
|
+
return response.json()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* GET HTTP responsee from API
|
|
78
|
+
* @param {string} url - API service URL, including endpoint paths and query parameters.
|
|
79
|
+
* @param {object} [config] - Custom request configs. See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options
|
|
80
|
+
* @returns {object} response object
|
|
81
|
+
*/
|
|
82
|
+
function get(url, config = {}) {
|
|
83
|
+
if (!url) {
|
|
84
|
+
console.error('Could not fetch data. Missing API URL')
|
|
85
|
+
} else {
|
|
86
|
+
startLoading()
|
|
87
|
+
return fetch( url, {
|
|
88
|
+
...config,
|
|
89
|
+
method: 'GET'
|
|
90
|
+
})
|
|
91
|
+
.then((response) => {
|
|
92
|
+
return HttpResponseHandler(response)
|
|
93
|
+
})
|
|
94
|
+
.then((response) => {
|
|
95
|
+
// Finally, return the parsed JSON response
|
|
96
|
+
endLoading()
|
|
97
|
+
return response
|
|
98
|
+
})
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
// ... unless something goes wrong
|
|
101
|
+
console.error(`Fetch error: ${error}`)
|
|
102
|
+
endLoading()
|
|
103
|
+
return error
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* POST HTTP request to API
|
|
110
|
+
* @param {string} url - API service URL, including endpoint paths and query parameters.
|
|
111
|
+
* @param {object} requestbody - Request data
|
|
112
|
+
* @param {string} token - Authentication token from Dataforsyningen
|
|
113
|
+
* @returns {object} response object
|
|
114
|
+
*/
|
|
115
|
+
function post(url, requestbody, token) {
|
|
116
|
+
if (!url || !token || !requestbody) {
|
|
117
|
+
console.error('Could not fetch data. Missing API token, request body, or URL')
|
|
118
|
+
} else {
|
|
119
|
+
startLoading()
|
|
120
|
+
return fetch( url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'token': token,
|
|
124
|
+
'Content-Type': 'application/json'
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify(requestbody)
|
|
127
|
+
})
|
|
128
|
+
.then((response) => {
|
|
129
|
+
return HttpResponseHandler(response)
|
|
130
|
+
})
|
|
131
|
+
.then((response) => {
|
|
132
|
+
// Finally, return the parsed JSON response
|
|
133
|
+
endLoading()
|
|
134
|
+
return response
|
|
135
|
+
})
|
|
136
|
+
.catch((error) => {
|
|
137
|
+
// ... unless something goes wrong
|
|
138
|
+
console.error(`Fetch error: ${error}`)
|
|
139
|
+
endLoading()
|
|
140
|
+
return error
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* API method to fetch data from DHM
|
|
147
|
+
* @param {string} query - DHM API query. Find details at https://datafordeler.dk/dataoversigt/danmarks-hoejdemodel-dhm/koter/
|
|
148
|
+
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
149
|
+
* @returns {object} Response data
|
|
150
|
+
*/
|
|
151
|
+
function getDHM(query, auth) {
|
|
152
|
+
const auth_params = `&username=${auth.API_DHM_USERNAME}&password=${auth.API_DHM_PASSWORD}`
|
|
153
|
+
return get(encodeURI(auth.API_DHM_BASEURL + query + auth_params))
|
|
154
|
+
.then((data) => data)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* API method to GET data from STAC API
|
|
159
|
+
* @param {string} query - STAC API query.
|
|
160
|
+
* @param {{API_STAC_BASEURL: string, API_STAC_TOKEN: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
161
|
+
* @returns {object} Response data
|
|
162
|
+
*/
|
|
163
|
+
function getSTAC(query, auth) {
|
|
164
|
+
return get(auth.API_STAC_BASEURL + query, {headers: {'token': auth.API_STAC_TOKEN}})
|
|
165
|
+
.then((data) => data)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* API method to POST data to STAC API
|
|
170
|
+
* @param {string} endpoint - STAC API endpoint. Ex. `/collections`
|
|
171
|
+
* @param {object} data - request body
|
|
172
|
+
* @param {{API_STAC_BASEURL: string, API_STAC_TOKEN: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
173
|
+
* @returns {object} Reponse data
|
|
174
|
+
*/
|
|
175
|
+
function postSTAC(endpoint, data, auth) {
|
|
176
|
+
return post(auth.API_STAC_BASEURL + endpoint, data, auth.API_STAC_TOKEN)
|
|
177
|
+
.then((data) => data)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
get,
|
|
182
|
+
post,
|
|
183
|
+
getSTAC,
|
|
184
|
+
postSTAC,
|
|
185
|
+
getDHM
|
|
186
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAUL photogrammetry utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDHM } from './api.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[X,Y,Z]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Converts lat,lon coordinates to x,y coordinates within a specific image
|
|
64
|
+
* @param {Object} image_data - skraafoto-stac-api image data
|
|
65
|
+
* @param {Number} Y - northing
|
|
66
|
+
* @param {Number} X - easting
|
|
67
|
+
* @param {Number} [Z] - elevation (geoide)
|
|
68
|
+
* @returns {array} [x,y] Column/row image coordinate
|
|
69
|
+
*/
|
|
70
|
+
function world2image(image_data, X, Y, Z = 0) {
|
|
71
|
+
|
|
72
|
+
// constants pulled from image_data
|
|
73
|
+
const xx0 = image_data.properties['pers:interior_orientation'].principal_point_offset[0]
|
|
74
|
+
const yy0 = image_data.properties['pers:interior_orientation'].principal_point_offset[1]
|
|
75
|
+
const ci = image_data.properties['pers:interior_orientation'].focal_length
|
|
76
|
+
const pix = image_data.properties['pers:interior_orientation'].pixel_spacing[0]
|
|
77
|
+
const dimXi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[0]
|
|
78
|
+
const dimYi = image_data.properties['pers:interior_orientation'].sensor_array_dimensions[1]
|
|
79
|
+
const X0 = image_data.properties['pers:perspective_center'][0]
|
|
80
|
+
const Y0 = image_data.properties['pers:perspective_center'][1]
|
|
81
|
+
const Z0 = image_data.properties['pers:perspective_center'][2]
|
|
82
|
+
const Ome = image_data.properties['pers:omega']
|
|
83
|
+
const Phi = image_data.properties['pers:phi']
|
|
84
|
+
const Kap = image_data.properties['pers:kappa']
|
|
85
|
+
|
|
86
|
+
// recalc values
|
|
87
|
+
var c = ci*(-1)
|
|
88
|
+
var dimX = dimXi*pix/2*(-1)
|
|
89
|
+
var dimY = dimYi*pix/2*(-1)
|
|
90
|
+
|
|
91
|
+
// ... Do calculations ...
|
|
92
|
+
var o = radians(Ome)
|
|
93
|
+
var p = radians(Phi)
|
|
94
|
+
var k = radians(Kap)
|
|
95
|
+
var D11 = Math.cos(p) * Math.cos(k)
|
|
96
|
+
var D12 = - Math.cos(p) * Math.sin(k)
|
|
97
|
+
var D13 = Math.sin(p)
|
|
98
|
+
var D21 = Math.cos(o) * Math.sin(k) + Math.sin(o) * Math.sin(p) * Math.cos(k)
|
|
99
|
+
var D22 = Math.cos(o) * Math.cos(k) - Math.sin(o) * Math.sin(p) * Math.sin(k)
|
|
100
|
+
var D23 = - Math.sin(o) * Math.cos(p)
|
|
101
|
+
var D31 = Math.sin(o) * Math.sin(k) - Math.cos(o) * Math.sin(p) * Math.cos(k)
|
|
102
|
+
var D32 = Math.sin(o) * Math.cos(k) + Math.cos(o) * Math.sin(p) * Math.sin(k)
|
|
103
|
+
var D33 = Math.cos(o) * Math.cos(p)
|
|
104
|
+
|
|
105
|
+
var x_dot = (-1)*c*((D11*(X-X0)+D21*(Y-Y0)+D31*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
|
|
106
|
+
var y_dot = (-1)*c*((D12*(X-X0)+D22*(Y-Y0)+D32*(Z-Z0))/(D13*(X-X0)+D23*(Y-Y0)+D33*(Z-Z0)))
|
|
107
|
+
|
|
108
|
+
var col = ((x_dot-xx0)+(dimX))*(-1)/pix
|
|
109
|
+
var row = ((y_dot-yy0)+(dimY))*(-1)/pix
|
|
110
|
+
|
|
111
|
+
return [col, row]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Converts degress to radians */
|
|
115
|
+
function radians(degrees) {
|
|
116
|
+
return degrees * (Math.PI / 180)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fetches elevation based on X,Y coordinates using DHM/Koter endpoint
|
|
121
|
+
* @param {number} xcoor - EPSG:25832 X coordinate
|
|
122
|
+
* @param {number} ycoor - EPSG:25832 Y coordinate
|
|
123
|
+
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
124
|
+
* @returns {number} Elevation in meters
|
|
125
|
+
*/
|
|
126
|
+
async function getZ(xcoor, ycoor, auth) {
|
|
127
|
+
let zcoor_data = await getDHM(`?geop=POINT(${xcoor} ${ycoor})&elevationmodel=dtm`, auth)
|
|
128
|
+
let z = zcoor_data.HentKoterRespons.data[0].kote
|
|
129
|
+
return z
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Iterates guessing at a world coordinate using image coordinates and elevation info */
|
|
133
|
+
function iterateRecursive(image_data, col, row, z, count, limit, auth, i) {
|
|
134
|
+
|
|
135
|
+
// Get coordinates based on current z value
|
|
136
|
+
const worldcoor = image2world(image_data,col,row,z)
|
|
137
|
+
|
|
138
|
+
i--
|
|
139
|
+
if (i <= 0) {
|
|
140
|
+
// max iterations reached
|
|
141
|
+
return [worldcoor, 0, count]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get new z value from coordinates
|
|
145
|
+
return getZ(worldcoor[0],worldcoor[1], auth).then((new_z) => {
|
|
146
|
+
|
|
147
|
+
// How big is the different between z and new z?
|
|
148
|
+
const delta = Math.abs(new_z - z)
|
|
149
|
+
|
|
150
|
+
if (delta >= limit) {
|
|
151
|
+
// If the difference is too big, try building coordinates with the new z
|
|
152
|
+
count = count + 1
|
|
153
|
+
return iterateRecursive(image_data, col, row, new_z, count, limit, auth, i)
|
|
154
|
+
} else {
|
|
155
|
+
// If the difference is small, return coordinates
|
|
156
|
+
return [worldcoor, delta, count]
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
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
|
+
* @param {object} image_data - image item data from STAC API
|
|
164
|
+
* @param {number} col - image x coordinate (from left to right)
|
|
165
|
+
* @param {number} row - image y coordinate (from top to bottom)
|
|
166
|
+
* @param {{API_DHM_BASEURL: string, API_DHM_USERNAME: string, API_DHM_PASSWORD: string}} auth - API autentication data. See ../config.js.example for reference.
|
|
167
|
+
* @param {number} [limit] - result may be inaccurate within this limit. Default is 1.
|
|
168
|
+
* @returns {array} [world coordindates (array), elevation discrepancy, calculation iterations]
|
|
169
|
+
*/
|
|
170
|
+
function iterate(image_data, col, row, auth, limit = 1) {
|
|
171
|
+
let i = 20 // Maximum number of iterations allowed
|
|
172
|
+
return iterateRecursive(image_data, col, row, 0.5, 0, limit, auth, i)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
image2world,
|
|
177
|
+
world2image,
|
|
178
|
+
getZ,
|
|
179
|
+
iterate
|
|
180
|
+
}
|
|
181
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAUL map projection and transformation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import proj4 from 'proj4'
|
|
6
|
+
|
|
7
|
+
/** Defines an EPSG:25832 projection using proj4.
|
|
8
|
+
* Learn about EPSG:25832 at https://epsg.io/25832 and proj4 at http://proj4js.org/
|
|
9
|
+
* EPSG:25832 definition can be requested at Definition can be requested from https://epsg.io/?format=json&q=25832
|
|
10
|
+
* @param {object} proj4object - a proj4 instance
|
|
11
|
+
* @returns {object} Returns same proj4 instance with an added EPSG:25832 projection definition
|
|
12
|
+
*/
|
|
13
|
+
function epsg25832proj(proj4object) {
|
|
14
|
+
proj4object.defs('EPSG:25832', "+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs")
|
|
15
|
+
return proj4object
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Creates an object with methods for translating coordinates from one projection to the other
|
|
19
|
+
* @param {string} [projection1] - from Projection
|
|
20
|
+
* @param {string} [projection2] - to projection
|
|
21
|
+
* @returns {object} An object with two methods is returned, its methods are `forward` which projects from the first projection to the second and `inverse` which projects from the second to the first.
|
|
22
|
+
*/
|
|
23
|
+
function createTranslator(projection1 = 'WGS84', projection2 = 'EPSG:25832') {
|
|
24
|
+
if (!proj4.defs['EPSG:25832']) {
|
|
25
|
+
epsg25832proj(proj4) // Define EPSG:25832 projection if missing
|
|
26
|
+
}
|
|
27
|
+
return proj4(projection1, projection2)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
epsg25832proj,
|
|
32
|
+
createTranslator
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dataforsyningen/saul",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "This is a collection of stand-alone methods for photogrammetry calculations and API interactions for 'Danske Skråfotos'.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo 'TESTS WILL ONLY RUN UNDER NODE v18' && node ./test/saul-projection.test.js && node ./test/saul-core.test.js",
|
|
9
|
+
"docs": "jsdoc ./modules/*.js --destination docs"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/SDFIdk/saul.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"skråfoto",
|
|
17
|
+
"SDFI",
|
|
18
|
+
"saul"
|
|
19
|
+
],
|
|
20
|
+
"author": "Styrelsen for Dataforsyning og Infrastruktur (https://sdfi.dk/)",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/SDFIdk/saul/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/SDFIdk/saul#readme",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"jsdoc": "^3.6.10"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"proj4": "^2.8.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Testing saul-core methods
|
|
2
|
+
|
|
3
|
+
// You should add your own config.js with proper tokens, passwords, etc.
|
|
4
|
+
// Check config.js.example for info on how to set it up
|
|
5
|
+
import auth from '../config.js'
|
|
6
|
+
|
|
7
|
+
import assert from 'assert'
|
|
8
|
+
import {getSTAC} from '../modules/api.js'
|
|
9
|
+
import {world2image, image2world, getZ, iterate} from '../modules/saul-core.js'
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
const item = await getSTAC('/collections/skraafotos2019/items/2019_83_37_2_0046_00001113', auth)
|
|
13
|
+
const world_x = 580341
|
|
14
|
+
const world_y = 6130925
|
|
15
|
+
const image_x = 7373
|
|
16
|
+
const image_y = 3205
|
|
17
|
+
const world_elevation = 32.7199
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const item = await getSTAC('/collections/skraafotos2021/items/2021_83_36_4_0013_00003824', auth)
|
|
21
|
+
const world_x = 587739
|
|
22
|
+
const world_y = 6130951
|
|
23
|
+
const image_x = 41839.698939
|
|
24
|
+
const image_y = 3204.326930
|
|
25
|
+
const world_elevation = 38.874336
|
|
26
|
+
|
|
27
|
+
function is_equalIsh(num1, num2) {
|
|
28
|
+
const deviation = 0.05
|
|
29
|
+
if (Math.abs(num1 - num2) > deviation) {
|
|
30
|
+
return false
|
|
31
|
+
} else {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Test getZ
|
|
37
|
+
try {
|
|
38
|
+
|
|
39
|
+
let elevation = await getZ(world_x, world_y, auth)
|
|
40
|
+
|
|
41
|
+
assert(is_equalIsh(elevation, world_elevation), "getZ fail: This is not the Z value we were hoping for.")
|
|
42
|
+
|
|
43
|
+
console.log("Test getZ OK")
|
|
44
|
+
|
|
45
|
+
} catch(error) {
|
|
46
|
+
console.error(error)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Test iterate
|
|
50
|
+
try {
|
|
51
|
+
|
|
52
|
+
let ite = await iterate(item, image_x, image_y, auth, 0.05)
|
|
53
|
+
|
|
54
|
+
assert(is_equalIsh(ite[0][0], world_x), "iterate fail: The coordinate values are not equal") //test if the values are equal
|
|
55
|
+
assert(is_equalIsh(ite[0][1], world_y), "iterate fail: The coordinate values are not equal") //test if the values are equal
|
|
56
|
+
|
|
57
|
+
console.log("Test iterate OK")
|
|
58
|
+
|
|
59
|
+
} catch(error) {
|
|
60
|
+
console.error(error)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Test world2image
|
|
64
|
+
try {
|
|
65
|
+
|
|
66
|
+
let xy = world2image(item, world_x, world_y, world_elevation)
|
|
67
|
+
|
|
68
|
+
assert(is_equalIsh(xy[0], image_x), "world2image fail: The x coordinate values are not equal")
|
|
69
|
+
assert(is_equalIsh(xy[1], image_y), "world2image fail: The y coordinate values are not equal")
|
|
70
|
+
|
|
71
|
+
console.log("Test world2image OK")
|
|
72
|
+
|
|
73
|
+
} catch(error) {
|
|
74
|
+
console.error(error)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Test image2world
|
|
78
|
+
try {
|
|
79
|
+
|
|
80
|
+
let coords = image2world(item, image_x, image_y, world_elevation)
|
|
81
|
+
|
|
82
|
+
assert(is_equalIsh(coords[0], world_x), "image2world fail: The longitude values are not equal")
|
|
83
|
+
assert(is_equalIsh(coords[1], world_y), "image2world fail: The latitude values are not equal")
|
|
84
|
+
|
|
85
|
+
console.log("Test image2world OK")
|
|
86
|
+
|
|
87
|
+
} catch(error) {
|
|
88
|
+
console.error(error)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Test world2image > image2world
|
|
92
|
+
try {
|
|
93
|
+
|
|
94
|
+
let xy1 = world2image(item, world_x, world_y, world_elevation)
|
|
95
|
+
let coords1 = image2world(item, xy1[0], xy1[1], world_elevation)
|
|
96
|
+
|
|
97
|
+
assert(is_equalIsh(coords1[0], world_x), "world2image > image2world fail: The longitude values are not equal")
|
|
98
|
+
assert(is_equalIsh(coords1[1], world_y), "world2image > image2world fail: The latitude values are not equal")
|
|
99
|
+
|
|
100
|
+
console.log("Test world2image > image2world OK")
|
|
101
|
+
|
|
102
|
+
} catch(error) {
|
|
103
|
+
console.error(error)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Test world2image > iterate
|
|
107
|
+
try {
|
|
108
|
+
|
|
109
|
+
let xy2 = world2image(item, world_x, world_y, world_elevation)
|
|
110
|
+
let coords2 = iterate(item, xy2[0], xy2[1], auth)
|
|
111
|
+
|
|
112
|
+
assert(is_equalIsh(coords2[0], world_x), "world2image > iterate fail: The longitude values are not equal")
|
|
113
|
+
assert(is_equalIsh(coords2[1], world_y), "world2image > iterate fail: The latitude values are not equal")
|
|
114
|
+
|
|
115
|
+
console.log("Test world2image > iterate OK")
|
|
116
|
+
|
|
117
|
+
} catch(error) {
|
|
118
|
+
console.error(error)
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Testing saul-projection methods
|
|
2
|
+
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import proj4 from 'proj4'
|
|
5
|
+
import {epsg25832proj, createTranslator} from '../modules/saul-projection.js'
|
|
6
|
+
|
|
7
|
+
// Test epsg25832proj
|
|
8
|
+
try {
|
|
9
|
+
|
|
10
|
+
const projection = epsg25832proj(proj4)
|
|
11
|
+
|
|
12
|
+
assert.ok(projection.defs['EPSG:25832'], "Projection not defined.")
|
|
13
|
+
|
|
14
|
+
console.log("Test epsg25832proj Ok")
|
|
15
|
+
|
|
16
|
+
} catch(error) {
|
|
17
|
+
console.error(error)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Test createTranslator
|
|
21
|
+
try {
|
|
22
|
+
|
|
23
|
+
const translator = createTranslator()
|
|
24
|
+
const forwarded_coors = translator.forward([10.31,55.37])
|
|
25
|
+
const inverse_coors = translator.inverse(forwarded_coors)
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(forwarded_coors[0], 583023.3267001238, "Failed to forward coordinates")
|
|
28
|
+
assert.strictEqual(inverse_coors[0], 10.31, "Failed to inverse coordinates")
|
|
29
|
+
|
|
30
|
+
console.log("Test createTranslator Ok")
|
|
31
|
+
|
|
32
|
+
} catch(error) {
|
|
33
|
+
console.error(error)
|
|
34
|
+
}
|