@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.
Files changed (39) hide show
  1. package/README.md +118 -0
  2. package/dist/index.cjs +7 -0
  3. package/dist/index.d.ts +3 -14
  4. package/dist/index.js +8012 -18103
  5. package/dist/ol.cjs +1 -0
  6. package/dist/ol.d.ts +10 -0
  7. package/dist/ol.js +4467 -0
  8. package/dist/registerProj4-BuUOcPpF.cjs +23 -0
  9. package/dist/registerProj4-CwR_kPOz.js +10172 -0
  10. package/eslint.config.mts +12 -0
  11. package/index.html +14 -0
  12. package/package.json +30 -29
  13. package/setup-vitest.ts +8 -0
  14. package/src/DevApp.vue +65 -0
  15. package/src/__test__/coordinatesUtils.spec.ts +178 -0
  16. package/src/__test__/extentUtils.spec.ts +92 -0
  17. package/src/coordinatesUtils.ts +188 -0
  18. package/src/dev.ts +6 -0
  19. package/src/extentUtils.ts +196 -0
  20. package/src/index.ts +29 -0
  21. package/src/ol.ts +52 -0
  22. package/src/proj/CoordinateSystem.ts +315 -0
  23. package/src/proj/CoordinateSystemBounds.ts +170 -0
  24. package/src/proj/CustomCoordinateSystem.ts +58 -0
  25. package/src/proj/LV03CoordinateSystem.ts +23 -0
  26. package/src/proj/LV95CoordinateSystem.ts +35 -0
  27. package/src/proj/StandardCoordinateSystem.ts +22 -0
  28. package/src/proj/SwissCoordinateSystem.ts +233 -0
  29. package/src/proj/WGS84CoordinateSystem.ts +97 -0
  30. package/src/proj/WebMercatorCoordinateSystem.ts +89 -0
  31. package/src/proj/__test__/CoordinateSystem.spec.ts +63 -0
  32. package/src/proj/__test__/CoordinateSystemBounds.spec.ts +252 -0
  33. package/src/proj/__test__/SwissCoordinateSystem.spec.ts +136 -0
  34. package/src/proj/index.ts +65 -0
  35. package/src/proj/types.ts +22 -0
  36. package/src/registerProj4.ts +38 -0
  37. package/tsconfig.json +4 -0
  38. package/vite.config.ts +46 -0
  39. package/dist/index.umd.cjs +0 -29
@@ -0,0 +1,12 @@
1
+ import defaultConfig from '@swissgeo/config-eslint'
2
+
3
+ export default [
4
+ ...defaultConfig,
5
+ {
6
+ languageOptions: {
7
+ parserOptions: {
8
+ tsconfigRootDir: import.meta.dirname,
9
+ },
10
+ },
11
+ },
12
+ ]
package/index.html ADDED
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Preview for @swissgeo/coordinates</title>
6
+ </head>
7
+ <body>
8
+ <div id="app"></div>
9
+ <script
10
+ type="module"
11
+ src="src/dev.ts"
12
+ ></script>
13
+ </body>
14
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swissgeo/coordinates",
3
- "version": "1.0.0-beta.2",
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-beta.2",
22
- "@swissgeo/numbers": "1.0.0-beta.2"
21
+ "@swissgeo/log": "1.0.0",
22
+ "@swissgeo/numbers": "1.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@microsoft/api-extractor": "^7.55.0",
26
- "@prettier/plugin-xml": "^3.4.2",
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.20",
31
- "chai": "^6.2.1",
32
- "eslint": "^9.39.1",
33
- "prettier": "^3.6.2",
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": "^7.2.2",
40
- "vitest": "^4.0.8",
41
- "@swissgeo/config-typescript": "1.0.0-beta.2",
42
- "@swissgeo/config-eslint": "1.0.0-beta.2",
43
- "@swissgeo/config-prettier": "1.0.0-beta.2"
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
- "ol": "^10.7.0",
47
- "proj4": "^2.20.0"
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 --run --mode development --environment jsdom",
61
- "test:unit:watch": "vitest --mode development --environment jsdom",
62
- "type-check": "tsc -p tsconfig.json"
61
+ "test:unit": "vitest run",
62
+ "test:unit:watch": "vitest watch",
63
+ "type-check": "vue-tsc --build"
63
64
  }
64
65
  }
@@ -0,0 +1,8 @@
1
+ import proj4 from 'proj4'
2
+ import { beforeAll } from 'vitest'
3
+
4
+ import registerProj4 from '@/registerProj4'
5
+
6
+ beforeAll(() => {
7
+ registerProj4(proj4)
8
+ })
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
package/src/dev.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { createApp } from 'vue'
2
+
3
+ import DevApp from '@/DevApp.vue'
4
+
5
+ const devApp = createApp(DevApp)
6
+ devApp.mount('#app')