@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,89 @@
1
+ import { round } from '@swissgeo/numbers'
2
+ import proj4 from 'proj4'
3
+
4
+ import type { SingleCoordinate } from '@/coordinatesUtils'
5
+
6
+ import { WGS84 } from '@/proj'
7
+ import { PIXEL_LENGTH_IN_KM_AT_ZOOM_ZERO_WITH_256PX_TILES } from '@/proj/CoordinateSystem'
8
+ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
9
+ import StandardCoordinateSystem from '@/proj/StandardCoordinateSystem'
10
+
11
+ export default class WebMercatorCoordinateSystem extends StandardCoordinateSystem {
12
+ constructor() {
13
+ super({
14
+ epsgNumber: 3857,
15
+ label: 'WebMercator',
16
+ // matrix comes from https://epsg.io/3857.proj4
17
+ proj4transformationMatrix:
18
+ '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=proj',
19
+ // bounds are coming from https://github.com/geoadmin/lib-gatilegrid/blob/58d6e574b69d32740a24edbc086d97897d4b41dc/gatilegrid/tilegrids.py#L122-L125
20
+ bounds: new CoordinateSystemBounds({
21
+ lowerX: -20037508.342789244,
22
+ upperX: 20037508.342789244,
23
+ lowerY: -20037508.342789244,
24
+ upperY: 20037508.342789244,
25
+ // center of LV95's extent transformed with epsg.io website
26
+ customCenter: [917209.87, 5914737.43],
27
+ }),
28
+ usesMercatorPyramid: true,
29
+ })
30
+ }
31
+
32
+ roundCoordinateValue(value: number): number {
33
+ return round(value, 2)
34
+ }
35
+
36
+ /**
37
+ * Formula comes from
38
+ * https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
39
+ *
40
+ * resolution = 156543.03 meters / pixel * cos(latitude) / (2 ^ zoom level)
41
+ */
42
+ getResolutionForZoomAndCenter(zoom: number, center: SingleCoordinate): number {
43
+ const centerInRad = proj4(this.epsg, WGS84.epsg, center).map(
44
+ (coordinate) => (coordinate * Math.PI) / 180.0
45
+ )
46
+ if (typeof centerInRad[1] !== 'number') {
47
+ return 0
48
+ }
49
+ return round(
50
+ Math.abs(
51
+ (PIXEL_LENGTH_IN_KM_AT_ZOOM_ZERO_WITH_256PX_TILES * Math.cos(centerInRad[1])) /
52
+ Math.pow(2, zoom)
53
+ ),
54
+ 2
55
+ )
56
+ }
57
+
58
+ /**
59
+ * Calculating zoom level by reversing formula from
60
+ * https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale :
61
+ *
62
+ * resolution = 156543.03 * cos(latitude) / (2 ^ zoom level)
63
+ *
64
+ * So that
65
+ *
66
+ * zoom level = log2( resolution / 156543.03 / cos(latitude) )
67
+ *
68
+ * @param resolution Resolution in meter/pixel
69
+ * @param center As the use an equatorial constant to calculate the zoom level, we need to know
70
+ * the latitude of the position the resolution must be calculated at, as we need to take into
71
+ * account the deformation of the WebMercator projection (that is greater the further north we
72
+ * are)
73
+ */
74
+ getZoomForResolutionAndCenter(resolution: number, center: SingleCoordinate): number {
75
+ const centerInRad = proj4(this.epsg, WGS84.epsg, center).map(
76
+ (coordinate) => (coordinate * Math.PI) / 180.0
77
+ )
78
+ if (typeof centerInRad[1] !== 'number') {
79
+ return 0
80
+ }
81
+ return Math.abs(
82
+ Math.log2(
83
+ resolution /
84
+ PIXEL_LENGTH_IN_KM_AT_ZOOM_ZERO_WITH_256PX_TILES /
85
+ Math.cos(centerInRad[1])
86
+ )
87
+ )
88
+ }
89
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { LV95, WEBMERCATOR, WGS84 } from '@/proj'
4
+ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
5
+ import StandardCoordinateSystem from '@/proj/StandardCoordinateSystem'
6
+
7
+ class BoundlessCoordinateSystem extends StandardCoordinateSystem {
8
+ constructor() {
9
+ super({
10
+ usesMercatorPyramid: false,
11
+ proj4transformationMatrix: 'test',
12
+ label: 'test',
13
+ epsgNumber: 1234,
14
+ })
15
+ }
16
+ getResolutionForZoomAndCenter(): number {
17
+ return 0
18
+ }
19
+
20
+ getZoomForResolutionAndCenter(): number {
21
+ return 0
22
+ }
23
+
24
+ roundCoordinateValue(): number {
25
+ return 0
26
+ }
27
+ }
28
+
29
+ describe('CoordinateSystem', () => {
30
+ const coordinateSystemWithouBounds = new BoundlessCoordinateSystem()
31
+ describe('getBoundsAs', () => {
32
+ it('returns undefined if the bounds are not defined', () => {
33
+ expect(coordinateSystemWithouBounds.getBoundsAs(WEBMERCATOR)).to.be.undefined
34
+ })
35
+ it('transforms LV95 into WebMercator correctly', () => {
36
+ const result = LV95.getBoundsAs(WEBMERCATOR)
37
+ expect(result).to.be.an.instanceOf(CoordinateSystemBounds)
38
+ // numbers are coming from epsg.io's transform tool
39
+ const acceptableDelta = 0.01
40
+ expect(result!.lowerX).to.approximately(572215.44, acceptableDelta)
41
+ expect(result!.lowerY).to.approximately(5684416.96, acceptableDelta)
42
+ expect(result!.upperX).to.approximately(1277662.36, acceptableDelta)
43
+ expect(result!.upperY).to.approximately(6145307.39, acceptableDelta)
44
+ })
45
+ it('transforms LV95 into WGS84 correctly', () => {
46
+ const result = LV95.getBoundsAs(WGS84)
47
+ expect(result).to.be.an.instanceOf(CoordinateSystemBounds)
48
+ // numbers are coming from epsg.io's transform tool
49
+ const acceptableDelta = 0.0001
50
+ expect(result!.lowerX).to.approximately(5.14029, acceptableDelta)
51
+ expect(result!.lowerY).to.approximately(45.39812, acceptableDelta)
52
+ expect(result!.upperX).to.approximately(11.47744, acceptableDelta)
53
+ expect(result!.upperY).to.approximately(48.23062, acceptableDelta)
54
+ })
55
+ })
56
+ describe('isInBound', () => {
57
+ it('returns false if no bounds are defined', () => {
58
+ expect(coordinateSystemWithouBounds.isInBounds(0, 0)).to.be.false
59
+ expect(coordinateSystemWithouBounds.isInBounds(1, 1)).to.be.false
60
+ })
61
+ // the remaining tests for this function are handled in the CoordinateSystemBounds.spec.ts file
62
+ })
63
+ })
@@ -0,0 +1,252 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import type { SingleCoordinate } from '@/coordinatesUtils'
4
+
5
+ import { LV95 } from '@/proj'
6
+ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
7
+
8
+ describe('CoordinateSystemBounds', () => {
9
+ describe('splitIfOutOfBounds(coordinates, bounds)', () => {
10
+ let bounds: CoordinateSystemBounds
11
+
12
+ beforeEach(() => {
13
+ bounds = new CoordinateSystemBounds({ lowerX: 0, upperX: 100, lowerY: 50, upperY: 100 })
14
+ })
15
+
16
+ it('returns a single CoordinatesChunk if no split is needed', () => {
17
+ const coordinatesWithinBounds: SingleCoordinate[] = [
18
+ [bounds.lowerX + 1, bounds.upperY - 1],
19
+ [bounds.lowerX + 2, bounds.upperY - 2],
20
+ [bounds.lowerX + 3, bounds.upperY - 3],
21
+ [bounds.lowerX + 4, bounds.upperY - 4],
22
+ [bounds.lowerX + 5, bounds.upperY - 5],
23
+ [bounds.lowerX + 6, bounds.upperY - 6],
24
+ [bounds.lowerX + 7, bounds.upperY - 7],
25
+ ]
26
+ const result = bounds.splitIfOutOfBounds(coordinatesWithinBounds)
27
+ expect(result).to.be.an('Array').of.length(1)
28
+ const [singleChunk] = result!
29
+ expect(singleChunk).to.be.an('Object').that.has.ownProperty('coordinates')
30
+ expect(singleChunk).to.haveOwnProperty('isWithinBounds')
31
+ expect(singleChunk!.isWithinBounds).to.be.true
32
+ expect(singleChunk!.coordinates).to.eql(coordinatesWithinBounds)
33
+ })
34
+ it('splits the given coordinates in two chunks if part of it is outside bounds', () => {
35
+ const yValue = 50
36
+ const coordinatesOverlappingBounds: SingleCoordinate[] = [
37
+ // starting by adding coordinates out of bounds
38
+ [bounds.lowerX - 1, yValue],
39
+ // split should occur here as we start to be in bounds
40
+ [bounds.lowerX + 1, yValue],
41
+ [50, yValue],
42
+ [bounds.upperX - 1, yValue],
43
+ ]
44
+ const result = bounds.splitIfOutOfBounds(coordinatesOverlappingBounds)
45
+ expect(result).to.be.an('Array').of.length(2)
46
+ const [outOfBoundChunk, inBoundChunk] = result!
47
+ expect(outOfBoundChunk).to.haveOwnProperty('isWithinBounds')
48
+ expect(outOfBoundChunk!.isWithinBounds).to.be.false
49
+ expect(outOfBoundChunk!.coordinates).to.be.an('Array').of.length(2)
50
+ expect(outOfBoundChunk!.coordinates[0]).to.eql(coordinatesOverlappingBounds[0])
51
+ // checking that the split happened on the bounds
52
+ const intersectingCoordinate = outOfBoundChunk!.coordinates[1]
53
+ expect(intersectingCoordinate).to.be.an('Array').of.length(2)
54
+ expect(intersectingCoordinate).to.eql([bounds.lowerX, yValue])
55
+ // next chunk must start by the intersecting coordinate
56
+ expect(inBoundChunk).to.haveOwnProperty('isWithinBounds')
57
+ expect(inBoundChunk!.isWithinBounds).to.be.true
58
+ expect(inBoundChunk!.coordinates).to.be.an('Array').of.length(4)
59
+ const [firstInBoundCoordinate] = inBoundChunk!.coordinates
60
+ expect(firstInBoundCoordinate).to.be.an('Array').of.length(2)
61
+ expect(firstInBoundCoordinate).to.eql([bounds.lowerX, yValue])
62
+ // checking that further coordinates have been correctly copied
63
+ coordinatesOverlappingBounds.slice(1).forEach((coordinate, index) => {
64
+ expect(inBoundChunk!.coordinates[index + 1]![0]).to.eq(coordinate[0])
65
+ expect(inBoundChunk!.coordinates[index + 1]![1]).to.eq(coordinate[1])
66
+ })
67
+ })
68
+ it('gives similar results if coordinates are given in the reverse order', () => {
69
+ const yValue = 50
70
+ // same test data as previous test, but reversed
71
+ const coordinatesOverlappingBounds: SingleCoordinate[] = [
72
+ [bounds.lowerX - 1, yValue] as SingleCoordinate,
73
+ [bounds.lowerX + 1, yValue] as SingleCoordinate,
74
+ [50, yValue] as SingleCoordinate,
75
+ [bounds.upperX - 1, yValue] as SingleCoordinate,
76
+ ].toReversed()
77
+ const result = bounds.splitIfOutOfBounds(coordinatesOverlappingBounds)
78
+ expect(result).to.be.an('Array').of.length(2)
79
+ const [inBoundChunk, outOfBoundChunk] = result!
80
+
81
+ // first chunk must now be the in bound one
82
+ expect(inBoundChunk).to.haveOwnProperty('isWithinBounds')
83
+ expect(inBoundChunk!.isWithinBounds).to.be.true
84
+ expect(inBoundChunk!.coordinates).to.be.an('Array').of.length(4)
85
+ const lastInBoundCoordinate = inBoundChunk!.coordinates.splice(-1)[0]
86
+ expect(lastInBoundCoordinate).to.be.an('Array').of.length(2)
87
+ expect(lastInBoundCoordinate).to.eql([bounds.lowerX, yValue])
88
+
89
+ expect(outOfBoundChunk).to.haveOwnProperty('isWithinBounds')
90
+ expect(outOfBoundChunk!.isWithinBounds).to.be.false
91
+ expect(outOfBoundChunk!.coordinates).to.be.an('Array').of.length(2)
92
+ expect(outOfBoundChunk!.coordinates[0]).to.eql([bounds.lowerX, yValue])
93
+ })
94
+ it('handles properly a line going multiple times out of bounds', () => {
95
+ const coordinatesGoingBackAndForth: SingleCoordinate[] = [
96
+ [-1, 51], // outside
97
+ [1, 51], // inside going in the X direction
98
+ [1, 101], // outside going in the Y direction
99
+ [101, 101], // outside
100
+ [99, 99], // inside going both directions
101
+ [1, 51], // inside moving on the other side of the bounds
102
+ ]
103
+ const expectedFirstIntersection: SingleCoordinate = [bounds.lowerX, 51]
104
+ const expectedSecondIntersection: SingleCoordinate = [1, bounds.upperY]
105
+ const expectedThirdIntersection: SingleCoordinate = [bounds.upperX, bounds.upperY]
106
+
107
+ const result = bounds.splitIfOutOfBounds(coordinatesGoingBackAndForth)
108
+ expect(result).to.be.an('Array').of.length(4)
109
+ const [firstChunk, secondChunk, thirdChunk, fourthChunk] = result!
110
+ // first chunk should have two coordinates, the first from the list and the first intersection
111
+ expect(firstChunk!.isWithinBounds).to.be.false
112
+ expect(firstChunk!.coordinates).to.be.an('Array').of.length(2)
113
+ expect(firstChunk!.coordinates[0]).to.eql(coordinatesGoingBackAndForth[0])
114
+ expect(firstChunk!.coordinates[1]).to.eql(expectedFirstIntersection)
115
+ // second chunk should start with the first intersection, then include the second coord
116
+ // and finish with the second intersection
117
+ expect(secondChunk!.coordinates).to.be.an('Array').of.length(3)
118
+ expect(secondChunk!.isWithinBounds).to.be.true
119
+ expect(secondChunk!.coordinates[0]).to.eql(expectedFirstIntersection)
120
+ expect(secondChunk!.coordinates[1]).to.eql(coordinatesGoingBackAndForth[1])
121
+ expect(secondChunk!.coordinates[2]).to.eql(expectedSecondIntersection)
122
+ // third chunk should be : intersection2, coord3, coord4, intersection3
123
+ expect(thirdChunk!.coordinates).to.be.an('Array').of.length(4)
124
+ expect(thirdChunk!.isWithinBounds).to.be.false
125
+ expect(thirdChunk!.coordinates[0]).to.eql(expectedSecondIntersection)
126
+ expect(thirdChunk!.coordinates[1]).to.eql(coordinatesGoingBackAndForth[2])
127
+ expect(thirdChunk!.coordinates[2]).to.eql(coordinatesGoingBackAndForth[3])
128
+ expect(thirdChunk!.coordinates[3]).to.eql(expectedThirdIntersection)
129
+ // last chunk should be : intersection3, coord5, coord6
130
+ expect(fourthChunk!.coordinates).to.be.an('Array').of.length(3)
131
+ expect(fourthChunk!.isWithinBounds).to.be.true
132
+ expect(fourthChunk!.coordinates[0]).to.eql(expectedThirdIntersection)
133
+ expect(fourthChunk!.coordinates[1]).to.eql(coordinatesGoingBackAndForth[4])
134
+ expect(fourthChunk!.coordinates[2]).to.eql(coordinatesGoingBackAndForth[5])
135
+ })
136
+ it('splits correctly a line crossing bounds two times in a straight line (no stop inside)', () => {
137
+ const coordinatesGoingThrough: SingleCoordinate[] = [
138
+ [-1, 50], // outside
139
+ [101, 50], // outside
140
+ ]
141
+ const expectedFirstIntersection: SingleCoordinate = [bounds.lowerX, 50]
142
+ const expectedSecondIntersection: SingleCoordinate = [bounds.upperX, 50]
143
+
144
+ const result = bounds.splitIfOutOfBounds(coordinatesGoingThrough)
145
+ expect(result).to.be.an('Array').of.length(3)
146
+ const [firstChunk, secondChunk, thirdChunk] = result!
147
+
148
+ expect(firstChunk!.isWithinBounds).to.be.false
149
+ expect(firstChunk!.coordinates).to.be.an('Array').of.length(2)
150
+ expect(firstChunk!.coordinates[0]).to.eql(coordinatesGoingThrough[0])
151
+ expect(firstChunk!.coordinates[1]).to.eql(expectedFirstIntersection)
152
+
153
+ expect(secondChunk!.isWithinBounds).to.be.true
154
+ expect(secondChunk!.coordinates).to.be.an('Array').of.length(2)
155
+ expect(secondChunk!.coordinates[0]).to.eql(expectedFirstIntersection)
156
+ expect(secondChunk!.coordinates[1]).to.eql(expectedSecondIntersection)
157
+
158
+ expect(thirdChunk!.isWithinBounds).to.be.false
159
+ expect(thirdChunk!.coordinates).to.be.an('Array').of.length(2)
160
+ expect(thirdChunk!.coordinates[0]).to.eql(expectedSecondIntersection)
161
+ expect(thirdChunk!.coordinates[1]).to.eql(coordinatesGoingThrough[1])
162
+ })
163
+ it('handles some "real" use case well', () => {
164
+ const sample1: SingleCoordinate[] = [
165
+ [2651000, 1392000],
166
+ [2932500, 894500],
167
+ ]
168
+ const result = LV95.bounds.splitIfOutOfBounds(sample1)
169
+ expect(result).to.be.an('Array').of.length(3)
170
+ const [firstChunk, secondChunk, thirdChunk] = result!
171
+
172
+ expect(firstChunk!.isWithinBounds).to.be.false
173
+ expect(firstChunk!.coordinates).to.be.an('Array').of.length(2)
174
+ expect(firstChunk!.coordinates[0]).to.eql(sample1[0])
175
+ expect(firstChunk!.coordinates[1]![0]).to.approximately(2674764.8, 0.1)
176
+ expect(firstChunk!.coordinates[1]![1]).to.approximately(1350000, 0.1)
177
+
178
+ expect(secondChunk!.isWithinBounds).to.be.true
179
+ expect(secondChunk!.coordinates).to.be.an('Array').of.length(2)
180
+ expect(secondChunk!.coordinates[0]![0]).to.approximately(2674764.8, 0.1)
181
+ expect(secondChunk!.coordinates[0]![1]).to.approximately(1350000, 0.1)
182
+ expect(secondChunk!.coordinates[1]![0]).to.approximately(2855830.1, 0.1)
183
+ expect(secondChunk!.coordinates[1]![1]).to.approximately(1030000, 0.1)
184
+
185
+ expect(thirdChunk!.isWithinBounds).to.be.false
186
+ expect(thirdChunk!.coordinates).to.be.an('Array').of.length(2)
187
+ expect(thirdChunk!.coordinates[0]![0]).to.approximately(2855830.1, 0.1)
188
+ expect(thirdChunk!.coordinates[0]![1]).to.approximately(1030000, 0.1)
189
+ expect(thirdChunk!.coordinates[1]).to.eql(sample1[1])
190
+
191
+ const reversedResult = LV95.bounds.splitIfOutOfBounds(sample1.toReversed())
192
+ expect(reversedResult).to.be.an('Array').of.length(3)
193
+ const [firstReversedChunk, secondReversedChunk, thirdReversedChunk] = reversedResult!
194
+
195
+ expect(firstReversedChunk!.isWithinBounds).to.be.false
196
+ expect(firstReversedChunk!.coordinates).to.be.an('Array').of.length(2)
197
+ expect(firstReversedChunk!.coordinates[0]).to.eql(sample1[1])
198
+ expect(firstReversedChunk!.coordinates[1]![0]).to.approximately(2855830.1, 0.1)
199
+ expect(firstReversedChunk!.coordinates[1]![1]).to.approximately(1030000, 0.1)
200
+
201
+ expect(secondReversedChunk!.isWithinBounds).to.be.true
202
+ expect(secondReversedChunk!.coordinates).to.be.an('Array').of.length(2)
203
+ expect(secondReversedChunk!.coordinates[0]![0]).to.approximately(2855830.1, 0.1)
204
+ expect(secondReversedChunk!.coordinates[0]![1]).to.approximately(1030000, 0.1)
205
+ expect(secondReversedChunk!.coordinates[1]![0]).to.approximately(2674764.8, 0.1)
206
+ expect(secondReversedChunk!.coordinates[1]![1]).to.approximately(1350000, 0.1)
207
+
208
+ expect(thirdReversedChunk!.isWithinBounds).to.be.false
209
+ expect(thirdReversedChunk!.coordinates).to.be.an('Array').of.length(2)
210
+ expect(thirdReversedChunk!.coordinates[0]![0]).to.approximately(2674764.8, 0.1)
211
+ expect(thirdReversedChunk!.coordinates[0]![1]).to.approximately(1350000, 0.1)
212
+ expect(thirdReversedChunk!.coordinates[1]).to.eql(sample1[0])
213
+ })
214
+ })
215
+ describe('isInBounds(x, y)', () => {
216
+ const testInstance = new CoordinateSystemBounds({
217
+ lowerX: -1,
218
+ upperX: 1,
219
+ lowerY: -1,
220
+ upperY: 1,
221
+ })
222
+ it('returns true if we are on the border of the bounds', () => {
223
+ expect(testInstance.isInBounds(-1, -1)).to.be.true
224
+ expect(testInstance.isInBounds(-1, 1)).to.be.true
225
+ expect(testInstance.isInBounds(1, -1)).to.be.true
226
+ expect(testInstance.isInBounds(1, 1)).to.be.true
227
+ })
228
+ it('returns true if we are in bounds not touching any border', () => {
229
+ expect(testInstance.isInBounds(0, 0)).to.be.true
230
+ })
231
+ it('returns false if only one parameter (X or Y) is out of bound', () => {
232
+ expect(testInstance.isInBounds(-1, -2)).to.be.false
233
+ expect(testInstance.isInBounds(-2, -1)).to.be.false
234
+ expect(testInstance.isInBounds(-1, 2)).to.be.false
235
+ expect(testInstance.isInBounds(2, -1)).to.be.false
236
+ expect(testInstance.isInBounds(1, -2)).to.be.false
237
+ expect(testInstance.isInBounds(-2, 1)).to.be.false
238
+ expect(testInstance.isInBounds(1, 2)).to.be.false
239
+ expect(testInstance.isInBounds(2, 1)).to.be.false
240
+ })
241
+ })
242
+ describe('flatten', () => {
243
+ const lowerX = 123
244
+ const upperX = 456
245
+ const lowerY = 345
246
+ const upperY = 678
247
+ const testInstance = new CoordinateSystemBounds({ lowerX, upperX, lowerY, upperY })
248
+ it('produces a flatten array correctly', () => {
249
+ expect(testInstance.flatten).to.eql([lowerX, lowerY, upperX, upperY])
250
+ })
251
+ })
252
+ })
@@ -0,0 +1,136 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { LV03, LV95 } from '@/proj'
4
+ import {
5
+ LV95_RESOLUTIONS,
6
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX,
7
+ } from '@/proj/SwissCoordinateSystem'
8
+
9
+ describe('Unit test functions from SwissCoordinateSystem', () => {
10
+ describe('transformCustomZoomLevelToStandard', () => {
11
+ it('transforms rounded value correctly', () => {
12
+ // most zoom levels on mf-geoadmin3 were forced as integer, so we have to make sure we translate them correctly
13
+ // there is 14 zoom levels described in mf-geoadmin3
14
+ for (let swisstopoZoom = 0; swisstopoZoom <= 14; swisstopoZoom++) {
15
+ expect(LV95.transformCustomZoomLevelToStandard(swisstopoZoom)).to.eq(
16
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX[swisstopoZoom]
17
+ )
18
+ expect(LV03.transformCustomZoomLevelToStandard(swisstopoZoom)).to.eq(
19
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX[swisstopoZoom]
20
+ )
21
+ }
22
+ })
23
+ it('floors any floating swisstopo zoom given before searching for the equivalent', () => {
24
+ for (let swisstopoZoom = 0; swisstopoZoom <= 14; swisstopoZoom++) {
25
+ for (let above = swisstopoZoom; above < swisstopoZoom + 1; above += 0.1) {
26
+ expect(LV95.transformCustomZoomLevelToStandard(above)).to.eq(
27
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX[swisstopoZoom]
28
+ )
29
+ expect(LV03.transformCustomZoomLevelToStandard(above)).to.eq(
30
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX[swisstopoZoom]
31
+ )
32
+ }
33
+ }
34
+ })
35
+ })
36
+ describe('transformStandardZoomLevelToCustom', () => {
37
+ it('transforms exact value correctly', () => {
38
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX.forEach(
39
+ (mercatorZoom, swisstopoZoom) => {
40
+ expect(LV95.transformStandardZoomLevelToCustom(mercatorZoom)).to.eq(
41
+ swisstopoZoom
42
+ )
43
+ expect(LV03.transformStandardZoomLevelToCustom(mercatorZoom)).to.eq(
44
+ swisstopoZoom
45
+ )
46
+ }
47
+ )
48
+ })
49
+ it('finds the closest swisstopo zoom from the mercator zoom given', () => {
50
+ const acceptableDeltaInMercatorZoomLevel = 0.15
51
+ // generating ranges of mercator zoom that matches the steps of the matrix
52
+ const rangeOfMercatorZoomToTest = SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX.map(
53
+ (mercatorZoom, lv95Zoom) => {
54
+ if (lv95Zoom === 0) {
55
+ return {
56
+ start: 0,
57
+ end:
58
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX[0]! -
59
+ acceptableDeltaInMercatorZoomLevel,
60
+ expected: 0,
61
+ }
62
+ }
63
+ if (lv95Zoom >= 14) {
64
+ return {
65
+ start: 21,
66
+ end: 30,
67
+ expected: lv95Zoom,
68
+ }
69
+ }
70
+ const nextZoomLevel =
71
+ SWISSTOPO_TILEGRID_ZOOM_TO_STANDARD_ZOOM_MATRIX[lv95Zoom + 1]!
72
+ return {
73
+ start: mercatorZoom + acceptableDeltaInMercatorZoomLevel,
74
+ end: nextZoomLevel,
75
+ expected: lv95Zoom + 1,
76
+ }
77
+ }
78
+ )
79
+ rangeOfMercatorZoomToTest.forEach((range) => {
80
+ for (
81
+ let zoomLevel = range.start;
82
+ zoomLevel <= range.end;
83
+ zoomLevel += acceptableDeltaInMercatorZoomLevel
84
+ ) {
85
+ expect(LV95.transformStandardZoomLevelToCustom(zoomLevel)).to.eq(
86
+ range.expected,
87
+ `Mercator zoom ${zoomLevel} was not translated to LV95 correctly`
88
+ )
89
+ expect(LV03.transformStandardZoomLevelToCustom(zoomLevel)).to.eq(
90
+ range.expected,
91
+ `Mercator zoom ${zoomLevel} was not translated to LV03 correctly`
92
+ )
93
+ }
94
+ })
95
+ })
96
+ })
97
+ describe('getZoomForResolutionAndY', () => {
98
+ it('returns zoom=0 if the resolution is too great', () => {
99
+ expect(LV95.getZoomForResolutionAndCenter(LV95_RESOLUTIONS[0]! + 1)).to.eq(0)
100
+ expect(LV03.getZoomForResolutionAndCenter(LV95_RESOLUTIONS[0]! + 1)).to.eq(0)
101
+ })
102
+ it('returns zoom correctly while resolution is exactly on a threshold', () => {
103
+ for (let i = 0; i < LV95_RESOLUTIONS.length - 1; i++) {
104
+ expect(LV95.getZoomForResolutionAndCenter(LV95_RESOLUTIONS[i]!)).to.eq(i)
105
+ expect(LV03.getZoomForResolutionAndCenter(LV95_RESOLUTIONS[i]!)).to.eq(i)
106
+ }
107
+ })
108
+ it('returns zoom correctly while resolution is in between the two thresholds', () => {
109
+ for (let i = 0; i < LV95_RESOLUTIONS.length - 2; i++) {
110
+ for (
111
+ let resolution = LV95_RESOLUTIONS[i]! - 1;
112
+ resolution > LV95_RESOLUTIONS[i + 1]!;
113
+ resolution--
114
+ ) {
115
+ expect(LV95.getZoomForResolutionAndCenter(resolution)).to.eq(
116
+ i + 1,
117
+ `resolution ${resolution} was misinterpreted`
118
+ )
119
+ expect(LV03.getZoomForResolutionAndCenter(resolution)).to.eq(
120
+ i + 1,
121
+ `resolution ${resolution} was misinterpreted`
122
+ )
123
+ }
124
+ }
125
+ })
126
+ it('returns the max zoom available, event if the resolution is smaller than expected', () => {
127
+ const smallestResolution = LV95_RESOLUTIONS[LV95_RESOLUTIONS.length - 1]!
128
+ expect(LV95.getZoomForResolutionAndCenter(smallestResolution - 0.1)).to.eq(
129
+ LV95_RESOLUTIONS.indexOf(smallestResolution)
130
+ )
131
+ expect(LV03.getZoomForResolutionAndCenter(smallestResolution - 0.1)).to.eq(
132
+ LV95_RESOLUTIONS.indexOf(smallestResolution)
133
+ )
134
+ })
135
+ })
136
+ })
@@ -0,0 +1,65 @@
1
+ import CoordinateSystem, {
2
+ STANDARD_ZOOM_LEVEL_1_25000_MAP,
3
+ SWISS_ZOOM_LEVEL_1_25000_MAP,
4
+ } from '@/proj/CoordinateSystem'
5
+ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
6
+ import CustomCoordinateSystem from '@/proj/CustomCoordinateSystem'
7
+ import LV03CoordinateSystem from '@/proj/LV03CoordinateSystem'
8
+ import LV95CoordinateSystem from '@/proj/LV95CoordinateSystem'
9
+ import StandardCoordinateSystem from '@/proj/StandardCoordinateSystem'
10
+ import SwissCoordinateSystem, {
11
+ LV95_RESOLUTIONS,
12
+ SWISSTOPO_TILEGRID_RESOLUTIONS,
13
+ } from '@/proj/SwissCoordinateSystem'
14
+ import WebMercatorCoordinateSystem from '@/proj/WebMercatorCoordinateSystem'
15
+ import WGS84CoordinateSystem from '@/proj/WGS84CoordinateSystem'
16
+
17
+ export const LV95: LV95CoordinateSystem = new LV95CoordinateSystem()
18
+ export const LV03: LV03CoordinateSystem = new LV03CoordinateSystem()
19
+ export const WGS84: WGS84CoordinateSystem = new WGS84CoordinateSystem()
20
+ export const WEBMERCATOR: WebMercatorCoordinateSystem = new WebMercatorCoordinateSystem()
21
+
22
+ export type * from '@/proj/types'
23
+
24
+ /** Representation of many (available in this app) projection systems */
25
+ export const allCoordinateSystems: CoordinateSystem[] = [LV95, LV03, WGS84, WEBMERCATOR]
26
+
27
+ interface SwissGeoCoordinateConstants {
28
+ STANDARD_ZOOM_LEVEL_1_25000_MAP: number
29
+ SWISS_ZOOM_LEVEL_1_25000_MAP: number
30
+ LV95_RESOLUTIONS: number[]
31
+ SWISSTOPO_TILEGRID_RESOLUTIONS: number[]
32
+ }
33
+
34
+ const constants: SwissGeoCoordinateConstants = {
35
+ STANDARD_ZOOM_LEVEL_1_25000_MAP,
36
+ SWISS_ZOOM_LEVEL_1_25000_MAP,
37
+ LV95_RESOLUTIONS,
38
+ SWISSTOPO_TILEGRID_RESOLUTIONS,
39
+ }
40
+
41
+ export interface SwissGeoCoordinateCRS {
42
+ LV95: LV95CoordinateSystem
43
+ LV03: LV03CoordinateSystem
44
+ WGS84: WGS84CoordinateSystem
45
+ WEBMERCATOR: WebMercatorCoordinateSystem
46
+ allCoordinateSystems: CoordinateSystem[]
47
+ }
48
+
49
+ const crs: SwissGeoCoordinateCRS = {
50
+ LV95,
51
+ LV03,
52
+ WGS84,
53
+ WEBMERCATOR,
54
+ allCoordinateSystems,
55
+ }
56
+ export {
57
+ crs,
58
+ constants,
59
+ CoordinateSystem,
60
+ CoordinateSystemBounds,
61
+ CustomCoordinateSystem,
62
+ StandardCoordinateSystem,
63
+ SwissCoordinateSystem,
64
+ }
65
+ export default crs
@@ -0,0 +1,22 @@
1
+ import type { SingleCoordinate } from '@/coordinatesUtils'
2
+
3
+ /**
4
+ * Group of coordinates resulting in a "split by bounds" function. Will also contain information if
5
+ * this chunk is within or outside the bounds from which it was cut from.
6
+ */
7
+ export interface CoordinatesChunk {
8
+ /** Coordinates of this chunk */
9
+ coordinates: SingleCoordinate[]
10
+ /** Will be true if this chunk contains coordinates that are located within bounds */
11
+ isWithinBounds: boolean
12
+ }
13
+
14
+ /** Representation of a resolution step in a coordinate system. Can be linked to a zoom level or not. */
15
+ export interface ResolutionStep {
16
+ /** Resolution of this step, in meters/pixel */
17
+ resolution: number
18
+ /** Corresponding zoom level for this resolution step */
19
+ zoom?: number
20
+ /** Name of the map product shown at this resolution/zoom */
21
+ label?: string
22
+ }
@@ -0,0 +1,38 @@
1
+ import type proj4 from 'proj4'
2
+
3
+ import log from '@swissgeo/log'
4
+
5
+ import type CoordinateSystem from '@/proj/CoordinateSystem'
6
+
7
+ import { LV03, LV95, WEBMERCATOR } from '@/proj'
8
+
9
+ /**
10
+ * Proj4 comes with [EPSG:4326]{@link https://epsg.io/4326} as default projection.
11
+ *
12
+ * By default, this adds the two Swiss projections ([LV95/EPSG:2056]{@link https://epsg.io/2056} and
13
+ * [LV03/EPSG:21781]{@link https://epsg.io/21781}) and metric Web Mercator
14
+ * ([EPSG:3857]{@link https://epsg.io/3857}) definitions to proj4
15
+ *
16
+ * Further projection can be added by settings the param projections (do not forget to include LV95,
17
+ * LV03 and/or WebMercator if you intended to use them too)
18
+ */
19
+ const registerProj4 = (
20
+ proj4Instance: typeof proj4,
21
+ projections: CoordinateSystem[] = [WEBMERCATOR, LV95, LV03]
22
+ ): void => {
23
+ // adding projection defining a transformation matrix to proj4 (these projection matrices can be found on the epsg.io website)
24
+ projections
25
+ .filter((projection) => projection.proj4transformationMatrix)
26
+ .forEach((projection) => {
27
+ try {
28
+ proj4Instance.defs(projection.epsg, projection.proj4transformationMatrix)
29
+ } catch (err) {
30
+ const error = err ? (err as Error) : new Error('Unknown error')
31
+ log.error('Error while setting up projection in proj4', projection.epsg, error)
32
+ throw error
33
+ }
34
+ })
35
+ }
36
+
37
+ export { registerProj4 }
38
+ export default registerProj4
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@swissgeo/config-typescript/tsconfig.vue.json",
3
+ "include": ["**/*.ts", "**/*.vue"]
4
+ }