ol
Version:
OpenLayers mapping library
486 lines (427 loc) • 14.3 kB
JavaScript
/**
* @module ol/source/ogcTileUtil
*/
import {error as logError} from '../console.js';
import {getIntersection as intersectExtents} from '../extent.js';
import {getJSON, resolveUrl} from '../net.js';
import {get as getProjection} from '../proj.js';
import TileGrid from '../tilegrid/TileGrid.js';
/**
* See https://ogcapi.ogc.org/tiles/.
*/
/**
* @typedef {'map' | 'vector'} TileType
*/
/**
* @typedef {'topLeft' | 'bottomLeft'} CornerOfOrigin
*/
/**
* @typedef {Object} TileSet
* @property {TileType} dataType Type of data represented in the tileset.
* @property {string} [tileMatrixSetDefinition] Reference to a tile matrix set definition.
* @property {TileMatrixSet} [tileMatrixSet] Tile matrix set definition.
* @property {Array<TileMatrixSetLimit>} [tileMatrixSetLimits] Tile matrix set limits.
* @property {Array<Link>} links Tileset links.
*/
/**
* @typedef {Object} Link
* @property {string} rel The link rel attribute.
* @property {string} href The link URL.
* @property {string} type The link type.
*/
/**
* @typedef {Object} TileMatrixSetLimit
* @property {string} tileMatrix The tile matrix id.
* @property {number} minTileRow The minimum tile row.
* @property {number} maxTileRow The maximum tile row.
* @property {number} minTileCol The minimum tile column.
* @property {number} maxTileCol The maximum tile column.
*/
/**
* @typedef {Object} TileMatrixSet
* @property {string} id The tile matrix set identifier.
* @property {string|CrsUri|CrsWkt|CrsReferenceSystem} crs The coordinate reference system.
* @property {Array<string>} [orderedAxes] Axis order.
* @property {Array<TileMatrix>} tileMatrices Array of tile matrices.
*/
/**
* @typedef {Object} CrsUri
* @property {string} uri Reference to one coordinate reference system (CRS).
*/
/**
* @typedef {Object} CrsWkt
* @property {Object} wkt JSON encoding for WKT representation of CRS 2.0.
*/
/**
* @typedef {Object} CrsReferenceSystem
* @property {Object} referenceSystem Data structure as defined in the MD_ReferenceSystem of the ISO 19115.
*/
/**
* @typedef {Object} TileMatrix
* @property {string} id The tile matrix identifier.
* @property {number} cellSize The pixel resolution (map units per pixel).
* @property {Array<number>} pointOfOrigin The map location of the matrix origin.
* @property {CornerOfOrigin} [cornerOfOrigin='topLeft'] The corner of the matrix that represents the origin ('topLeft' or 'bottomLeft').
* @property {number} matrixWidth The number of columns.
* @property {number} matrixHeight The number of rows.
* @property {number} tileWidth The pixel width of a tile.
* @property {number} tileHeight The pixel height of a tile.
*/
/**
* @type {Object<string, boolean>}
*/
const knownMapMediaTypes = {
'image/png': true,
'image/jpeg': true,
'image/gif': true,
'image/webp': true,
};
/**
* @type {Object<string, boolean>}
*/
const knownVectorMediaTypes = {
'application/vnd.mapbox-vector-tile': true,
'application/geo+json': true,
};
/**
* @typedef {Object} TileSetInfo
* @property {string} urlTemplate The tile URL template.
* @property {import("../proj/Projection.js").default} projection The source projection.
* @property {import("../tilegrid/TileGrid.js").default} grid The tile grid.
* @property {import("../Tile.js").UrlFunction} urlFunction The tile URL function.
*/
/**
* @typedef {Object} SourceInfo
* @property {string} url The tile set URL.
* @property {string} mediaType The preferred tile media type.
* @property {Array<string>} [supportedMediaTypes] The supported media types.
* @property {import("../proj/Projection.js").default} projection The source projection.
* @property {Object} [context] Optional context for constructing the URL.
* @property {Array<string>} [collections] Optional collections to append the URL with.
*/
/**
* @param {string} tileUrlTemplate Tile URL template.
* @param {Array<string>} collections List of collections to include as query parameter.
* @return {string} The tile URL template with appended collections query parameter.
*/
export function appendCollectionsQueryParam(tileUrlTemplate, collections) {
if (!collections.length) {
return tileUrlTemplate;
}
// making sure we can always construct a URL instance.
const url = new URL(tileUrlTemplate, 'file:/');
if (url.pathname.split('/').includes('collections')) {
logError(
'The "collections" query parameter cannot be added to collection endpoints',
);
return tileUrlTemplate;
}
// According to conformance class
// http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/collections-selection
// commata in the identifiers of the `collections` query parameter
// need to be URLEncoded, while the commata separating the identifiers
// should not.
const encodedCollections = collections
.map((c) => encodeURIComponent(c))
.join(',');
url.searchParams.append('collections', encodedCollections);
const baseUrl = tileUrlTemplate.split('?')[0];
const queryParams = decodeURIComponent(url.searchParams.toString());
return `${baseUrl}?${queryParams}`;
}
/**
* @param {Array<Link>} links Tileset links.
* @param {string} [mediaType] The preferred media type.
* @param {Array<string>} [collections] Optional collections to append the URL with.
* @return {string} The tile URL template.
*/
export function getMapTileUrlTemplate(links, mediaType, collections) {
let tileUrlTemplate;
let fallbackUrlTemplate;
for (let i = 0; i < links.length; ++i) {
const link = links[i];
if (link.rel === 'item') {
if (link.type === mediaType) {
tileUrlTemplate = link.href;
break;
}
if (knownMapMediaTypes[link.type]) {
fallbackUrlTemplate = link.href;
} else if (!fallbackUrlTemplate && link.type.startsWith('image/')) {
fallbackUrlTemplate = link.href;
}
}
}
if (!tileUrlTemplate) {
if (fallbackUrlTemplate) {
tileUrlTemplate = fallbackUrlTemplate;
} else {
throw new Error('Could not find "item" link');
}
}
if (collections) {
tileUrlTemplate = appendCollectionsQueryParam(tileUrlTemplate, collections);
}
return tileUrlTemplate;
}
/**
* @param {Array<Link>} links Tileset links.
* @param {string} [mediaType] The preferred media type.
* @param {Array<string>} [supportedMediaTypes] The media types supported by the parser.
* @param {Array<string>} [collections] Optional collections to append the URL with.
* @return {string} The tile URL template.
*/
export function getVectorTileUrlTemplate(
links,
mediaType,
supportedMediaTypes,
collections,
) {
let tileUrlTemplate;
let fallbackUrlTemplate;
/**
* Lookup of URL by media type.
* @type {Object<string, string>}
*/
const hrefLookup = {};
for (let i = 0; i < links.length; ++i) {
const link = links[i];
hrefLookup[link.type] = link.href;
if (link.rel === 'item') {
if (link.type === mediaType) {
tileUrlTemplate = link.href;
break;
}
if (knownVectorMediaTypes[link.type]) {
fallbackUrlTemplate = link.href;
}
}
}
if (!tileUrlTemplate && supportedMediaTypes) {
for (let i = 0; i < supportedMediaTypes.length; ++i) {
const supportedMediaType = supportedMediaTypes[i];
if (hrefLookup[supportedMediaType]) {
tileUrlTemplate = hrefLookup[supportedMediaType];
break;
}
}
}
if (!tileUrlTemplate) {
if (fallbackUrlTemplate) {
tileUrlTemplate = fallbackUrlTemplate;
} else {
throw new Error('Could not find "item" link');
}
}
if (collections) {
tileUrlTemplate = appendCollectionsQueryParam(tileUrlTemplate, collections);
}
return tileUrlTemplate;
}
/**
* @param {SourceInfo} sourceInfo The source info.
* @param {TileMatrixSet} tileMatrixSet Tile matrix set.
* @param {string} tileUrlTemplate Tile URL template.
* @param {Array<TileMatrixSetLimit>} [tileMatrixSetLimits] Tile matrix set limits.
* @return {TileSetInfo} Tile set info.
*/
function parseTileMatrixSet(
sourceInfo,
tileMatrixSet,
tileUrlTemplate,
tileMatrixSetLimits,
) {
let projection = sourceInfo.projection;
if (!projection) {
if (typeof tileMatrixSet.crs === 'string') {
projection = getProjection(tileMatrixSet.crs);
} else if ('uri' in tileMatrixSet.crs) {
projection = getProjection(tileMatrixSet.crs.uri);
}
if (!projection) {
throw new Error(`Unsupported CRS: ${JSON.stringify(tileMatrixSet.crs)}`);
}
}
const orderedAxes = tileMatrixSet.orderedAxes;
const axisOrientation = orderedAxes
? orderedAxes
.slice(0, 2)
.map((s) => s.replace(/E|X|Lon/i, 'e').replace(/N|Y|Lat/i, 'n'))
.join('')
: projection.getAxisOrientation();
const backwards = !axisOrientation.startsWith('en');
const matrices = tileMatrixSet.tileMatrices;
/**
* @type {Object<string, TileMatrix>}
*/
const matrixLookup = {};
for (let i = 0; i < matrices.length; ++i) {
const matrix = matrices[i];
matrixLookup[matrix.id] = matrix;
}
/**
* @type {Object<string, TileMatrixSetLimit>}
*/
const limitLookup = {};
/**
* @type {Array<string>}
*/
const matrixIds = [];
if (tileMatrixSetLimits) {
for (let i = 0; i < tileMatrixSetLimits.length; ++i) {
const limit = tileMatrixSetLimits[i];
const id = limit.tileMatrix;
matrixIds.push(id);
limitLookup[id] = limit;
}
} else {
for (let i = 0; i < matrices.length; ++i) {
const id = matrices[i].id;
matrixIds.push(id);
}
}
const length = matrixIds.length;
const origins = new Array(length);
const resolutions = new Array(length);
const sizes = new Array(length);
const tileSizes = new Array(length);
const extent = [-Infinity, -Infinity, Infinity, Infinity];
for (let i = 0; i < length; ++i) {
const id = matrixIds[i];
const matrix = matrixLookup[id];
const origin = matrix.pointOfOrigin;
if (backwards) {
origins[i] = [origin[1], origin[0]];
} else {
origins[i] = origin;
}
resolutions[i] = matrix.cellSize;
sizes[i] = [matrix.matrixWidth, matrix.matrixHeight];
tileSizes[i] = [matrix.tileWidth, matrix.tileHeight];
const limit = limitLookup[id];
if (limit) {
const tileMapWidth = matrix.cellSize * matrix.tileWidth;
const minX = origins[i][0] + limit.minTileCol * tileMapWidth;
const maxX = origins[i][0] + (limit.maxTileCol + 1) * tileMapWidth;
const tileMapHeight = matrix.cellSize * matrix.tileHeight;
const upsideDown = matrix.cornerOfOrigin === 'bottomLeft';
let minY;
let maxY;
if (upsideDown) {
minY = origins[i][1] + limit.minTileRow * tileMapHeight;
maxY = origins[i][1] + (limit.maxTileRow + 1) * tileMapHeight;
} else {
minY = origins[i][1] - (limit.maxTileRow + 1) * tileMapHeight;
maxY = origins[i][1] - limit.minTileRow * tileMapHeight;
}
intersectExtents(extent, [minX, minY, maxX, maxY], extent);
}
}
const tileGrid = new TileGrid({
origins: origins,
resolutions: resolutions,
sizes: sizes,
tileSizes: tileSizes,
extent: tileMatrixSetLimits ? extent : undefined,
});
const context = sourceInfo.context;
const base = sourceInfo.url;
/** @type {import('../Tile.js').UrlFunction} */
function tileUrlFunction(tileCoord, pixelRatio, projection) {
if (!tileCoord) {
return undefined;
}
const id = matrixIds[tileCoord[0]];
const matrix = matrixLookup[id];
const upsideDown = matrix.cornerOfOrigin === 'bottomLeft';
const localContext = {
tileMatrix: id,
tileCol: tileCoord[1],
tileRow: upsideDown ? -tileCoord[2] - 1 : tileCoord[2],
};
if (tileMatrixSetLimits) {
const limit = limitLookup[matrix.id];
if (
localContext.tileCol < limit.minTileCol ||
localContext.tileCol > limit.maxTileCol ||
localContext.tileRow < limit.minTileRow ||
localContext.tileRow > limit.maxTileRow
) {
return undefined;
}
}
Object.assign(localContext, context);
const url = tileUrlTemplate.replace(/\{(\w+?)\}/g, function (m, p) {
return localContext[p];
});
return resolveUrl(base, url);
}
return {
grid: tileGrid,
projection: projection,
urlTemplate: tileUrlTemplate,
urlFunction: tileUrlFunction,
};
}
/**
* @param {SourceInfo} sourceInfo The source info.
* @param {TileSet} tileSet Tile set.
* @return {TileSetInfo|Promise<TileSetInfo>} Tile set info.
*/
function parseTileSetMetadata(sourceInfo, tileSet) {
const tileMatrixSetLimits = tileSet.tileMatrixSetLimits;
/** @type {string} */
let tileUrlTemplate;
if (tileSet.dataType === 'map') {
tileUrlTemplate = getMapTileUrlTemplate(
tileSet.links,
sourceInfo.mediaType,
sourceInfo.collections,
);
} else if (tileSet.dataType === 'vector') {
tileUrlTemplate = getVectorTileUrlTemplate(
tileSet.links,
sourceInfo.mediaType,
sourceInfo.supportedMediaTypes,
sourceInfo.collections,
);
} else {
throw new Error('Expected tileset data type to be "map" or "vector"');
}
if (tileSet.tileMatrixSet) {
return parseTileMatrixSet(
sourceInfo,
tileSet.tileMatrixSet,
tileUrlTemplate,
tileMatrixSetLimits,
);
}
const tileMatrixSetLink = tileSet.links.find(
(link) =>
link.rel === 'http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme',
);
if (!tileMatrixSetLink) {
throw new Error(
'Expected http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme link or tileMatrixSet',
);
}
const tileMatrixSetDefinition = tileMatrixSetLink.href;
const url = resolveUrl(sourceInfo.url, tileMatrixSetDefinition);
return getJSON(url).then(function (tileMatrixSet) {
return parseTileMatrixSet(
sourceInfo,
tileMatrixSet,
tileUrlTemplate,
tileMatrixSetLimits,
);
});
}
/**
* @param {SourceInfo} sourceInfo Source info.
* @return {Promise<TileSetInfo>} Tile set info.
*/
export function getTileSetInfo(sourceInfo) {
return getJSON(sourceInfo.url).then(function (tileSet) {
return parseTileSetMetadata(sourceInfo, tileSet);
});
}