@swissgeo/coordinates 1.0.0-beta.2 → 1.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/README.md +118 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.ts +3 -14
- package/dist/index.js +8012 -18103
- package/dist/ol.cjs +1 -0
- package/dist/ol.d.ts +10 -0
- package/dist/ol.js +4467 -0
- package/dist/registerProj4-BuUOcPpF.cjs +23 -0
- package/dist/registerProj4-CwR_kPOz.js +10172 -0
- package/eslint.config.mts +12 -0
- package/index.html +14 -0
- package/package.json +30 -29
- package/setup-vitest.ts +8 -0
- package/src/DevApp.vue +65 -0
- package/src/__test__/coordinatesUtils.spec.ts +178 -0
- package/src/__test__/extentUtils.spec.ts +92 -0
- package/src/coordinatesUtils.ts +188 -0
- package/src/dev.ts +6 -0
- package/src/extentUtils.ts +196 -0
- package/src/index.ts +29 -0
- package/src/ol.ts +52 -0
- package/src/proj/CoordinateSystem.ts +315 -0
- package/src/proj/CoordinateSystemBounds.ts +170 -0
- package/src/proj/CustomCoordinateSystem.ts +58 -0
- package/src/proj/LV03CoordinateSystem.ts +23 -0
- package/src/proj/LV95CoordinateSystem.ts +35 -0
- package/src/proj/StandardCoordinateSystem.ts +22 -0
- package/src/proj/SwissCoordinateSystem.ts +233 -0
- package/src/proj/WGS84CoordinateSystem.ts +97 -0
- package/src/proj/WebMercatorCoordinateSystem.ts +89 -0
- package/src/proj/__test__/CoordinateSystem.spec.ts +63 -0
- package/src/proj/__test__/CoordinateSystemBounds.spec.ts +252 -0
- package/src/proj/__test__/SwissCoordinateSystem.spec.ts +136 -0
- package/src/proj/index.ts +65 -0
- package/src/proj/types.ts +22 -0
- package/src/registerProj4.ts +38 -0
- package/tsconfig.json +4 -0
- package/vite.config.ts +46 -0
- package/dist/index.umd.cjs +0 -29
package/index.html
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swissgeo/coordinates",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Projection definition and coordinates utils for SWISSGEO projects.",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"type": "module",
|
|
@@ -9,42 +9,43 @@
|
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./ol": {
|
|
14
|
+
"types": "./dist/ol.d.ts",
|
|
15
|
+
"import": "./dist/ol.js",
|
|
16
|
+
"require": "./dist/ol.cjs"
|
|
12
17
|
}
|
|
13
18
|
},
|
|
14
|
-
"main": "./dist/index.umd.cjs",
|
|
15
|
-
"module": "./dist/index.js",
|
|
16
|
-
"files": [
|
|
17
|
-
"dist"
|
|
18
|
-
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"lodash": "^4.17.21",
|
|
21
|
-
"@swissgeo/log": "1.0.0
|
|
22
|
-
"@swissgeo/numbers": "1.0.0
|
|
21
|
+
"@swissgeo/log": "1.0.0",
|
|
22
|
+
"@swissgeo/numbers": "1.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@microsoft/api-extractor": "^7.55.
|
|
26
|
-
"@
|
|
27
|
-
"@turf/turf": "^7.2.0",
|
|
25
|
+
"@microsoft/api-extractor": "^7.55.2",
|
|
26
|
+
"@turf/turf": "^7.3.1",
|
|
28
27
|
"@types/chai": "^5.2.3",
|
|
29
28
|
"@types/geojson": "^7946.0.16",
|
|
30
|
-
"@types/lodash": "^4.17.
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"prettier-plugin-jsdoc": "^1.5.0",
|
|
35
|
-
"prettier-plugin-packagejson": "^2.5.19",
|
|
36
|
-
"prettier-plugin-tailwindcss": "^0.7.1",
|
|
29
|
+
"@types/lodash": "^4.17.21",
|
|
30
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
31
|
+
"chai": "^6.2.2",
|
|
32
|
+
"eslint": "^9.39.2",
|
|
37
33
|
"typescript": "^5.9.3",
|
|
38
34
|
"unplugin-dts": "1.0.0-beta.6",
|
|
39
|
-
"vite": "
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
35
|
+
"vite": "7.2.2",
|
|
36
|
+
"vite-plugin-vue-devtools": "^8.0.5",
|
|
37
|
+
"vite-tsconfig-paths": "^6.0.3",
|
|
38
|
+
"vitest": "^4.0.16",
|
|
39
|
+
"vue": "^3.5.26",
|
|
40
|
+
"vue-tsc": "^3.2.2",
|
|
41
|
+
"@swissgeo/config-typescript": "1.0.0",
|
|
42
|
+
"@swissgeo/config-eslint": "1.0.0"
|
|
44
43
|
},
|
|
45
44
|
"peerDependencies": {
|
|
46
|
-
"
|
|
47
|
-
|
|
45
|
+
"proj4": "^2.20.2"
|
|
46
|
+
},
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"ol": "^10.7.0"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "pnpm run type-check && pnpm run generate-types && vite build",
|
|
@@ -53,12 +54,12 @@
|
|
|
53
54
|
"build:int": "pnpm run build --mode integration",
|
|
54
55
|
"build:prod": "pnpm run build --mode production",
|
|
55
56
|
"dev": "vite",
|
|
56
|
-
"generate-types": "tsc --declaration",
|
|
57
|
+
"generate-types": "vue-tsc --declaration",
|
|
57
58
|
"lint": "eslint --fix",
|
|
58
59
|
"lint:no-fix": "eslint",
|
|
59
60
|
"preview": "vite preview",
|
|
60
|
-
"test:unit": "vitest
|
|
61
|
-
"test:unit:watch": "vitest
|
|
62
|
-
"type-check": "tsc
|
|
61
|
+
"test:unit": "vitest run",
|
|
62
|
+
"test:unit:watch": "vitest watch",
|
|
63
|
+
"type-check": "vue-tsc --build"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/setup-vitest.ts
ADDED
package/src/DevApp.vue
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import OLTileLayer from 'ol/layer/Tile'
|
|
3
|
+
import OLMap from 'ol/Map'
|
|
4
|
+
import XYZ from 'ol/source/XYZ'
|
|
5
|
+
import { onMounted } from 'vue'
|
|
6
|
+
|
|
7
|
+
import { getLV95TileGrid, getLV95View, registerSwissGeoProjections as registerOL } from '@/ol'
|
|
8
|
+
import { LV95 } from '@/proj'
|
|
9
|
+
|
|
10
|
+
const pixelKarteFarbeURL = 'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-farbe/default/current/2056/{z}/{x}/{y}.jpeg'
|
|
11
|
+
|
|
12
|
+
function setupOpenLayers() {
|
|
13
|
+
|
|
14
|
+
registerOL()
|
|
15
|
+
|
|
16
|
+
const pixelKarteFarbe = new OLTileLayer({
|
|
17
|
+
source: new XYZ({
|
|
18
|
+
url: pixelKarteFarbeURL,
|
|
19
|
+
projection: LV95.epsg,
|
|
20
|
+
tileGrid: getLV95TileGrid()
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
new OLMap({
|
|
25
|
+
target: 'ol-map',
|
|
26
|
+
layers: [pixelKarteFarbe],
|
|
27
|
+
view: getLV95View()
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMounted(() => {
|
|
32
|
+
setupOpenLayers()
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="dev-app">
|
|
38
|
+
<div class="map-container">
|
|
39
|
+
<h2>OpenLayers</h2>
|
|
40
|
+
<div
|
|
41
|
+
id="ol-map"
|
|
42
|
+
class="map-container-element"
|
|
43
|
+
></div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<style scoped lang="scss">
|
|
49
|
+
.dev-app {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 0;
|
|
52
|
+
left: 0;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 100%;
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-rows: 1fr 1fr;
|
|
57
|
+
}
|
|
58
|
+
.map-container {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
&-element {
|
|
62
|
+
flex-grow: 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { Single3DCoordinate, SingleCoordinate } from '@/coordinatesUtils'
|
|
4
|
+
import type { CoordinateSystem } from '@/proj'
|
|
5
|
+
|
|
6
|
+
import coordinatesUtils from '@/coordinatesUtils'
|
|
7
|
+
import { LV95, WEBMERCATOR, WGS84 } from '@/proj'
|
|
8
|
+
|
|
9
|
+
describe('Unit test for coordinatesUtils', () => {
|
|
10
|
+
describe('toRoundedString', () => {
|
|
11
|
+
it('rounds without decimal if 0 is given as digits', () => {
|
|
12
|
+
expect(coordinatesUtils.toRoundedString([1.49, 2.49], 0)).to.eq(
|
|
13
|
+
'1, 2',
|
|
14
|
+
'it should floor any number lower than .5'
|
|
15
|
+
)
|
|
16
|
+
expect(coordinatesUtils.toRoundedString([1.5, 2.5], 0)).to.eq(
|
|
17
|
+
'2, 3',
|
|
18
|
+
'it should raise any number greater or equal to .5'
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
it('rounds with decimal if a number is given as digits', () => {
|
|
22
|
+
expect(coordinatesUtils.toRoundedString([1.44, 2.44], 1)).to.eq('1.4, 2.4')
|
|
23
|
+
expect(coordinatesUtils.toRoundedString([1.45, 2.45], 1)).to.eq('1.5, 2.5')
|
|
24
|
+
})
|
|
25
|
+
it('correctly enforcers digits when asked for', () => {
|
|
26
|
+
expect(coordinatesUtils.toRoundedString([1.44, 2.44], 5, false, true)).to.eq(
|
|
27
|
+
'1.44000, 2.44000'
|
|
28
|
+
)
|
|
29
|
+
expect(coordinatesUtils.toRoundedString([1, 2], 3, false, true)).to.eq('1.000, 2.000')
|
|
30
|
+
expect(coordinatesUtils.toRoundedString([1234.5678, 1234.5678], 6, true, true)).to.eq(
|
|
31
|
+
"1'234.567800, 1'234.567800"
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('wrapXCoordinates()', () => {
|
|
37
|
+
it('can wrap a single coordinate', () => {
|
|
38
|
+
function testLowerWrap(projection: CoordinateSystem): void {
|
|
39
|
+
const bounds = projection.bounds
|
|
40
|
+
expect(bounds).to.be.an('Object')
|
|
41
|
+
expect(
|
|
42
|
+
coordinatesUtils.wrapXCoordinates(
|
|
43
|
+
[bounds!.lowerX - 1, bounds!.center[1]],
|
|
44
|
+
projection
|
|
45
|
+
)
|
|
46
|
+
).to.deep.equal([bounds!.upperX - 1, bounds!.center[1]])
|
|
47
|
+
}
|
|
48
|
+
testLowerWrap(WGS84)
|
|
49
|
+
testLowerWrap(WEBMERCATOR)
|
|
50
|
+
|
|
51
|
+
function testUpperWrap(projection: CoordinateSystem) {
|
|
52
|
+
const bounds = projection.bounds
|
|
53
|
+
expect(bounds).to.be.an('Object')
|
|
54
|
+
expect(
|
|
55
|
+
coordinatesUtils.wrapXCoordinates(
|
|
56
|
+
[bounds!.upperX + 1, bounds!.center[1]],
|
|
57
|
+
projection
|
|
58
|
+
)
|
|
59
|
+
).to.deep.equal([bounds!.lowerX + 1, bounds!.center[1]])
|
|
60
|
+
}
|
|
61
|
+
testUpperWrap(WGS84)
|
|
62
|
+
testUpperWrap(WEBMERCATOR)
|
|
63
|
+
})
|
|
64
|
+
it('do not wrap if projection is not global (world-wide)', () => {
|
|
65
|
+
const justOffBoundCoordinate: SingleCoordinate = [
|
|
66
|
+
LV95.bounds.lowerX - 1,
|
|
67
|
+
LV95.bounds.center[1],
|
|
68
|
+
]
|
|
69
|
+
expect(coordinatesUtils.wrapXCoordinates(justOffBoundCoordinate, LV95)).to.deep.equal(
|
|
70
|
+
justOffBoundCoordinate
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
it('can wrap every coordinates of an array of coordinates', () => {
|
|
74
|
+
function testMultipleWrap(projection: CoordinateSystem) {
|
|
75
|
+
const bounds = projection.bounds
|
|
76
|
+
expect(bounds).to.be.an('Object')
|
|
77
|
+
const lowOutOfBoundCoordinate: SingleCoordinate = [
|
|
78
|
+
bounds!.lowerX - 1,
|
|
79
|
+
bounds!.center[1],
|
|
80
|
+
]
|
|
81
|
+
const inBoundCoordinate: SingleCoordinate = [bounds!.lowerX, bounds!.center[1]]
|
|
82
|
+
const inBoundCoordinate2: SingleCoordinate = [bounds!.center[0], bounds!.center[1]]
|
|
83
|
+
const inBoundCoordinate3: SingleCoordinate = [bounds!.upperX, bounds!.center[1]]
|
|
84
|
+
const upOutOfBoundCoordinate: SingleCoordinate = [
|
|
85
|
+
bounds!.upperX + 1,
|
|
86
|
+
bounds!.center[1],
|
|
87
|
+
]
|
|
88
|
+
const original = [
|
|
89
|
+
lowOutOfBoundCoordinate,
|
|
90
|
+
inBoundCoordinate,
|
|
91
|
+
inBoundCoordinate2,
|
|
92
|
+
inBoundCoordinate3,
|
|
93
|
+
upOutOfBoundCoordinate,
|
|
94
|
+
]
|
|
95
|
+
const result = coordinatesUtils.wrapXCoordinates(original, projection)
|
|
96
|
+
expect(result).to.be.an('Array').lengthOf(original.length)
|
|
97
|
+
const [first, second, third, fourth, fifth] = result
|
|
98
|
+
expect(first).to.deep.equal([bounds!.upperX - 1, lowOutOfBoundCoordinate[1]])
|
|
99
|
+
expect(second).to.deep.equal(inBoundCoordinate, 'wrong lowerX handling')
|
|
100
|
+
expect(third).to.deep.equal(inBoundCoordinate2, 'wrong center handling')
|
|
101
|
+
expect(fourth).to.deep.equal(inBoundCoordinate3, 'wrong upperX handling')
|
|
102
|
+
expect(fifth).to.deep.equal([bounds!.lowerX + 1, upOutOfBoundCoordinate[1]])
|
|
103
|
+
}
|
|
104
|
+
testMultipleWrap(WGS84)
|
|
105
|
+
testMultipleWrap(WEBMERCATOR)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('unwrapGeometryCoordinates(coordinates)', () => {
|
|
110
|
+
it('returns the input if nothing is required', () => {
|
|
111
|
+
expect(coordinatesUtils.unwrapGeometryCoordinates([])).to.be.an('Array').lengthOf(0)
|
|
112
|
+
const alreadyUnwrappedCoordinates: SingleCoordinate[] = [
|
|
113
|
+
[1, 2],
|
|
114
|
+
[3, 4],
|
|
115
|
+
[5, 6],
|
|
116
|
+
]
|
|
117
|
+
expect(coordinatesUtils.unwrapGeometryCoordinates(alreadyUnwrappedCoordinates)).to.eql(
|
|
118
|
+
alreadyUnwrappedCoordinates
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
it('unwraps when required', () => {
|
|
122
|
+
const expectedOutcome: SingleCoordinate[] = [
|
|
123
|
+
[1, 2],
|
|
124
|
+
[3, 4],
|
|
125
|
+
[5, 6],
|
|
126
|
+
]
|
|
127
|
+
const wrappedCoordinates = [expectedOutcome]
|
|
128
|
+
expect(coordinatesUtils.unwrapGeometryCoordinates(wrappedCoordinates)).to.eql(
|
|
129
|
+
expectedOutcome
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
it('can deal with multiple wrapping array around coordinates', () => {
|
|
133
|
+
const expectedOutcome: SingleCoordinate[] = [
|
|
134
|
+
[1, 2],
|
|
135
|
+
[3, 4],
|
|
136
|
+
[5, 6],
|
|
137
|
+
]
|
|
138
|
+
const wrappedCoordinates = [[expectedOutcome]]
|
|
139
|
+
expect(coordinatesUtils.unwrapGeometryCoordinates(wrappedCoordinates)).to.eql(
|
|
140
|
+
expectedOutcome
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('removeZValues', () => {
|
|
146
|
+
it('returns the input if an empty array is given', () => {
|
|
147
|
+
expect(coordinatesUtils.removeZValues([])).to.eql([])
|
|
148
|
+
})
|
|
149
|
+
it('returns coordinate untouched if they have no Z values', () => {
|
|
150
|
+
const coordinates: SingleCoordinate[] = [
|
|
151
|
+
[1, 2],
|
|
152
|
+
[3, 4],
|
|
153
|
+
[5, 6],
|
|
154
|
+
]
|
|
155
|
+
expect(coordinatesUtils.removeZValues(coordinates)).to.eql(coordinates)
|
|
156
|
+
})
|
|
157
|
+
it('removes Z values when needed', () => {
|
|
158
|
+
const coordinateWithoutZValues: SingleCoordinate[] = [
|
|
159
|
+
[1, 2],
|
|
160
|
+
[3, 4],
|
|
161
|
+
[5, 6],
|
|
162
|
+
]
|
|
163
|
+
expect(
|
|
164
|
+
coordinatesUtils.removeZValues(
|
|
165
|
+
coordinateWithoutZValues.map(
|
|
166
|
+
(coordinate): Single3DCoordinate => [
|
|
167
|
+
coordinate[0],
|
|
168
|
+
coordinate[1],
|
|
169
|
+
Math.floor(1 + 10 * Math.random()),
|
|
170
|
+
]
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
).to.eql(coordinateWithoutZValues)
|
|
174
|
+
// testing with only one coordinate
|
|
175
|
+
expect(coordinatesUtils.removeZValues([[1, 2, 3]])).to.eql([[1, 2]])
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { SingleCoordinate } from '@/coordinatesUtils'
|
|
4
|
+
import type { FlatExtent } from '@/extentUtils'
|
|
5
|
+
|
|
6
|
+
import coordinatesUtils from '@/coordinatesUtils'
|
|
7
|
+
import { getExtentIntersectionWithCurrentProjection, getExtentCenter } from '@/extentUtils'
|
|
8
|
+
import { LV95, WGS84 } from '@/proj'
|
|
9
|
+
|
|
10
|
+
describe('Test extent utils', () => {
|
|
11
|
+
describe('reproject and cut extent within projection bounds', () => {
|
|
12
|
+
function expectExtentIs(
|
|
13
|
+
toBeTested: FlatExtent,
|
|
14
|
+
expected: FlatExtent,
|
|
15
|
+
acceptableDelta = 0.5
|
|
16
|
+
) {
|
|
17
|
+
expect(toBeTested).to.be.an('Array').lengthOf(4)
|
|
18
|
+
expected.forEach((value, index) => {
|
|
19
|
+
expect(toBeTested[index]).to.be.approximately(value, acceptableDelta)
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
it('reproject extent of a single coordinate inside the bounds of the projection', () => {
|
|
24
|
+
const singleCoordinate: SingleCoordinate = [8.2, 47.5]
|
|
25
|
+
const singleCoordinateInLV95 = coordinatesUtils.reprojectAndRound(
|
|
26
|
+
WGS84,
|
|
27
|
+
LV95,
|
|
28
|
+
singleCoordinate
|
|
29
|
+
)
|
|
30
|
+
const extent = [singleCoordinate, singleCoordinate].flat() as FlatExtent
|
|
31
|
+
const result = getExtentIntersectionWithCurrentProjection(extent, WGS84, LV95)
|
|
32
|
+
expect(result).to.be.an('Array').lengthOf(4)
|
|
33
|
+
expectExtentIs(result!, [...singleCoordinateInLV95, ...singleCoordinateInLV95])
|
|
34
|
+
})
|
|
35
|
+
it('returns undefined if a single coordinate outside of bounds is given', () => {
|
|
36
|
+
const singleCoordinateOutOfLV95Bounds = [8.2, 40]
|
|
37
|
+
const extent = [
|
|
38
|
+
singleCoordinateOutOfLV95Bounds,
|
|
39
|
+
singleCoordinateOutOfLV95Bounds,
|
|
40
|
+
].flat() as FlatExtent
|
|
41
|
+
expect(getExtentIntersectionWithCurrentProjection(extent, WGS84, LV95)).to.be.undefined
|
|
42
|
+
})
|
|
43
|
+
it('returns undefined if the extent given is completely outside of the projection bounds', () => {
|
|
44
|
+
const extent: FlatExtent = [-25.0, -20.0, -5.0, -45.0]
|
|
45
|
+
expect(getExtentIntersectionWithCurrentProjection(extent, WGS84, LV95)).to.be.undefined
|
|
46
|
+
})
|
|
47
|
+
it('reproject and cut an extent that is greater than LV95 extent on all sides', () => {
|
|
48
|
+
const result = getExtentIntersectionWithCurrentProjection(
|
|
49
|
+
[-2.4, 35, 21.3, 51.7],
|
|
50
|
+
WGS84,
|
|
51
|
+
LV95
|
|
52
|
+
)
|
|
53
|
+
expect(result).to.be.an('Array').lengthOf(4)
|
|
54
|
+
expectExtentIs(result!, [...LV95.bounds.bottomLeft, ...LV95.bounds.topRight])
|
|
55
|
+
})
|
|
56
|
+
it('reproject and cut an extent that is partially bigger than LV95 bounds', () => {
|
|
57
|
+
const result = getExtentIntersectionWithCurrentProjection(
|
|
58
|
+
// extent of file linked to PB-1221
|
|
59
|
+
[-122.08, -33.85, 151.21, 51.5],
|
|
60
|
+
WGS84,
|
|
61
|
+
LV95
|
|
62
|
+
)
|
|
63
|
+
expect(result).to.be.an('Array').lengthOf(4)
|
|
64
|
+
expectExtentIs(result!, [...LV95.bounds.bottomLeft, ...LV95.bounds.topRight])
|
|
65
|
+
})
|
|
66
|
+
it('only gives back the portion of an extent that is within LV95 bounds', () => {
|
|
67
|
+
const singleCoordinateInsideLV95: SingleCoordinate = [7.54, 48.12]
|
|
68
|
+
const singleCoordinateInLV95 = coordinatesUtils.reprojectAndRound(
|
|
69
|
+
WGS84,
|
|
70
|
+
LV95,
|
|
71
|
+
singleCoordinateInsideLV95
|
|
72
|
+
)
|
|
73
|
+
const overlappingExtent: FlatExtent = [0, 0, ...singleCoordinateInsideLV95]
|
|
74
|
+
const result = getExtentIntersectionWithCurrentProjection(
|
|
75
|
+
overlappingExtent,
|
|
76
|
+
WGS84,
|
|
77
|
+
LV95
|
|
78
|
+
)
|
|
79
|
+
expect(result).to.be.an('Array').lengthOf(4)
|
|
80
|
+
expectExtentIs(result!, [...LV95.bounds.bottomLeft, ...singleCoordinateInLV95])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
describe('getExtentCenter', () => {
|
|
84
|
+
it('calculates the center of an extent', () => {
|
|
85
|
+
const extent: FlatExtent = [0, 0, 30, 70]
|
|
86
|
+
const center = getExtentCenter(extent)
|
|
87
|
+
expect(center).to.be.an('Array').lengthOf(2)
|
|
88
|
+
expect(center[0]).to.be.closeTo(15, 0.0001)
|
|
89
|
+
expect(center[1]).to.be.closeTo(35, 0.0001)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { formatThousand, isNumber, round } from '@swissgeo/numbers'
|
|
2
|
+
import proj4 from 'proj4'
|
|
3
|
+
|
|
4
|
+
import type CoordinateSystem from '@/proj/CoordinateSystem'
|
|
5
|
+
|
|
6
|
+
import { allCoordinateSystems, WGS84 } from '@/proj'
|
|
7
|
+
|
|
8
|
+
export type SingleCoordinate = [number, number]
|
|
9
|
+
export type Single3DCoordinate = [number, number, number]
|
|
10
|
+
|
|
11
|
+
function isValidCoordinate(input: unknown): boolean {
|
|
12
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
return typeof input[0] === 'number' && typeof input[1] === 'number'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns rounded coordinate with thousands separator and comma.
|
|
20
|
+
*
|
|
21
|
+
* @param coordinate The raw coordinate as array.
|
|
22
|
+
* @param digits Decimal digits to round to.
|
|
23
|
+
* @param [withThousandsSeparator=true] If thousands should be separated with a single quote
|
|
24
|
+
* character. Default is `true`
|
|
25
|
+
* @param [enforceDigit=false] If set to true, we want to have that many figures after the period.
|
|
26
|
+
* Otherwise, we don't care. Default is `false`
|
|
27
|
+
* @returns Formatted coordinate.
|
|
28
|
+
* @see https://stackoverflow.com/a/2901298/4840446
|
|
29
|
+
*/
|
|
30
|
+
function toRoundedString(
|
|
31
|
+
coordinate: SingleCoordinate,
|
|
32
|
+
digits: number,
|
|
33
|
+
withThousandsSeparator: boolean = true,
|
|
34
|
+
enforceDigit: boolean = false
|
|
35
|
+
): string | undefined {
|
|
36
|
+
if (
|
|
37
|
+
!Array.isArray(coordinate) ||
|
|
38
|
+
coordinate.length !== 2 ||
|
|
39
|
+
!coordinate.every(isNumber) ||
|
|
40
|
+
coordinate.some(
|
|
41
|
+
(value) => value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY
|
|
42
|
+
)
|
|
43
|
+
) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
return coordinate
|
|
47
|
+
.map((value) => {
|
|
48
|
+
const roundedValue: number = round(value, digits)
|
|
49
|
+
let stringValue: string
|
|
50
|
+
if (enforceDigit) {
|
|
51
|
+
stringValue = roundedValue.toFixed(digits)
|
|
52
|
+
} else {
|
|
53
|
+
stringValue = roundedValue.toString()
|
|
54
|
+
}
|
|
55
|
+
if (withThousandsSeparator) {
|
|
56
|
+
return formatThousand(stringValue)
|
|
57
|
+
}
|
|
58
|
+
return stringValue
|
|
59
|
+
})
|
|
60
|
+
.join(', ')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wraps the provided coordinates in the world extents (i.e. the coordinate range that if equivalent
|
|
65
|
+
* to the wgs84 [-180, 180])
|
|
66
|
+
*
|
|
67
|
+
* @param coordinates The coordinates (or array of coordinates) to wrap
|
|
68
|
+
* @param projection Projection of the coordinates
|
|
69
|
+
* @returns Coordinates wrapped on the X axis
|
|
70
|
+
*/
|
|
71
|
+
function wrapXCoordinates<T extends SingleCoordinate | SingleCoordinate[]>(
|
|
72
|
+
coordinates: T,
|
|
73
|
+
projection: CoordinateSystem
|
|
74
|
+
): T {
|
|
75
|
+
if (projection.usesMercatorPyramid && projection.bounds && Array.isArray(coordinates)) {
|
|
76
|
+
if (coordinates.length === 2 && coordinates.every(isNumber)) {
|
|
77
|
+
const [x, y] = coordinates as SingleCoordinate
|
|
78
|
+
if (x >= projection.bounds.lowerX && x <= projection.bounds.upperX) {
|
|
79
|
+
return coordinates
|
|
80
|
+
}
|
|
81
|
+
const boundsWidth = projection.bounds.upperX - projection.bounds.lowerX
|
|
82
|
+
const worldsAway = Math.floor((x - projection.bounds.lowerX) / boundsWidth)
|
|
83
|
+
const offset = worldsAway * boundsWidth
|
|
84
|
+
return [x - offset, y] as T
|
|
85
|
+
} else if (coordinates.every(Array.isArray)) {
|
|
86
|
+
return (coordinates as SingleCoordinate[]).map((coordinate) =>
|
|
87
|
+
wrapXCoordinates(coordinate, projection)
|
|
88
|
+
) as T
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return coordinates
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the coordinates unwrapped if they were placed into an extra array. This can happen when
|
|
96
|
+
* dealing with GeoJSON coordinate, where some geometry types require coordinate in a format such as
|
|
97
|
+
* [ [ [x,y], [x,y] ], [...feature2...] ]
|
|
98
|
+
*
|
|
99
|
+
* Most of our backends only deal with the first feature of such array, this function will unwrap
|
|
100
|
+
* it, or return the array as is if it is not required
|
|
101
|
+
*/
|
|
102
|
+
function unwrapGeometryCoordinates(
|
|
103
|
+
geometryCoordinates?: SingleCoordinate[] | SingleCoordinate[][] | SingleCoordinate[][][]
|
|
104
|
+
): SingleCoordinate[] {
|
|
105
|
+
if (!geometryCoordinates) {
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (geometryCoordinates.every((value) => isValidCoordinate(value))) {
|
|
110
|
+
return geometryCoordinates as SingleCoordinate[]
|
|
111
|
+
} else {
|
|
112
|
+
return unwrapGeometryCoordinates(
|
|
113
|
+
(geometryCoordinates as SingleCoordinate[][] | SingleCoordinate[][][])[0]
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Remove any Z value from a set of coordinates
|
|
120
|
+
*
|
|
121
|
+
* @param coordinates
|
|
122
|
+
*/
|
|
123
|
+
function removeZValues(coordinates: SingleCoordinate[] | Single3DCoordinate[]): SingleCoordinate[] {
|
|
124
|
+
if (Array.isArray(coordinates)) {
|
|
125
|
+
if (coordinates.every((coordinate) => coordinate.length === 2)) {
|
|
126
|
+
return coordinates
|
|
127
|
+
} else if (coordinates.some((coordinate) => coordinate.length > 2)) {
|
|
128
|
+
return coordinates.map((coordinate) => [coordinate[0], coordinate[1]])
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error('Invalid coordinates received, cannot remove Z values')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function reprojectAndRound<T extends SingleCoordinate | SingleCoordinate[]>(
|
|
135
|
+
from: CoordinateSystem,
|
|
136
|
+
into: CoordinateSystem,
|
|
137
|
+
coordinates: T
|
|
138
|
+
): T {
|
|
139
|
+
if (!from || !into) {
|
|
140
|
+
throw new Error('Invalid arguments, must receive two CRS')
|
|
141
|
+
}
|
|
142
|
+
if (!isValidCoordinate(coordinates)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
'Invalid coordinates received, must be an array of number or an array of coordinates'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
const depthOne = coordinates[0]
|
|
148
|
+
if (Array.isArray(depthOne)) {
|
|
149
|
+
return (coordinates as SingleCoordinate[]).map((coordinate) =>
|
|
150
|
+
reprojectAndRound(from, into, coordinate)
|
|
151
|
+
) as T
|
|
152
|
+
}
|
|
153
|
+
return proj4(from.epsg, into.epsg, coordinates as SingleCoordinate).map((value) =>
|
|
154
|
+
into.roundCoordinateValue(value)
|
|
155
|
+
) as T
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseCRS(crs?: string): CoordinateSystem | undefined {
|
|
159
|
+
const epsgNumber = crs?.split(':').pop()
|
|
160
|
+
if (!epsgNumber) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (epsgNumber === 'WGS84') {
|
|
165
|
+
return WGS84
|
|
166
|
+
}
|
|
167
|
+
return allCoordinateSystems.find((system) => system.epsg === `EPSG:${epsgNumber}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface SwissGeoCoordinatesUtils {
|
|
171
|
+
toRoundedString: typeof toRoundedString
|
|
172
|
+
wrapXCoordinates: typeof wrapXCoordinates
|
|
173
|
+
unwrapGeometryCoordinates: typeof unwrapGeometryCoordinates
|
|
174
|
+
removeZValues: typeof removeZValues
|
|
175
|
+
reprojectAndRound: typeof reprojectAndRound
|
|
176
|
+
parseCRS: typeof parseCRS
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const coordinatesUtils: SwissGeoCoordinatesUtils = {
|
|
180
|
+
toRoundedString,
|
|
181
|
+
wrapXCoordinates,
|
|
182
|
+
unwrapGeometryCoordinates,
|
|
183
|
+
removeZValues,
|
|
184
|
+
reprojectAndRound,
|
|
185
|
+
parseCRS,
|
|
186
|
+
}
|
|
187
|
+
export { coordinatesUtils }
|
|
188
|
+
export default coordinatesUtils
|