ogcopenapis 0.0.12 → 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 (61) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE +20 -20
  3. package/README.md +305 -72
  4. package/lib/OgcOpenApiCrsTools.d.ts +10 -9
  5. package/lib/OgcOpenApiCrsTools.d.ts.map +1 -0
  6. package/lib/OgcOpenApiCrsTools.js +40 -39
  7. package/lib/OgcOpenApiCrsTools.js.map +1 -0
  8. package/lib/OgcOpenApiFeatureStore.d.ts +94 -93
  9. package/lib/OgcOpenApiFeatureStore.d.ts.map +1 -0
  10. package/lib/OgcOpenApiFeatureStore.js +200 -244
  11. package/lib/OgcOpenApiFeatureStore.js.map +1 -0
  12. package/lib/OgcOpenApiFetchTools.d.ts +37 -30
  13. package/lib/OgcOpenApiFetchTools.d.ts.map +1 -0
  14. package/lib/OgcOpenApiFetchTools.js +68 -50
  15. package/lib/OgcOpenApiFetchTools.js.map +1 -0
  16. package/lib/OgcOpenApiGetCapabilities.d.ts +209 -112
  17. package/lib/OgcOpenApiGetCapabilities.d.ts.map +1 -0
  18. package/lib/OgcOpenApiGetCapabilities.js +424 -277
  19. package/lib/OgcOpenApiGetCapabilities.js.map +1 -0
  20. package/lib/OgcOpenApiMapsModel.d.ts +41 -36
  21. package/lib/OgcOpenApiMapsModel.d.ts.map +1 -0
  22. package/lib/OgcOpenApiMapsModel.js +88 -75
  23. package/lib/OgcOpenApiMapsModel.js.map +1 -0
  24. package/lib/OgcOpenApiMvtTools.d.ts +12 -0
  25. package/lib/OgcOpenApiMvtTools.d.ts.map +1 -0
  26. package/lib/OgcOpenApiMvtTools.js +100 -0
  27. package/lib/OgcOpenApiMvtTools.js.map +1 -0
  28. package/lib/OgcOpenApiStyleTools.d.ts +51 -0
  29. package/lib/OgcOpenApiStyleTools.d.ts.map +1 -0
  30. package/lib/OgcOpenApiStyleTools.js +155 -0
  31. package/lib/OgcOpenApiStyleTools.js.map +1 -0
  32. package/lib/OgcOpenApiTileMatrixTools.d.ts +41 -0
  33. package/lib/OgcOpenApiTileMatrixTools.d.ts.map +1 -0
  34. package/lib/OgcOpenApiTileMatrixTools.js +88 -0
  35. package/lib/OgcOpenApiTileMatrixTools.js.map +1 -0
  36. package/lib/OgcOpenApiTilesModel.d.ts +56 -31
  37. package/lib/OgcOpenApiTilesModel.d.ts.map +1 -0
  38. package/lib/OgcOpenApiTilesModel.js +167 -128
  39. package/lib/OgcOpenApiTilesModel.js.map +1 -0
  40. package/lib/OgcOpenApiVectorTilesModel.d.ts +39 -0
  41. package/lib/OgcOpenApiVectorTilesModel.d.ts.map +1 -0
  42. package/lib/OgcOpenApiVectorTilesModel.js +122 -0
  43. package/lib/OgcOpenApiVectorTilesModel.js.map +1 -0
  44. package/lib/OgcOpenApiWmtsTools.d.ts +30 -0
  45. package/lib/OgcOpenApiWmtsTools.d.ts.map +1 -0
  46. package/lib/OgcOpenApiWmtsTools.js +89 -0
  47. package/lib/OgcOpenApiWmtsTools.js.map +1 -0
  48. package/package.json +57 -27
  49. package/src/OgcOpenApiCrsTools.ts +40 -0
  50. package/src/OgcOpenApiFeatureStore.ts +296 -0
  51. package/src/OgcOpenApiFetchTools.ts +84 -0
  52. package/src/OgcOpenApiGetCapabilities.ts +641 -0
  53. package/src/OgcOpenApiMapsModel.ts +128 -0
  54. package/src/OgcOpenApiMvtTools.ts +104 -0
  55. package/src/OgcOpenApiStyleTools.ts +187 -0
  56. package/src/OgcOpenApiTileMatrixTools.ts +111 -0
  57. package/src/OgcOpenApiTilesModel.ts +214 -0
  58. package/src/OgcOpenApiVectorTilesModel.ts +141 -0
  59. package/src/OgcOpenApiWmtsTools.ts +114 -0
  60. package/lib/OgcOpenApiGetCollectionTiles.d.ts +0 -7
  61. package/lib/OgcOpenApiGetCollectionTiles.js +0 -7
@@ -0,0 +1,128 @@
1
+ import {RasterDataType} from "@luciad/ria/model/tileset/RasterDataType.js";
2
+ import {RasterSamplingMode} from "@luciad/ria/model/tileset/RasterSamplingMode.js";
3
+ import {WMSTileSetModel} from "@luciad/ria/model/tileset/WMSTileSetModel.js";
4
+ import {OgcOpenApiCrsTools} from "./OgcOpenApiCrsTools.js";
5
+ import {getReference} from "@luciad/ria/reference/ReferenceProvider.js";
6
+ import type {TileCoordinate} from "@luciad/ria/model/tileset/TileCoordinate.js";
7
+ import {Bounds} from "@luciad/ria/shape/Bounds.js";
8
+ import {CoordinateReference} from "@luciad/ria/reference/CoordinateReference.js";
9
+ import {createBounds} from "@luciad/ria/shape/ShapeFactory.js";
10
+ import type {ProxifyFunction} from "./OgcOpenApiGetCapabilities.js";
11
+ import {OgcOpenApiTileMatrixTools} from "./OgcOpenApiTileMatrixTools.js";
12
+
13
+ // Half the extent of the Web Mercator world, in metres
14
+ const WEB_MERCATOR_HALF_WORLD = 20037508.3427892;
15
+
16
+ interface OgcOpenApiMapsModelConstructorOptions {
17
+ crs: string;
18
+ baseURL: string;
19
+ collection: string;
20
+ dataType?: RasterDataType;
21
+ samplingMode?: RasterSamplingMode;
22
+ credentials?: boolean;
23
+ requestHeaders?: { [p: string]: string };
24
+ format?: string;
25
+ reverseAxis?: boolean;
26
+ datetime?: string;
27
+ subset?: string[];
28
+ transparent?: boolean;
29
+ bgcolor?: string;
30
+ // The extent that is tiled, defaults to the Web Mercator world for EPSG:3857
31
+ bounds?: Bounds;
32
+
33
+ proxify?: ProxifyFunction
34
+ }
35
+
36
+ class OgcOpenApiMapsModel extends WMSTileSetModel {
37
+ private format: string | undefined;
38
+ private crs: string;
39
+ private reverseAxis: boolean | undefined;
40
+ private datetime: string | undefined;
41
+ private subset: string[] | undefined;
42
+ private bgcolor: string | undefined;
43
+ private proxy: ProxifyFunction;
44
+ // The query of the map link, such as f=png: servers advertise the f values they accept
45
+ private linkParameters: { [key: string]: string };
46
+
47
+ constructor(o: OgcOpenApiMapsModelConstructorOptions) {
48
+ const referenceName = OgcOpenApiCrsTools.getReferenceName(o.crs)
49
+ const reference = getReference(referenceName);
50
+ super({
51
+ reference,
52
+ bounds: o.bounds ? o.bounds : OgcOpenApiMapsModel.getDefaultBounds(reference),
53
+ getMapRoot: o.baseURL,
54
+ layers: [o.collection],
55
+ dataType: o.dataType,
56
+ samplingMode: o.samplingMode,
57
+ credentials: o.credentials,
58
+ requestHeaders: o.requestHeaders
59
+ });
60
+ this.crs = o.crs;
61
+ this.format = o.format;
62
+ this.reverseAxis = o.reverseAxis;
63
+
64
+ this.datetime = o.datetime;
65
+ this.subset = o.subset;
66
+ this.transparent = typeof o.transparent !== "undefined" ? o.transparent : true;
67
+ this.bgcolor = o.bgcolor;
68
+
69
+ this.proxy = o.proxify;
70
+ this.linkParameters = OgcOpenApiMapsModel.getQueryParameters(o.baseURL);
71
+
72
+ this.modelDescriptor = {
73
+ source: o.baseURL,
74
+ name: o.collection,
75
+ description: "OGC Open API Maps",
76
+ type: super.dataType
77
+ };
78
+ }
79
+
80
+ getTileURL(baseURL: string, tile: TileCoordinate): string | null {
81
+ let aURL = super.getTileURL(baseURL, tile);
82
+ if (aURL) {
83
+ const parts = aURL.split("?");
84
+ const queryString = parts.length>1 ? parts[1] : "";
85
+ const url = parts[0];
86
+ const urlParams = new URLSearchParams(queryString);
87
+ const bbox = urlParams.get('BBOX');
88
+ const urlParameters = {
89
+ ...this.linkParameters,
90
+ bbox:bbox,
91
+ "bbox-crs":this.crs,
92
+ crs:this.crs,
93
+ f: this.format ? this.format : this.linkParameters.f,
94
+ datetime: this.datetime ,
95
+ subset: this.subset ? this.subset.join(",") : undefined,
96
+ transparent: this.transparent,
97
+ bgcolor: this.transparent ? undefined : this.bgcolor
98
+ }
99
+ let transformedUrl = OgcOpenApiTileMatrixTools.appendQueryParameters(url, urlParameters);
100
+ if (typeof this.proxy === "function") {
101
+ const proxyfied = this.proxy(transformedUrl, {});
102
+ transformedUrl = proxyfied.url;
103
+ }
104
+ return transformedUrl;
105
+ } else {
106
+ return aURL
107
+ }
108
+ }
109
+
110
+ private static getQueryParameters(url: string) {
111
+ const parameters: { [key: string]: string } = {};
112
+ const query = url.split("?")[1];
113
+ if (query) new URLSearchParams(query).forEach((value, key) => parameters[key] = value);
114
+ return parameters;
115
+ }
116
+
117
+ // The reference's own bounds for EPSG:3857 are a little larger than the Web Mercator world (y up to 20048966),
118
+ // tiles cover exactly the Web Mercator world instead, the same bounds as LuciadRIA's OSM tiles
119
+ private static getDefaultBounds(reference: CoordinateReference): Bounds | undefined {
120
+ if (reference.identifier !== "EPSG:3857" && reference.identifier !== "EPSG:900913") return undefined;
121
+ return createBounds(reference, [-WEB_MERCATOR_HALF_WORLD, 2 * WEB_MERCATOR_HALF_WORLD, -WEB_MERCATOR_HALF_WORLD, 2 * WEB_MERCATOR_HALF_WORLD]);
122
+ }
123
+
124
+ }
125
+
126
+ export {
127
+ OgcOpenApiMapsModel
128
+ }
@@ -0,0 +1,104 @@
1
+ import type {TileSetLayer} from "./OgcOpenApiGetCapabilities.js";
2
+
3
+ /**
4
+ * Reads the layers of a Mapbox Vector Tile: their names, and the geometryDimension of their features (0 points,
5
+ * 1 lines, 2 surfaces). Only what is needed to style a tileset that does not describe its layers.
6
+ * See https://github.com/mapbox/vector-tile-spec/blob/master/2.1/README.md
7
+ */
8
+ const LAYER = 3;
9
+ const LAYER_NAME = 1;
10
+ const LAYER_FEATURE = 2;
11
+ const FEATURE_TYPE = 3;
12
+ // MVT GeomType POINT, LINESTRING, POLYGON
13
+ const GEOMETRY_DIMENSION = {1: 0, 2: 1, 3: 2};
14
+
15
+ const WIRE_VARINT = 0;
16
+ const WIRE_64BIT = 1;
17
+ const WIRE_LENGTH = 2;
18
+ const WIRE_32BIT = 5;
19
+
20
+ interface Field {
21
+ field: number;
22
+ value: number | Uint8Array;
23
+ }
24
+
25
+ class OgcOpenApiMvtTools {
26
+
27
+ /**
28
+ * The layers of a tile, a layer has geometryDimension when all its features have the same geometry type.
29
+ * The tile may be gzip compressed.
30
+ */
31
+ public static async getLayers(content: ArrayBuffer): Promise<TileSetLayer[]> {
32
+ const bytes = await OgcOpenApiMvtTools.decompress(new Uint8Array(content));
33
+ const layers: TileSetLayer[] = [];
34
+ for (const layer of OgcOpenApiMvtTools.readFields(bytes)) {
35
+ if (layer.field !== LAYER || typeof layer.value === "number") continue;
36
+ let id: string | undefined;
37
+ const dimensions = new Set<number>();
38
+ for (const property of OgcOpenApiMvtTools.readFields(layer.value)) {
39
+ if (property.field === LAYER_NAME && typeof property.value !== "number") {
40
+ id = new TextDecoder().decode(property.value);
41
+ } else if (property.field === LAYER_FEATURE && typeof property.value !== "number") {
42
+ for (const feature of OgcOpenApiMvtTools.readFields(property.value)) {
43
+ if (feature.field === FEATURE_TYPE && typeof feature.value === "number" && feature.value in GEOMETRY_DIMENSION) {
44
+ dimensions.add(GEOMETRY_DIMENSION[feature.value]);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ if (id !== undefined && dimensions.size > 0) {
50
+ layers.push(dimensions.size === 1 ? {id, geometryDimension: dimensions.values().next().value} : {id});
51
+ }
52
+ }
53
+ return layers;
54
+ }
55
+
56
+ private static async decompress(bytes: Uint8Array): Promise<Uint8Array> {
57
+ if (bytes.length < 2 || bytes[0] !== 0x1f || bytes[1] !== 0x8b) return bytes;
58
+ // DecompressionStream is in all current browsers and Node 18
59
+ if (typeof DecompressionStream === "undefined") throw new Error("A gzip compressed vector tile needs DecompressionStream");
60
+ // A copy, as Response takes a Uint8Array over an ArrayBuffer only, not over a SharedArrayBuffer
61
+ const stream = new Response(new Uint8Array(bytes)).body.pipeThrough(new DecompressionStream("gzip"));
62
+ return new Uint8Array(await new Response(stream).arrayBuffer());
63
+ }
64
+
65
+ // The fields of a protobuf message, nested messages are returned as bytes
66
+ private static* readFields(bytes: Uint8Array): Generator<Field> {
67
+ let position = 0;
68
+ const readVarint = () => {
69
+ let result = 0;
70
+ let shift = 0;
71
+ let byte: number;
72
+ do {
73
+ if (position >= bytes.length) throw new Error("Invalid vector tile");
74
+ byte = bytes[position++];
75
+ result += (byte & 0x7f) * Math.pow(2, shift);
76
+ shift += 7;
77
+ } while (byte >= 0x80);
78
+ return result;
79
+ };
80
+ while (position < bytes.length) {
81
+ const key = readVarint();
82
+ const field = Math.floor(key / 8);
83
+ const wireType = key % 8;
84
+ if (wireType === WIRE_VARINT) {
85
+ yield {field, value: readVarint()};
86
+ } else if (wireType === WIRE_LENGTH) {
87
+ const length = readVarint();
88
+ if (position + length > bytes.length) throw new Error("Invalid vector tile");
89
+ yield {field, value: bytes.subarray(position, position + length)};
90
+ position += length;
91
+ } else if (wireType === WIRE_64BIT) {
92
+ position += 8;
93
+ } else if (wireType === WIRE_32BIT) {
94
+ position += 4;
95
+ } else {
96
+ throw new Error("Invalid vector tile");
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ export {
103
+ OgcOpenApiMvtTools
104
+ }
@@ -0,0 +1,187 @@
1
+ import {
2
+ CollectionLinkType,
3
+ type LinkType,
4
+ type OgcOpenApiCapabilitiesCollection,
5
+ type OgcOpenApiCapabilitiesObject,
6
+ OgcOpenApiGetCapabilities,
7
+ type TileSetLayer
8
+ } from "./OgcOpenApiGetCapabilities.js";
9
+ import {OgcOpenApiFetchTools} from "./OgcOpenApiFetchTools.js";
10
+
11
+ export const MAPBOX_STYLE_TYPE = "application/vnd.mapbox.style+json";
12
+
13
+ type RequestHeaders = {[headerName: string]: string;} | null
14
+
15
+ export interface OgcOpenApiStyle {
16
+ id: string;
17
+ title: string;
18
+ // The stylesheets of this style in the Mapbox GL encoding, the only one LuciadRIA reads
19
+ mapboxStylesheets: LinkType[];
20
+ }
21
+
22
+ const DEFAULT_COLORS = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
23
+
24
+ class OgcOpenApiStyleTools {
25
+
26
+ /**
27
+ * Lists the styles of a collection (OGC API Styles) that are available as a Mapbox GL style, or with collection
28
+ * null the styles of the dataset, for its dataset tilesets.
29
+ */
30
+ public static fetchStyles(
31
+ capabilities: OgcOpenApiCapabilitiesObject | null,
32
+ collection: OgcOpenApiCapabilitiesCollection | null,
33
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
34
+ ) {
35
+ return new Promise<OgcOpenApiStyle[]>((resolve, reject) => {
36
+ if (capabilities === null) {
37
+ reject();
38
+ return;
39
+ }
40
+ const href = OgcOpenApiStyleTools.withJsonFormat(OgcOpenApiStyleTools.getStylesHref(capabilities, collection));
41
+ OgcOpenApiFetchTools.fetch(href, {
42
+ method: "GET",
43
+ credentials: options?.credentials ? "same-origin" : "omit",
44
+ headers: {...(options?.requestHeaders ? options.requestHeaders : {}), Accept: "application/json"}
45
+ }, capabilities.serverOptions).then(response => {
46
+ // A collection without styles
47
+ if (response.status === 404) {
48
+ resolve([]);
49
+ return;
50
+ }
51
+ if (response.status !== 200) {
52
+ reject({error: true, message: "Invalid response: " + response.status});
53
+ return;
54
+ }
55
+ response.json().then(data => {
56
+ const styles = data.styles ? data.styles : [];
57
+ resolve(styles.map(style => ({
58
+ id: style.id,
59
+ title: style.title ? style.title : style.id,
60
+ mapboxStylesheets: (style.links ? style.links : [])
61
+ .filter(l => l.rel === "stylesheet" && typeof l.type === "string" && l.type.indexOf(MAPBOX_STYLE_TYPE) === 0)
62
+ // Relative to the styles document
63
+ .map(l => ({...l, href: OgcOpenApiFetchTools.resolveUrl(l.href, href, capabilities.serverOptions)}))
64
+ })).filter(style => style.mapboxStylesheets.length > 0));
65
+ }, () => reject({error: true, message: "Not JSON data"}));
66
+ }, err => reject({error: true, message: err.message}));
67
+ });
68
+ }
69
+
70
+ /**
71
+ * The styles for a tile matrix set. Zoom levels are tile matrix set specific, so a server can have a style per
72
+ * tile matrix set and name it after it (PDOK: bgt_standaardvisualisatie__netherlandsrdnewquad). A style named after
73
+ * another of tileMatrixSetIds, and not after tileMatrixSetId, is left out. Other styles have a stylesheet per tile
74
+ * matrix set or serve all of them (pickStylesheet).
75
+ */
76
+ public static getStylesForTileMatrixSet(styles: OgcOpenApiStyle[], tileMatrixSetId: string, tileMatrixSetIds: string[]) {
77
+ const names = (style: OgcOpenApiStyle, id: string) => (style.id + " " + style.title).toLowerCase().indexOf(id.toLowerCase()) > -1;
78
+ const others = tileMatrixSetIds.filter(id => id.toLowerCase() !== tileMatrixSetId.toLowerCase());
79
+ return styles.filter(style => names(style, tileMatrixSetId) || !others.some(id => names(style, id)));
80
+ }
81
+
82
+ /**
83
+ * A style can have one stylesheet per tile matrix set, because zoom levels are tile matrix set specific.
84
+ * Prefers the stylesheet for tileMatrixSetId, then the one without a tile matrix set.
85
+ */
86
+ public static pickStylesheet(style: OgcOpenApiStyle, tileMatrixSetId: string): LinkType | undefined {
87
+ const forTileMatrixSet = style.mapboxStylesheets.find(l => l.href.indexOf("tile-matrix-set=" + tileMatrixSetId) > -1);
88
+ if (forTileMatrixSet) return forTileMatrixSet;
89
+ const generic = style.mapboxStylesheets.find(l => l.href.indexOf("tile-matrix-set=") === -1);
90
+ return generic ? generic : style.mapboxStylesheets[0];
91
+ }
92
+
93
+ /**
94
+ * Fetches a Mapbox GL style. Properties without a value, as in `"filter" : ,`, are removed so that styles
95
+ * that are not valid JSON can still be used.
96
+ */
97
+ public static fetchMapboxStyle(
98
+ capabilities: OgcOpenApiCapabilitiesObject,
99
+ stylesheet: LinkType,
100
+ options?: {credentials?: boolean, requestHeaders?: RequestHeaders}
101
+ ) {
102
+ return OgcOpenApiFetchTools.fetch(stylesheet.href, {
103
+ method: "GET",
104
+ credentials: options?.credentials ? "same-origin" : "omit",
105
+ headers: {...(options?.requestHeaders ? options.requestHeaders : {}), Accept: MAPBOX_STYLE_TYPE}
106
+ }, capabilities.serverOptions).then(response => {
107
+ if (response.status !== 200) throw new Error("Invalid response: " + response.status);
108
+ return response.text();
109
+ }).then(text => OgcOpenApiStyleTools.parseMapboxStyle(text));
110
+ }
111
+
112
+ public static parseMapboxStyle(text: string): any {
113
+ try {
114
+ return JSON.parse(text);
115
+ } catch (e) {
116
+ const repaired = text
117
+ .replace(/"[^"\\]*"\s*:\s*(?=[,}\]])/g, "")
118
+ .replace(/,(\s*[,}\]])/g, "$1")
119
+ .replace(/,(\s*[,}\]])/g, "$1")
120
+ .replace(/([{\[])(\s*),/g, "$1$2");
121
+ return JSON.parse(repaired);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * A Mapbox GL style that paints every layer of a vector tileset, by geometryDimension: points as dots,
127
+ * lines as lines and surfaces as filled polygons. Layers without geometryDimension get all three, filtered
128
+ * by geometry type.
129
+ */
130
+ public static createDefaultStyle(layers: TileSetLayer[], name?: string): any {
131
+ const styleLayers = [];
132
+ layers.forEach((layer, index) => {
133
+ const color = DEFAULT_COLORS[index % DEFAULT_COLORS.length];
134
+ const dimension = layer.geometryDimension;
135
+ const known = typeof dimension === "number";
136
+ if (!known || dimension === 2) {
137
+ styleLayers.push({
138
+ id: layer.id + "_fill", type: "fill", "source-layer": layer.id,
139
+ ...(known ? {} : {filter: ["==", ["geometry-type"], "Polygon"]}),
140
+ paint: {"fill-color": color, "fill-opacity": 0.4, "fill-outline-color": color}
141
+ });
142
+ }
143
+ if (!known || dimension === 1 || dimension === 2) {
144
+ styleLayers.push({
145
+ id: layer.id + "_line", type: "line", "source-layer": layer.id,
146
+ ...(known ? {} : {filter: ["!=", ["geometry-type"], "Point"]}),
147
+ paint: {"line-color": color, "line-width": dimension === 2 ? 1 : 2}
148
+ });
149
+ }
150
+ if (!known || dimension === 0) {
151
+ // A text dot rather than a circle layer: LuciadRIA 2026.1.1 does not paint circle layers
152
+ styleLayers.push({
153
+ id: layer.id + "_point", type: "symbol", "source-layer": layer.id,
154
+ ...(known ? {} : {filter: ["==", ["geometry-type"], "Point"]}),
155
+ layout: {"text-field": "\u25CF", "text-size": 16, "text-allow-overlap": true},
156
+ paint: {"text-color": color, "text-halo-color": "#ffffff", "text-halo-width": 1}
157
+ });
158
+ }
159
+ });
160
+ return {version: 8, name: name ? name : "default", sources: {}, layers: styleLayers};
161
+ }
162
+
163
+ // Some servers (ldproxy) list the styles link only in the collection document, not in /collections.
164
+ // The styles of the dataset are linked from the landing page.
165
+ private static getStylesHref(capabilities: OgcOpenApiCapabilitiesObject, collection: OgcOpenApiCapabilitiesCollection | null) {
166
+ if (!collection) {
167
+ const datasetLinks = OgcOpenApiGetCapabilities.filterCollectionLinks(capabilities.links ? capabilities.links : [], CollectionLinkType.Styles);
168
+ const datasetLink = datasetLinks.find(l => l.type === "application/json") || datasetLinks.find(l => !l.type);
169
+ return datasetLink ? datasetLink.href : capabilities.baseUrl + "styles";
170
+ }
171
+ const stylesLinks = OgcOpenApiGetCapabilities.filterCollectionLinks(collection.links, CollectionLinkType.Styles);
172
+ const link = stylesLinks.find(l => l.type === "application/json") || stylesLinks.find(l => !l.type);
173
+ if (link) return link.href;
174
+ const self = collection.links.find(l => l.rel === "self");
175
+ const collectionHref = self ? self.href : capabilities.baseUrl + "collections/" + collection.id;
176
+ return OgcOpenApiFetchTools.cleanUrl(collectionHref) + "styles";
177
+ }
178
+
179
+ private static withJsonFormat(href: string) {
180
+ if (/[?&]f=/.test(href)) return href;
181
+ return href + (href.indexOf("?") > -1 ? "&" : "?") + "f=json";
182
+ }
183
+ }
184
+
185
+ export {
186
+ OgcOpenApiStyleTools
187
+ }
@@ -0,0 +1,111 @@
1
+ import {Bounds} from "@luciad/ria/shape/Bounds.js";
2
+ import {CoordinateReference} from "@luciad/ria/reference/CoordinateReference.js";
3
+ import type {TileCoordinate} from "@luciad/ria/model/tileset/TileCoordinate.js";
4
+ import {getReference} from "@luciad/ria/reference/ReferenceProvider.js";
5
+ import {createBounds} from "@luciad/ria/shape/ShapeFactory.js";
6
+ import {OgcOpenApiCrsTools} from "./OgcOpenApiCrsTools.js";
7
+ import {OgcOpenApiGetCapabilities, type TileMatrix, type TileSetData} from "./OgcOpenApiGetCapabilities.js";
8
+
9
+ /**
10
+ * The part of an OGC TileMatrixSet that LuciadRIA can address as a quad-tree:
11
+ * LuciadRIA level 0 maps to tileMatrices[levelOffset].
12
+ */
13
+ export interface OgcOpenApiTileGrid {
14
+ reference: CoordinateReference;
15
+ bounds: Bounds;
16
+ levelOffset: number;
17
+ levelCount: number;
18
+ level0Columns: number;
19
+ level0Rows: number;
20
+ tileWidth: number;
21
+ tileHeight: number;
22
+ tileMatrices: TileMatrix[];
23
+ }
24
+
25
+ class OgcOpenApiTileMatrixTools {
26
+
27
+ public static createTileGrid(tileSet: TileSetData): OgcOpenApiTileGrid {
28
+ if (!tileSet.tileMatrices || tileSet.tileMatrices.length === 0) {
29
+ throw new Error("TileMatrixSet " + tileSet.id + " has no tileMatrices");
30
+ }
31
+ const reference = getReference(OgcOpenApiCrsTools.getReferenceName(tileSet.crs));
32
+ const levelOffset = OgcOpenApiGetCapabilities.getQuadTreeCompatibleLevelOffset(tileSet);
33
+ const tileMatrices = tileSet.tileMatrices.slice(levelOffset);
34
+ const tm0 = tileMatrices[0];
35
+ return {
36
+ reference,
37
+ bounds: OgcOpenApiTileMatrixTools.getTileMatrixBounds(reference, tileSet, tm0),
38
+ levelOffset,
39
+ levelCount: tileMatrices.length,
40
+ level0Columns: tm0.matrixWidth,
41
+ level0Rows: tm0.matrixHeight,
42
+ tileWidth: tm0.tileWidth,
43
+ tileHeight: tm0.tileHeight,
44
+ tileMatrices
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Resolves {tileMatrix}, {tileRow} and {tileCol} for a LuciadRIA tile. LuciadRIA counts rows
50
+ * bottom-up, OGC counts them from the cornerOfOrigin (topLeft by default).
51
+ */
52
+ public static resolveTileTemplate(template: string, grid: OgcOpenApiTileGrid, tile: TileCoordinate): string | null {
53
+ const ogcTile = OgcOpenApiTileMatrixTools.getOgcTile(grid, tile);
54
+ if (ogcTile === null) return null;
55
+ return template
56
+ .replace("{tileMatrix}", ogcTile.tileMatrix)
57
+ .replace("{tileRow}", ogcTile.tileRow.toString())
58
+ .replace("{tileCol}", ogcTile.tileCol.toString());
59
+ }
60
+
61
+ public static getOgcTile(grid: OgcOpenApiTileGrid, tile: TileCoordinate): { tileMatrix: string, tileRow: number, tileCol: number } | null {
62
+ const tileMatrix = grid.tileMatrices[tile.level];
63
+ if (!tileMatrix) return null;
64
+ const tileRow = OgcOpenApiTileMatrixTools.isBottomLeft(tileMatrix) ? tile.y : tileMatrix.matrixHeight - 1 - tile.y;
65
+ return {tileMatrix: tileMatrix.id, tileRow, tileCol: tile.x};
66
+ }
67
+
68
+ // The inverse of getOgcTile, null when the tile matrix is not part of the grid
69
+ public static getLuciadTile(grid: OgcOpenApiTileGrid, tileMatrixId: string, tileRow: number, tileCol: number): TileCoordinate | null {
70
+ const level = grid.tileMatrices.findIndex(m => m.id === tileMatrixId);
71
+ if (level < 0) return null;
72
+ const tileMatrix = grid.tileMatrices[level];
73
+ const y = OgcOpenApiTileMatrixTools.isBottomLeft(tileMatrix) ? tileRow : tileMatrix.matrixHeight - 1 - tileRow;
74
+ return {level, x: tileCol, y};
75
+ }
76
+
77
+ public static appendQueryParameters(url: string, parameters: { [key: string]: string | number | boolean | undefined }): string {
78
+ const query = Object.keys(parameters)
79
+ .filter(k => parameters[k] !== undefined && parameters[k] !== null)
80
+ .map(k => encodeURIComponent(k) + "=" + encodeURIComponent(String(parameters[k])))
81
+ .join("&");
82
+ if (query === "") return url;
83
+ return url + (url.indexOf("?") > -1 ? "&" : "?") + query;
84
+ }
85
+
86
+ private static isBottomLeft(tileMatrix: TileMatrix) {
87
+ return typeof tileMatrix.cornerOfOrigin === "string" && tileMatrix.cornerOfOrigin.toUpperCase() === "BOTTOMLEFT";
88
+ }
89
+
90
+ // The grid extent follows from the origin and cell size, pointOfOrigin is given in orderedAxes order.
91
+ private static getTileMatrixBounds(reference: CoordinateReference, tileSet: TileSetData, tileMatrix: TileMatrix): Bounds {
92
+ const swap = OgcOpenApiTileMatrixTools.isFirstAxisNorthing(tileSet.orderedAxes);
93
+ const originX = tileMatrix.pointOfOrigin[swap ? 1 : 0];
94
+ const originY = tileMatrix.pointOfOrigin[swap ? 0 : 1];
95
+ const width = tileMatrix.cellSize * tileMatrix.tileWidth * tileMatrix.matrixWidth;
96
+ const height = tileMatrix.cellSize * tileMatrix.tileHeight * tileMatrix.matrixHeight;
97
+ const minY = OgcOpenApiTileMatrixTools.isBottomLeft(tileMatrix) ? originY : originY - height;
98
+ return createBounds(reference, [originX, width, minY, height]);
99
+ }
100
+
101
+ private static isFirstAxisNorthing(orderedAxes: string[] | undefined) {
102
+ if (!orderedAxes || orderedAxes.length === 0) return false;
103
+ const axis = orderedAxes[0].toUpperCase();
104
+ // EPSG:3035 names its northing Y and its easting X, with Y first
105
+ return ["LAT", "LATITUDE", "N", "S", "Y", "NORTHING"].indexOf(axis) > -1;
106
+ }
107
+ }
108
+
109
+ export {
110
+ OgcOpenApiTileMatrixTools
111
+ }