@swissgeo/coordinates 1.0.0-rc.1 → 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 +1 -12
- 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 -23
- 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,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,52 @@
|
|
|
1
|
+
/** @module swissgeo/coordinates/ol */
|
|
2
|
+
|
|
3
|
+
import { View } from "ol";
|
|
4
|
+
import { register } from "ol/proj/proj4";
|
|
5
|
+
import WMTSTileGrid from "ol/tilegrid/WMTS";
|
|
6
|
+
import proj4 from "proj4";
|
|
7
|
+
|
|
8
|
+
import { LV95 } from "@/proj";
|
|
9
|
+
import { LV95_RESOLUTIONS } from "@/proj/SwissCoordinateSystem";
|
|
10
|
+
import registerProj4 from "@/registerProj4";
|
|
11
|
+
|
|
12
|
+
export function registerSwissGeoProjections() {
|
|
13
|
+
registerProj4(proj4)
|
|
14
|
+
register(proj4)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function indexOfMaxResolution(layerMaxResolution: number): number {
|
|
18
|
+
const resolutionSteps = LV95.getResolutionSteps()
|
|
19
|
+
const matchResolutionStep = resolutionSteps.find(
|
|
20
|
+
(step) => step.resolution === layerMaxResolution
|
|
21
|
+
)
|
|
22
|
+
if (!matchResolutionStep) {
|
|
23
|
+
return resolutionSteps.length - 1
|
|
24
|
+
}
|
|
25
|
+
return resolutionSteps.indexOf(matchResolutionStep)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getLV95TileGrid(maxResolution = 0.25): WMTSTileGrid {
|
|
29
|
+
const maxResolutionIndex = indexOfMaxResolution(maxResolution)
|
|
30
|
+
let resolutionSteps = LV95.getResolutionSteps()
|
|
31
|
+
if (resolutionSteps.length > maxResolutionIndex) {
|
|
32
|
+
resolutionSteps = resolutionSteps.slice(0, maxResolutionIndex + 1)
|
|
33
|
+
}
|
|
34
|
+
return new WMTSTileGrid({
|
|
35
|
+
resolutions: resolutionSteps.map((step) => step.resolution),
|
|
36
|
+
origin: LV95.getTileOrigin(),
|
|
37
|
+
matrixIds: resolutionSteps.map((_, index) => index.toString()),
|
|
38
|
+
extent: LV95.bounds?.flatten,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getLV95View(): View {
|
|
43
|
+
return new View({
|
|
44
|
+
projection: LV95.epsg,
|
|
45
|
+
center: LV95.bounds.center,
|
|
46
|
+
zoom: LV95.getDefaultZoom(),
|
|
47
|
+
minResolution: LV95_RESOLUTIONS[LV95_RESOLUTIONS.length - 1],
|
|
48
|
+
resolutions: LV95_RESOLUTIONS,
|
|
49
|
+
extent: LV95.bounds.flatten,
|
|
50
|
+
constrainOnlyCenter: true,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
@@ -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
|
+
}
|