@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
|
@@ -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