ogcopenapis 0.0.12 → 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/LICENSE +20 -20
- package/README.md +310 -72
- package/lib/OgcOpenApiCrsTools.d.ts +10 -9
- package/lib/OgcOpenApiCrsTools.d.ts.map +1 -0
- package/lib/OgcOpenApiCrsTools.js +40 -39
- package/lib/OgcOpenApiCrsTools.js.map +1 -0
- package/lib/OgcOpenApiFeatureStore.d.ts +94 -93
- package/lib/OgcOpenApiFeatureStore.d.ts.map +1 -0
- package/lib/OgcOpenApiFeatureStore.js +200 -244
- package/lib/OgcOpenApiFeatureStore.js.map +1 -0
- package/lib/OgcOpenApiFetchTools.d.ts +37 -30
- package/lib/OgcOpenApiFetchTools.d.ts.map +1 -0
- package/lib/OgcOpenApiFetchTools.js +68 -50
- package/lib/OgcOpenApiFetchTools.js.map +1 -0
- package/lib/OgcOpenApiGetCapabilities.d.ts +209 -112
- package/lib/OgcOpenApiGetCapabilities.d.ts.map +1 -0
- package/lib/OgcOpenApiGetCapabilities.js +424 -277
- package/lib/OgcOpenApiGetCapabilities.js.map +1 -0
- package/lib/OgcOpenApiMapsModel.d.ts +41 -36
- package/lib/OgcOpenApiMapsModel.d.ts.map +1 -0
- package/lib/OgcOpenApiMapsModel.js +88 -75
- package/lib/OgcOpenApiMapsModel.js.map +1 -0
- package/lib/OgcOpenApiMvtTools.d.ts +12 -0
- package/lib/OgcOpenApiMvtTools.d.ts.map +1 -0
- package/lib/OgcOpenApiMvtTools.js +100 -0
- package/lib/OgcOpenApiMvtTools.js.map +1 -0
- package/lib/OgcOpenApiStyleTools.d.ts +51 -0
- package/lib/OgcOpenApiStyleTools.d.ts.map +1 -0
- package/lib/OgcOpenApiStyleTools.js +155 -0
- package/lib/OgcOpenApiStyleTools.js.map +1 -0
- package/lib/OgcOpenApiTileMatrixTools.d.ts +41 -0
- package/lib/OgcOpenApiTileMatrixTools.d.ts.map +1 -0
- package/lib/OgcOpenApiTileMatrixTools.js +88 -0
- package/lib/OgcOpenApiTileMatrixTools.js.map +1 -0
- package/lib/OgcOpenApiTilesModel.d.ts +56 -31
- package/lib/OgcOpenApiTilesModel.d.ts.map +1 -0
- package/lib/OgcOpenApiTilesModel.js +167 -128
- package/lib/OgcOpenApiTilesModel.js.map +1 -0
- package/lib/OgcOpenApiVectorTilesModel.d.ts +39 -0
- package/lib/OgcOpenApiVectorTilesModel.d.ts.map +1 -0
- package/lib/OgcOpenApiVectorTilesModel.js +122 -0
- package/lib/OgcOpenApiVectorTilesModel.js.map +1 -0
- package/lib/OgcOpenApiWmtsTools.d.ts +30 -0
- package/lib/OgcOpenApiWmtsTools.d.ts.map +1 -0
- package/lib/OgcOpenApiWmtsTools.js +89 -0
- package/lib/OgcOpenApiWmtsTools.js.map +1 -0
- package/package.json +56 -27
- package/src/OgcOpenApiCrsTools.ts +40 -0
- package/src/OgcOpenApiFeatureStore.ts +296 -0
- package/src/OgcOpenApiFetchTools.ts +84 -0
- package/src/OgcOpenApiGetCapabilities.ts +641 -0
- package/src/OgcOpenApiMapsModel.ts +128 -0
- package/src/OgcOpenApiMvtTools.ts +104 -0
- package/src/OgcOpenApiStyleTools.ts +187 -0
- package/src/OgcOpenApiTileMatrixTools.ts +111 -0
- package/src/OgcOpenApiTilesModel.ts +214 -0
- package/src/OgcOpenApiVectorTilesModel.ts +141 -0
- package/src/OgcOpenApiWmtsTools.ts +114 -0
- package/lib/OgcOpenApiGetCollectionTiles.d.ts +0 -7
- 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
|
+
}
|