@swissgeo/coordinates 1.0.0-rc.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +119 -0
  2. package/dist/index.cjs +7 -0
  3. package/dist/index.d.ts +1 -12
  4. package/dist/index.js +8012 -18103
  5. package/dist/ol.cjs +1 -0
  6. package/dist/ol.d.ts +11 -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 -23
  13. package/setup-vitest.ts +8 -0
  14. package/src/DevApp.vue +66 -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 +53 -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,196 @@
1
+ import { round } from '@swissgeo/numbers'
2
+ import { bbox, bboxClip, bboxPolygon, buffer, point } from '@turf/turf'
3
+ import proj4 from 'proj4'
4
+
5
+ import type { SingleCoordinate } from '@/coordinatesUtils'
6
+ import type { CoordinateSystem } from '@/proj'
7
+
8
+ import { WGS84 } from '@/proj'
9
+
10
+ export type FlatExtent = [number, number, number, number]
11
+ export type NormalizedExtent = [[number, number], [number, number]]
12
+
13
+ /**
14
+ * @param fromProj Current projection used to describe the extent
15
+ * @param toProj Target projection we want the extent be expressed in
16
+ * @param extent An extent, described as `[minx, miny, maxx, maxy].` or `[[minx, miny], [maxx,
17
+ * maxy]]`
18
+ * @returns The reprojected extent
19
+ */
20
+ export function projExtent<T extends FlatExtent | NormalizedExtent>(
21
+ fromProj: CoordinateSystem,
22
+ toProj: CoordinateSystem,
23
+ extent: T
24
+ ): T {
25
+ if (extent.length === 4) {
26
+ const bottomLeft = proj4(fromProj.epsg, toProj.epsg, [
27
+ extent[0],
28
+ extent[1],
29
+ ]) as SingleCoordinate
30
+ const topRight = proj4(fromProj.epsg, toProj.epsg, [
31
+ extent[2],
32
+ extent[3],
33
+ ]) as SingleCoordinate
34
+ return [...bottomLeft, ...topRight].map((value) => toProj.roundCoordinateValue(value)) as T
35
+ } else if (extent.length === 2) {
36
+ const bottomLeft = proj4(fromProj.epsg, toProj.epsg, extent[0]).map((value) =>
37
+ toProj.roundCoordinateValue(value)
38
+ )
39
+ const topRight = proj4(fromProj.epsg, toProj.epsg, extent[1]).map((value) =>
40
+ toProj.roundCoordinateValue(value)
41
+ )
42
+ return [bottomLeft, topRight] as T
43
+ }
44
+ return extent
45
+ }
46
+
47
+ /**
48
+ * Return an extent normalized to [[x, y], [x, y]] from a flat extent
49
+ *
50
+ * @param extent Extent to normalize
51
+ * @returns Extent in the form [[x, y], [x, y]]
52
+ */
53
+ export function normalizeExtent(extent: FlatExtent | NormalizedExtent): NormalizedExtent {
54
+ let extentNormalized = extent
55
+ if (extent?.length === 4) {
56
+ // convert to the flat extent to [[x, y], [x, y]]
57
+ extentNormalized = [
58
+ [extent[0], extent[1]],
59
+ [extent[2], extent[3]],
60
+ ]
61
+ }
62
+ return extentNormalized as NormalizedExtent
63
+ }
64
+
65
+ /**
66
+ * Flatten extent
67
+ *
68
+ * @param extent Extent to flatten
69
+ * @returns Flatten extent in from [minx, miny, maxx, maxy]
70
+ */
71
+ export function flattenExtent(extent: FlatExtent | NormalizedExtent): FlatExtent {
72
+ let flattenExtent = extent
73
+ if (extent?.length === 2) {
74
+ flattenExtent = [...extent[0], ...extent[1]]
75
+ }
76
+ return flattenExtent as FlatExtent
77
+ }
78
+
79
+ /**
80
+ * Get the intersection of the extent with the current projection, as a flatten extent expressed in
81
+ * the current projection
82
+ *
83
+ * @param extent Such as [minx, miny, maxx, maxy]. or [bottomLeft, topRight]
84
+ * @param extentProjection
85
+ * @param currentProjection
86
+ */
87
+ export function getExtentIntersectionWithCurrentProjection(
88
+ extent: FlatExtent | NormalizedExtent,
89
+ extentProjection: CoordinateSystem,
90
+ currentProjection: CoordinateSystem
91
+ ): FlatExtent | undefined {
92
+ if (
93
+ (extent?.length !== 4 && extent?.length !== 2) ||
94
+ !extentProjection ||
95
+ !currentProjection ||
96
+ !currentProjection.bounds
97
+ ) {
98
+ return undefined
99
+ }
100
+ let currentProjectionAsExtentProjection: FlatExtent = currentProjection.bounds
101
+ .flatten as FlatExtent
102
+ if (extentProjection.epsg !== currentProjection.epsg) {
103
+ // We used to reproject the extent here, but there's problem arising if current projection is LV95 and
104
+ // the extent is going a little bit out of Switzerland.
105
+ // As LV95 is quite location-locked, the further we get, the bigger the mathematical errors start growing.
106
+ // So to counteract that, we transform the current projection bounds in the extent projection to do the comparison.
107
+ currentProjectionAsExtentProjection = projExtent(
108
+ currentProjection,
109
+ extentProjection,
110
+ currentProjectionAsExtentProjection
111
+ )
112
+ }
113
+ let intersectionOfExtents = bbox(bboxClip(bboxPolygon(currentProjectionAsExtentProjection), flattenExtent(extent)))
114
+
115
+ if (
116
+ !intersectionOfExtents || intersectionOfExtents.length !== 4 ||
117
+ intersectionOfExtents.every((value) => Math.abs(value) === Infinity)
118
+ ) {
119
+ return undefined
120
+ }
121
+ if (extentProjection.epsg !== currentProjection.epsg) {
122
+ // if we transformed the current projection extent above, we now need to output the correct proj
123
+ intersectionOfExtents = projExtent(extentProjection, currentProjection, intersectionOfExtents)
124
+ }
125
+
126
+ return intersectionOfExtents
127
+ }
128
+
129
+ export function getExtentCenter(extent: FlatExtent | NormalizedExtent): SingleCoordinate {
130
+ const [topLeft, bottomRight] = normalizeExtent(extent)
131
+ return [(topLeft[0] + bottomRight[0]) / 2, (topLeft[1] + bottomRight[1]) / 2]
132
+ }
133
+
134
+ interface ConfigCreatePixelExtentAround {
135
+ /**
136
+ * Number of pixels the extent should be (if s100 is given, a box of 100x100 pixels with the
137
+ * coordinate at its center will be returned)
138
+ */
139
+ size: number
140
+ /** Where the center of the "size" pixel(s) extent should be. */
141
+ coordinate: SingleCoordinate
142
+ /** Projection used to describe the coordinates */
143
+ projection: CoordinateSystem
144
+ /** Current map resolution, necessary to calculate how much distance "size" pixel(s) means. */
145
+ resolution: number
146
+ /** Tells if the extent's value should be rounded before being returned. Default is `false` */
147
+ rounded?: boolean
148
+ }
149
+
150
+ export function createPixelExtentAround(
151
+ config: ConfigCreatePixelExtentAround
152
+ ): FlatExtent | undefined {
153
+ const { size, coordinate, projection, resolution, rounded = false } = config
154
+ if (!size || !coordinate || !projection || !resolution) {
155
+ return undefined
156
+ }
157
+ let coordinatesWgs84 = coordinate
158
+ if (projection.epsg !== WGS84.epsg) {
159
+ coordinatesWgs84 = proj4(projection.epsg, WGS84.epsg, coordinate)
160
+ }
161
+ const bufferAround = buffer(
162
+ point(coordinatesWgs84),
163
+ // sphere of the wanted number of pixels as radius around the coordinate
164
+ size * resolution,
165
+ { units: 'meters' }
166
+ )
167
+ if (!bufferAround) {
168
+ return undefined
169
+ }
170
+ const extent: FlatExtent = projExtent(WGS84, projection, bbox(bufferAround) as FlatExtent)
171
+
172
+ if (rounded) {
173
+ return extent.map((value: number) => round(value)) as FlatExtent
174
+ }
175
+ return extent
176
+ }
177
+
178
+ export interface SwissGeoExtentUtils {
179
+ projExtent: typeof projExtent
180
+ normalizeExtent: typeof normalizeExtent
181
+ flattenExtent: typeof flattenExtent
182
+ getExtentIntersectionWithCurrentProjection: typeof getExtentIntersectionWithCurrentProjection
183
+ createPixelExtentAround: typeof createPixelExtentAround
184
+ getExtentCenter: typeof getExtentCenter
185
+ }
186
+
187
+ const extentUtils: SwissGeoExtentUtils = {
188
+ projExtent,
189
+ normalizeExtent,
190
+ flattenExtent,
191
+ getExtentIntersectionWithCurrentProjection,
192
+ createPixelExtentAround,
193
+ getExtentCenter,
194
+ }
195
+ export { extentUtils }
196
+ export default extentUtils
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ /** @module swissgeo/coordinates */
2
+
3
+ import proj4 from 'proj4'
4
+
5
+ import type { SwissGeoCoordinatesUtils } from '@/coordinatesUtils'
6
+ import type { SwissGeoExtentUtils } from '@/extentUtils'
7
+ import type { SwissGeoCoordinateCRS } from '@/proj'
8
+
9
+ import { coordinatesUtils } from '@/coordinatesUtils'
10
+ import { extentUtils } from '@/extentUtils'
11
+ import crs from '@/proj'
12
+ import registerProj4 from '@/registerProj4'
13
+
14
+ export * from '@/proj'
15
+ export * from '@/registerProj4'
16
+ export * from '@/coordinatesUtils'
17
+ export * from '@/extentUtils'
18
+
19
+ // registering local instance of proj4, needed for some @swissgeo/coordinates functions
20
+ registerProj4(proj4)
21
+
22
+ interface SwissGeoCoordinates extends SwissGeoCoordinateCRS {
23
+ coordinatesUtils: SwissGeoCoordinatesUtils
24
+ extentUtils: SwissGeoExtentUtils
25
+ registerProj4: typeof registerProj4
26
+ }
27
+
28
+ const coordinates: SwissGeoCoordinates = { ...crs, coordinatesUtils, extentUtils, registerProj4 }
29
+ export default coordinates
package/src/ol.ts ADDED
@@ -0,0 +1,53 @@
1
+ /** @module swissgeo/coordinates/ol */
2
+
3
+ import type proj4 from 'proj4'
4
+
5
+ import { View } from 'ol'
6
+ import { register } from 'ol/proj/proj4'
7
+ import WMTSTileGrid from 'ol/tilegrid/WMTS'
8
+
9
+ import { LV95 } from '@/proj'
10
+ import { LV95_RESOLUTIONS } from '@/proj/SwissCoordinateSystem'
11
+ import registerProj4 from '@/registerProj4'
12
+
13
+ export function registerSwissGeoProjections(proj4Instance: typeof proj4) {
14
+ registerProj4(proj4Instance)
15
+ register(proj4Instance)
16
+ }
17
+
18
+ function indexOfMaxResolution(layerMaxResolution: number): number {
19
+ const resolutionSteps = LV95.getResolutionSteps()
20
+ const matchResolutionStep = resolutionSteps.find(
21
+ (step) => step.resolution === layerMaxResolution
22
+ )
23
+ if (!matchResolutionStep) {
24
+ return resolutionSteps.length - 1
25
+ }
26
+ return resolutionSteps.indexOf(matchResolutionStep)
27
+ }
28
+
29
+ export function getLV95TileGrid(maxResolution = 0.25): WMTSTileGrid {
30
+ const maxResolutionIndex = indexOfMaxResolution(maxResolution)
31
+ let resolutionSteps = LV95.getResolutionSteps()
32
+ if (resolutionSteps.length > maxResolutionIndex) {
33
+ resolutionSteps = resolutionSteps.slice(0, maxResolutionIndex + 1)
34
+ }
35
+ return new WMTSTileGrid({
36
+ resolutions: resolutionSteps.map((step) => step.resolution),
37
+ origin: LV95.getTileOrigin(),
38
+ matrixIds: resolutionSteps.map((_, index) => index.toString()),
39
+ extent: LV95.bounds?.flatten,
40
+ })
41
+ }
42
+
43
+ export function getLV95View(): View {
44
+ return new View({
45
+ projection: LV95.epsg,
46
+ center: LV95.bounds.center,
47
+ zoom: LV95.getDefaultZoom(),
48
+ minResolution: LV95_RESOLUTIONS[LV95_RESOLUTIONS.length - 1],
49
+ resolutions: LV95_RESOLUTIONS,
50
+ extent: LV95.bounds.flatten,
51
+ constrainOnlyCenter: true,
52
+ })
53
+ }
@@ -0,0 +1,315 @@
1
+ import { round } from '@swissgeo/numbers'
2
+ import { earthRadius } from '@turf/turf'
3
+ import proj4 from 'proj4'
4
+
5
+ import type { SingleCoordinate } from '@/coordinatesUtils'
6
+ import type { ResolutionStep } from '@/proj/types'
7
+
8
+ import CoordinateSystemBounds from '@/proj/CoordinateSystemBounds'
9
+
10
+ /**
11
+ * These are the zoom levels, for each projection, which give us a 1:25'000 ratio map.
12
+ *
13
+ * These variables are declared here, as the config.js import the LV95 coordinate system, and it
14
+ * could lead to initialization errors (even when initializing the constants before importing the
15
+ * class). Thus we declare them here, at the root class of the coordinates systems.
16
+ */
17
+ export const STANDARD_ZOOM_LEVEL_1_25000_MAP: number = 15.5
18
+ export const SWISS_ZOOM_LEVEL_1_25000_MAP: number = 8
19
+
20
+ /**
21
+ * Resolution (pixel/meter) found at zoom level 0 while looking at the equator. This constant is
22
+ * used to calculate the resolution taking latitude into account. With Mercator projection, the
23
+ * deformation increases when latitude increases.
24
+ *
25
+ * Length of the Earth around its equator (in meters) / 256 pixels
26
+ *
27
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
28
+ * @see https://wiki.openstreetmap.org/wiki/Zoom_levels
29
+ */
30
+ export const PIXEL_LENGTH_IN_KM_AT_ZOOM_ZERO_WITH_256PX_TILES: number =
31
+ (2 * Math.PI * earthRadius) / 256
32
+
33
+ export interface CoordinateSystemProps {
34
+ /**
35
+ * EPSG:xxxx representation of this coordinate system, but only the numerical part (without the
36
+ * "EPSG:")
37
+ */
38
+ epsgNumber: number
39
+ /**
40
+ * Label to show users when they are dealing with this coordinate system (can be a translation
41
+ * key)
42
+ */
43
+ label: string
44
+ /**
45
+ * Name of this projection, if applicable, so that it can be tested against name in fields such
46
+ * as COG metadata parsing.
47
+ */
48
+ technicalName?: string
49
+ /**
50
+ * A string describing how proj4 should handle projection/reprojection of this coordinate
51
+ * system, in regard to WGS84. These matrices can be found on the EPSG website for each
52
+ * projection in the Export section, inside the PROJ.4 export type (can be directly accessed by
53
+ * adding .proj4 to the URL of one projection's page on the EPSG website, i.e.
54
+ * https://epsg.io/3857.proj4 for WebMercator)
55
+ */
56
+ proj4transformationMatrix: string
57
+ /**
58
+ * Bounds of this projection system, expressed as in its own coordinate system. These boundaries
59
+ * can also be found on the EPSG website, in the section "Projected bounds" of a projection's
60
+ * page. It is possible to specify a custom center to these bounds, so that the application
61
+ * starts at this custom center instead of the natural center (when no coordinates are specified
62
+ * at startup).
63
+ */
64
+ bounds?: CoordinateSystemBounds
65
+ /**
66
+ * A boolean variable indicating whether the Mercator projection pyramid is used.
67
+ *
68
+ * The Mercator projection pyramid is a specific spatial reference system commonly used in
69
+ * mapping and geographic information systems (GIS) applications.
70
+ *
71
+ * If set to `true`, it signifies that the system or dataset is leveraging this projection
72
+ * methodology.
73
+ *
74
+ * If set to `false`, it indicates that the dataset uses an alternative projection or tiling
75
+ * scheme.
76
+ */
77
+ usesMercatorPyramid: boolean
78
+ }
79
+
80
+ /**
81
+ * Representation of a coordinate system (or also called projection system) in the context of this
82
+ * application.
83
+ *
84
+ * These coordinate systems will be used to drive the mapping framework, helping it grasp which zoom
85
+ * level or resolution to show, where to start the view (with the center of bounds), how to handle a
86
+ * projection change, etc...
87
+ *
88
+ * @abstract
89
+ */
90
+ export default abstract class CoordinateSystem {
91
+ /**
92
+ * EPSG:xxxx representation of this coordinate system, but only the numerical part (without the
93
+ * "EPSG:")
94
+ */
95
+ public readonly epsgNumber: number
96
+ /** EPSG:xxxx representation of this coordinate system */
97
+ public readonly epsg: string
98
+ /**
99
+ * Label to show users when they are dealing with this coordinate system (can be a translation
100
+ * key)
101
+ */
102
+ public readonly label: string
103
+ /**
104
+ * Name of this projection, if applicable, so that it can be tested against name in fields such
105
+ * as COG metadata parsing.
106
+ */
107
+ public readonly technicalName?: string
108
+ /**
109
+ * A string describing how proj4 should handle projection/reprojection of this coordinate
110
+ * system, in regard to WGS84. These matrices can be found on the EPSG website for each
111
+ * projection in the Export section, inside the PROJ.4 export type (can be directly accessed by
112
+ * adding .proj4 to the URL of one projection's page on the EPSG website, i.e.
113
+ * https://epsg.io/3857.proj4 for WebMercator)
114
+ */
115
+ public readonly proj4transformationMatrix: string
116
+ /**
117
+ * Bounds of this projection system, expressed as in its own coordinate system. These boundaries
118
+ * can also be found on the EPSG website, in the section "Projected bounds" of a projection's
119
+ * page. It is possible to specify a custom center to these bounds, so that the application
120
+ * starts at this custom center instead of the natural center (when no coordinates are specified
121
+ * at startup).
122
+ */
123
+ public readonly bounds?: CoordinateSystemBounds
124
+ /**
125
+ * A boolean variable indicating whether the Mercator projection pyramid is used.
126
+ *
127
+ * The Mercator projection pyramid is a specific spatial reference system commonly used in
128
+ * mapping and geographic information systems (GIS) applications.
129
+ *
130
+ * If set to `true`, it signifies that the system or dataset is leveraging this projection
131
+ * methodology.
132
+ *
133
+ * If set to `false`, it indicates that the dataset uses an alternative projection or tiling
134
+ * scheme.
135
+ */
136
+ public readonly usesMercatorPyramid: boolean
137
+
138
+ constructor(args: CoordinateSystemProps) {
139
+ const {
140
+ epsgNumber,
141
+ label,
142
+ proj4transformationMatrix,
143
+ bounds,
144
+ technicalName,
145
+ usesMercatorPyramid = false,
146
+ } = args
147
+ this.epsgNumber = epsgNumber
148
+ /**
149
+ * Full EPSG identifier used by most libraries (such as proj4, or OpenLayers)
150
+ *
151
+ * @type {`EPSG:${Number}`}
152
+ */
153
+ this.epsg = `EPSG:${epsgNumber}`
154
+ this.label = label
155
+ this.proj4transformationMatrix = proj4transformationMatrix
156
+ this.bounds = bounds
157
+ this.technicalName = technicalName
158
+ this.usesMercatorPyramid = usesMercatorPyramid
159
+ }
160
+
161
+ /**
162
+ * Transforms the bounds of this coordinates system to be expressed in the wanted coordinate
163
+ * system
164
+ *
165
+ * If the coordinate system is invalid, or if bounds are not defined, it will return null
166
+ *
167
+ * @param {CoordinateSystem} coordinateSystem The target coordinate system we want bounds
168
+ * expressed in
169
+ * @returns {CoordinateSystemBounds | undefined} Bounds, expressed in the coordinate system, or
170
+ * undefined if bounds are undefined or the coordinate system is invalid
171
+ */
172
+ getBoundsAs(coordinateSystem: CoordinateSystem): CoordinateSystemBounds | undefined {
173
+ if (this.bounds) {
174
+ if (coordinateSystem.epsg === this.epsg) {
175
+ return this.bounds
176
+ }
177
+ const newBottomLeft = proj4(this.epsg, coordinateSystem.epsg, this.bounds.bottomLeft)
178
+ const newTopRight = proj4(this.epsg, coordinateSystem.epsg, this.bounds.topRight)
179
+ let customCenter: SingleCoordinate | undefined
180
+ if (this.bounds.customCenter) {
181
+ customCenter = proj4(this.epsg, coordinateSystem.epsg, this.bounds.customCenter)
182
+ }
183
+ return new CoordinateSystemBounds({
184
+ lowerX: newBottomLeft[0],
185
+ lowerY: newBottomLeft[1],
186
+ upperX: newTopRight[0],
187
+ upperY: newTopRight[1],
188
+ customCenter,
189
+ })
190
+ }
191
+ return
192
+ }
193
+
194
+ isInBounds(x: number, y: number): boolean
195
+ isInBounds(coordinate: SingleCoordinate): boolean
196
+
197
+ /**
198
+ * Tells if a coordinate is in bound of this coordinate system.
199
+ *
200
+ * Will return false if no bounds are defined for this coordinate system
201
+ */
202
+ isInBounds(xOrCoordinate: number | SingleCoordinate, y?: number): boolean {
203
+ if (!this.bounds) {
204
+ return false
205
+ }
206
+ if (typeof xOrCoordinate === 'number') {
207
+ return this.bounds.isInBounds(xOrCoordinate, y!)
208
+ }
209
+ return this.bounds.isInBounds(xOrCoordinate[0], xOrCoordinate[1])
210
+ }
211
+
212
+ /**
213
+ * @abstract
214
+ * @returns The Index in the resolution list where the 1:25000 zoom level is
215
+ */
216
+ abstract get1_25000ZoomLevel(): number
217
+
218
+ /**
219
+ * @abstract
220
+ * @returns The default zoom to use when starting the map in this coordinate system (if none are
221
+ * set so far)
222
+ */
223
+ abstract getDefaultZoom(): number
224
+
225
+ /**
226
+ * Rounds a zoom level.
227
+ *
228
+ * You can, by overwriting this function, add custom zoom level roundings or similar function in
229
+ * your custom coordinate systems.
230
+ *
231
+ * @param zoom A zoom level in this coordinate system
232
+ * @returns The given zoom level after rounding
233
+ */
234
+ roundZoomLevel(zoom: number): number {
235
+ return round(zoom, 3)
236
+ }
237
+
238
+ /**
239
+ * Returns the corresponding resolution to the given zoom level and center (some coordinate
240
+ * system must take some deformation into account the further north we are)
241
+ *
242
+ * @abstract
243
+ * @param zoom A zoom level
244
+ * @param center The current center of view, expressed with this coordinate system
245
+ * @returns The resolution at the given zoom level, in the context of this coordinate system
246
+ */
247
+ abstract getResolutionForZoomAndCenter(zoom: number, center: SingleCoordinate): number
248
+
249
+ /**
250
+ * Returns the zoom level to match the given resolution and center (some coordinate system must
251
+ * take some deformation into account the further north we are)
252
+ *
253
+ * @abstract
254
+ * @param resolution The resolution of the map, expressed in meter per pixel
255
+ * @param center The current center of view, expressed in this coordinate system
256
+ * @returns The corresponding zoom level, in the context of this coordinate system
257
+ */
258
+ abstract getZoomForResolutionAndCenter(resolution: number, center: SingleCoordinate): number
259
+
260
+ /**
261
+ * Returns a rounded value of a coordinate value, in the context of this coordinate system. This
262
+ * enables us to decide how many decimal points we want to keep for numbers after calculation
263
+ * within this coordinate system (no need to keep values that are irrelevant to precision for
264
+ * most maps, such as below 1cm)
265
+ *
266
+ * @abstract
267
+ * @param {Number} value A value to be rounded
268
+ * @returns {Number} The rounded value, with desired remaining decimal point for this coordinate
269
+ * system
270
+ */
271
+ abstract roundCoordinateValue(value: number): number
272
+
273
+ /**
274
+ * A (descending) list of all the available resolution steps for this coordinate system. If this
275
+ * is not the behavior you want, you have to override this function.
276
+ */
277
+ getResolutionSteps(latitude?: number): ResolutionStep[] {
278
+ if (!this.bounds) {
279
+ return []
280
+ }
281
+ const zoom0PixelSizeInMeters = PIXEL_LENGTH_IN_KM_AT_ZOOM_ZERO_WITH_256PX_TILES
282
+ const resolutions: ResolutionStep[] = []
283
+ const latInRad = ((latitude ?? 0) * Math.PI) / 180.0
284
+ for (let z = 0; z < 21; ++z) {
285
+ resolutions.push({
286
+ resolution: (zoom0PixelSizeInMeters * Math.cos(latInRad)) / Math.pow(2, z),
287
+ zoom: z,
288
+ })
289
+ }
290
+ return resolutions
291
+ }
292
+
293
+ /**
294
+ * The origin to use as anchor for tile coordinate calculations. It will return the bound's
295
+ * [lowerX, upperY] as default value (meaning the top-left corner of bounds). If this is not the
296
+ * behavior you want, you have to override this function.
297
+ *
298
+ * If no bounds are defined, it will return [0, 0]
299
+ */
300
+ getTileOrigin(): SingleCoordinate {
301
+ return this.bounds?.topLeft ?? [0, 0]
302
+ }
303
+
304
+ /**
305
+ * List of matrix identifiers for this coordinate system. If this is not the behavior you want,
306
+ * you have to override this function.
307
+ */
308
+ getMatrixIds(): number[] {
309
+ const matrixIds = []
310
+ for (let z = 0; z < this.getResolutionSteps().length; ++z) {
311
+ matrixIds[z] = z
312
+ }
313
+ return matrixIds
314
+ }
315
+ }