From 0258811afc1447bcf797069f78080e1bd04d745d Mon Sep 17 00:00:00 2001 From: pyshx Date: Thu, 2 Mar 2023 11:08:14 +0530 Subject: [PATCH 1/2] feat: support wmts in reearth/core --- .../engines/Cesium/Feature/Raster/hooks.ts | 22 +- .../Cesium/Feature/Raster/index.stories.tsx | 31 ++ .../engines/Cesium/Feature/Raster/index.tsx | 3 +- .../engines/Cesium/Feature/Raster/wmts.ts | 497 ++++++++++++++++++ src/core/engines/Cesium/index.tsx | 2 +- src/core/mantle/types/index.ts | 3 +- 6 files changed, 554 insertions(+), 4 deletions(-) create mode 100644 src/core/engines/Cesium/Feature/Raster/wmts.ts diff --git a/src/core/engines/Cesium/Feature/Raster/hooks.ts b/src/core/engines/Cesium/Feature/Raster/hooks.ts index 9f13e7fc4..03bf15ff2 100644 --- a/src/core/engines/Cesium/Feature/Raster/hooks.ts +++ b/src/core/engines/Cesium/Feature/Raster/hooks.ts @@ -7,13 +7,14 @@ import { } from "cesium"; import { MVTImageryProvider } from "cesium-mvt-imagery-provider"; import md5 from "js-md5"; -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useCesium } from "resium"; import type { ComputedFeature, ComputedLayer, Feature } from "../../.."; import { extractSimpleLayer, extractSimpleLayerData } from "../utils"; import { Props } from "./types"; +import { getWMTSImageryProvider } from "./wmts"; const useImageryProvider = (imageryProvider: ImageryProvider | undefined) => { const { viewer } = useCesium(); @@ -64,6 +65,25 @@ export const useWMS = ({ useImageryProvider(imageryProvider); }; +export const useWMTS = async ({ isVisible, layer }: Pick) => { + const { type, url, layers } = useData(layer); + const [imageryProvider, setImageryProvider] = useState(); + + console.log("Reached HERE"); + + useEffect(() => { + const getImageryProvider = async () => { + const provider = await getWMTSImageryProvider(url, layers); + setImageryProvider(provider); + }; + if (isVisible && type === "wmts") { + getImageryProvider(); + } + }, [isVisible, type, url, layers]); + + useImageryProvider(imageryProvider); +}; + type TileCoords = { x: number; y: number; level: number }; const idFromGeometry = ( diff --git a/src/core/engines/Cesium/Feature/Raster/index.stories.tsx b/src/core/engines/Cesium/Feature/Raster/index.stories.tsx index f88587ac7..4f694a545 100644 --- a/src/core/engines/Cesium/Feature/Raster/index.stories.tsx +++ b/src/core/engines/Cesium/Feature/Raster/index.stories.tsx @@ -42,6 +42,37 @@ WMS.args = { }, }; +export const WMTS = Template.bind([]); +WMTS.args = { + engine: "cesium", + engines: { + cesium: engine, + }, + ready: true, + layers: [ + { + id: "l", + type: "simple", + data: { + type: "wmts", + url: "https://soggy2.zoology.ubc.ca/geoserver/gwc/service/wmts?", + layers: "dem_ssea_27m", + }, + raster: { + maximumLevel: 100, + }, + }, + ], + property: { + tiles: [ + { + id: "default", + tile_type: "default", + }, + ], + }, +}; + export const MVT = Template.bind([]); MVT.args = { engine: "cesium", diff --git a/src/core/engines/Cesium/Feature/Raster/index.tsx b/src/core/engines/Cesium/Feature/Raster/index.tsx index e159fc4b1..d911549a5 100644 --- a/src/core/engines/Cesium/Feature/Raster/index.tsx +++ b/src/core/engines/Cesium/Feature/Raster/index.tsx @@ -3,7 +3,7 @@ import { memo } from "react"; import { extractSimpleLayer, extractSimpleLayerData, type FeatureComponentConfig } from "../utils"; -import { useMVT, useWMS } from "./hooks"; +import { useMVT, useWMS, useWMTS } from "./hooks"; import type { Props } from "./types"; function Raster({ @@ -14,6 +14,7 @@ function Raster({ evalFeature, onFeatureDelete, }: Props) { + useWMTS({ isVisible, layer }); useWMS({ isVisible, layer, property }); useMVT({ isVisible, layer, property, evalFeature, onComputedFeatureFetch, onFeatureDelete }); diff --git a/src/core/engines/Cesium/Feature/Raster/wmts.ts b/src/core/engines/Cesium/Feature/Raster/wmts.ts new file mode 100644 index 000000000..55ef0ae58 --- /dev/null +++ b/src/core/engines/Cesium/Feature/Raster/wmts.ts @@ -0,0 +1,497 @@ +import { WebMapTileServiceImageryProvider, WebMercatorTilingScheme } from "cesium"; +import { XMLParser, X2jOptionsOptional } from "fast-xml-parser"; + +import { f, isDefined } from "@reearth/core/mantle/data/utils"; + +export const getWMTSImageryProvider = async (url?: string, layerCode?: string) => { + if (!isDefined(url) || !isDefined(layerCode)) return; + const capabilitiesUrl = getCapabilitiesUrl(url); + console.log("capablities: ", capabilitiesUrl); + if (!isDefined(capabilitiesUrl)) return; + const capabilities = await getCapablitiesFromUrl(capabilitiesUrl); + const layer = findLayerFromCapablities(capabilities, layerCode); + const layerIdentifier = layer?.Identifier; + if (!isDefined(layer) || !isDefined(layerIdentifier)) { + return; + } + + let format = "image/png"; + const formats = layer.Format; + if (formats && formats?.indexOf("image/png") === -1 && formats?.indexOf("image/jpeg") !== -1) { + format = "image/jpeg"; + } + + // if layer has defined ResourceURL we should use it because some layers support only Restful encoding. See #2927 + const resourceUrl: ResourceUrl | ResourceUrl[] | undefined = layer.ResourceURL; + const tempUrl = new URL(url); + tempUrl.search = ""; + let baseUrl: string = tempUrl.toString(); + + if (resourceUrl) { + if (Array.isArray(resourceUrl)) { + for (let i = 0; i < resourceUrl.length; i++) { + const url: ResourceUrl = resourceUrl[i]; + if (url.format.indexOf(format) !== -1 || url.format.indexOf("png") !== -1) { + baseUrl = url.template; + } + } + } else { + if (format === resourceUrl.format || resourceUrl.format.indexOf("png") !== -1) { + baseUrl = resourceUrl.template; + } + } + } + + const style = getStyle(layer); + + const tileMatrixSet = getTileMatrixSet(layer, capabilities.tileMatrixSets); + if (!isDefined(tileMatrixSet)) { + return; + } + + const imageryProvider = new WebMapTileServiceImageryProvider({ + url: baseUrl, + layer: layerIdentifier, + style: style ?? "default", + tileMatrixSetID: tileMatrixSet.id, + tileMatrixLabels: tileMatrixSet.labels, + minimumLevel: tileMatrixSet.minLevel, + maximumLevel: tileMatrixSet.maxLevel, + tileWidth: tileMatrixSet.tileWidth, + tileHeight: tileMatrixSet.tileHeight, + tilingScheme: new WebMercatorTilingScheme(), + format, + }); + return imageryProvider; +}; + +const getCapabilitiesUrl = (uri: string): string | undefined => { + if (uri) { + const url = new URL(uri); + url.searchParams.set("service", "WMTS"); + url.searchParams.set("version", "1.0.0"); + url.searchParams.set("request", "GetCapabilities"); + return url.toString(); + } else { + return undefined; + } +}; + +const getCapablitiesFromUrl = async (url: string): Promise => { + const xmlDataStr = await (await f(url)).text(); + const options: X2jOptionsOptional = { + ignoreAttributes: false, + attributeNamePrefix: "@_", + }; + const parser = new XMLParser(options); + const capabilitiesJson = parser.parse(xmlDataStr) as CapabilitiesJson; + + console.log("capabilities: ", capabilitiesJson); + + const layers: WmtsLayer[] = []; + const tileMatrixSets: TileMatrixSet[] = []; + + const layerElements = capabilitiesJson.Contents?.Layer as Array | WmtsLayer; + if (layerElements && Array.isArray(layerElements)) { + layers.push(...layerElements); + } else if (layerElements) { + layers.push(layerElements as WmtsLayer); + } + + const tileMatrixSetsElements = capabilitiesJson.Contents?.TileMatrixSet as + | Array + | TileMatrixSet; + if (tileMatrixSetsElements && Array.isArray(tileMatrixSetsElements)) { + tileMatrixSets.push(...tileMatrixSetsElements); + } else if (tileMatrixSetsElements) { + tileMatrixSets.push(tileMatrixSetsElements as TileMatrixSet); + } + + return { + layers, + tileMatrixSets, + }; +}; + +const findLayerFromCapablities = ( + capabilities: WmtsCapabilities, + name: string, +): WmtsLayer | undefined => { + // Look for an exact match on the name. + if (capabilities.layers === undefined) { + return undefined; + } + let match = capabilities.layers.find(layer => layer.Identifier === name || layer.Title === name); + if (!match) { + const colonIndex = name.indexOf(":"); + if (colonIndex >= 0) { + // This looks like a namespaced name. Such names will (usually?) show up in GetCapabilities + // as just their name without the namespace qualifier. + const nameWithoutNamespace = name.substring(colonIndex + 1); + match = capabilities.layers.find( + layer => layer.Identifier === nameWithoutNamespace || layer.Title === nameWithoutNamespace, + ); + } + } + + return match; +}; + +const getTileMatrixSet = ( + layer: WmtsLayer | undefined, + matrixSets: TileMatrixSet[], +): + | { + id: string; + labels: string[]; + maxLevel: number; + minLevel: number; + tileWidth: number; + tileHeight: number; + } + | undefined => { + const usableTileMatrixSets = getUsableTileMatrixSets(matrixSets); + + let tileMatrixSetLinks: TileMatrixSetLink[] = []; + if (layer?.TileMatrixSetLink) { + if (Array.isArray(layer?.TileMatrixSetLink)) { + tileMatrixSetLinks = [...layer.TileMatrixSetLink]; + } else { + tileMatrixSetLinks = [layer.TileMatrixSetLink]; + } + } + + let tileMatrixSetId = "urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible"; + let maxLevel = 0; + let minLevel = 0; + let tileWidth = 256; + let tileHeight = 256; + let tileMatrixSetLabels: string[] = []; + for (let i = 0; i < tileMatrixSetLinks.length; i++) { + const tileMatrixSet = tileMatrixSetLinks[i].TileMatrixSet; + if (usableTileMatrixSets?.[tileMatrixSet]) { + tileMatrixSetId = tileMatrixSet; + tileMatrixSetLabels = usableTileMatrixSets[tileMatrixSet].identifiers; + tileWidth = Number(usableTileMatrixSets[tileMatrixSet].tileWidth); + tileHeight = Number(usableTileMatrixSets[tileMatrixSet].tileHeight); + break; + } + } + + if (Array.isArray(tileMatrixSetLabels)) { + const levels = tileMatrixSetLabels.map(label => { + const lastIndex = label.lastIndexOf(":"); + return Math.abs(Number(label.substring(lastIndex + 1))); + }); + maxLevel = levels.reduce((currentMaximum, level) => { + return level > currentMaximum ? level : currentMaximum; + }, 0); + minLevel = levels.reduce((currentMaximum, level) => { + return level < currentMaximum ? level : currentMaximum; + }, 0); + } + + return { + id: tileMatrixSetId, + labels: tileMatrixSetLabels, + maxLevel: maxLevel, + minLevel: minLevel, + tileWidth: tileWidth, + tileHeight: tileHeight, + }; +}; + +const getUsableTileMatrixSets = (matrixSets: TileMatrixSet[]) => { + const usableTileMatrixSets: { [key: string]: UsableTileMatrixSets } = { + "urn:ogc:def:wkss:OGC:1.0:GoogleMapsCompatible": { + identifiers: ["0"], + tileWidth: 256, + tileHeight: 256, + }, + }; + + const standardTilingScheme = new WebMercatorTilingScheme(); + + if (matrixSets === undefined) { + return; + } + for (let i = 0; i < matrixSets.length; i++) { + const matrixSet = matrixSets[i]; + if ( + !matrixSet.SupportedCRS || + (!/EPSG.*900913/.test(matrixSet.SupportedCRS) && !/EPSG.*3857/.test(matrixSet.SupportedCRS)) + ) { + continue; + } + // Usable tile matrix sets must have a single 256x256 tile at the root. + const matrices = matrixSet.TileMatrix; + if (!isDefined(matrices) || matrices.length < 1) { + continue; + } + + const levelZeroMatrix = matrices[0]; + + if (!isDefined(levelZeroMatrix.TopLeftCorner)) { + continue; + } + + const levelZeroTopLeftCorner = levelZeroMatrix.TopLeftCorner.split(" "); + const startX = parseFloat(levelZeroTopLeftCorner[0]); + const startY = parseFloat(levelZeroTopLeftCorner[1]); + const rectangleInMeters = standardTilingScheme.rectangleToNativeRectangle( + standardTilingScheme.rectangle, + ); + if ( + Math.abs(startX - rectangleInMeters.west) > 1 || + Math.abs(startY - rectangleInMeters.north) > 1 + ) { + continue; + } + + if (isDefined(matrixSet.TileMatrix) && matrixSet.TileMatrix.length > 0) { + const ids = matrixSet.TileMatrix.map(function (item: { Identifier: any }) { + return item.Identifier; + }); + const firstTile = matrixSet.TileMatrix[0]; + usableTileMatrixSets[matrixSet.Identifier] = { + identifiers: ids, + tileWidth: firstTile.TileWidth, + tileHeight: firstTile.TileHeight, + }; + } + } + + return usableTileMatrixSets; +}; + +const getStyle = (capabilitiesLayer: WmtsLayer): string | undefined => { + const availStyles = getAvailStyles(capabilitiesLayer); + const layerAvailableStyles = availStyles.find( + candidate => candidate.layerName === capabilitiesLayer?.Identifier, + )?.styles; + + return ( + layerAvailableStyles?.find((style: WmtsAvailStyle) => style.isDefault)?.identifier ?? + layerAvailableStyles?.[0]?.identifier + ); +}; + +const getAvailStyles = (capabilitiesLayer: WmtsLayer): WmtsAvailLayerStyle[] => { + const result: any = []; + const layer = capabilitiesLayer; + if (!layer) { + return result; + } + const styles: ReadonlyArray = layer?.Style + ? Array.isArray(layer.Style) + ? layer.Style + : [layer.Style] + : []; + result.push({ + layerName: layer?.Identifier, + styles: styles.map((style: CapabilitiesStyle) => { + return { + identifier: style.Identifier, + isDefault: style.isDefault, + abstract: style.Abstract, + }; + }), + }); + + return result; +}; + +type WmtsAvailLayerStyle = { + layerName?: string; + styles?: WmtsAvailStyle[]; +}; + +type WmtsAvailStyle = { + identifier?: string; + title?: string; + abstract?: string; + isDefault: boolean; +}; + +type WmtsLayer = { + // according to start WMTS only have title + readonly Title: string; + readonly Abstract?: string; + readonly Identifier?: string; + readonly WGS84BoundingBox?: BoundingBox; + readonly Style?: CapabilitiesStyle | CapabilitiesStyle[]; + readonly Format?: string | ReadonlyArray; + readonly infoFormat?: string | ReadonlyArray; + readonly TileMatrixSetLink?: TileMatrixSetLink | TileMatrixSetLink[]; + readonly ResourceURL?: ResourceUrl | ResourceUrl[]; +}; + +type WmtsCapabilitiesLegend = CapabilitiesLegend & { + readonly OnlineResource?: undefined; + readonly "xlink:href"?: string; +}; + +type CapabilitiesLegend = { + readonly OnlineResource?: OnlineResource; + readonly MinScaleDenominator?: number; + readonly MaxScaleDenominator?: number; + readonly Format?: string; + readonly width?: number; + readonly height?: number; +}; + +type ResourceUrl = { + format: string; + resourceType: "tile"; + template: string; +}; + +type CapabilitiesStyle = { + readonly Identifier: string; + readonly Title: string; + readonly Abstract?: string; + readonly Keywords?: OwsKeywordList; + readonly LegendURL?: WmtsCapabilitiesLegend | ReadonlyArray; + readonly isDefault?: boolean; +}; + +type CapabilitiesJson = { + readonly Version: string; + readonly Contents?: Contents; + readonly ServiceIdentification?: ServiceIdentification; + readonly ServiceProvider?: ServiceProvider; + readonly OperationsMetadata?: OperationsMetadata; + readonly ServiceMetadataURL?: OnlineResource; +}; + +type OperationsMetadata = { + readonly Operation: Operation; +}; + +type Operation = { + name: string; + DCP: { + HTTP: { + Get?: OnlineResource; + }; + }; +}; + +type OnlineResource = { + "xlink:type"?: string; + "xlink:href": string; +}; + +type ServiceIdentification = { + readonly Title?: string; + readonly Abstract?: string; + readonly Fees?: string; + readonly AccessConstraints?: string; + readonly Keywords?: OwsKeywordList; + readonly ServiceType: string; + readonly ServiceTypeVersion: string; +}; + +type ServiceProvider = { + readonly ProviderName?: string; + readonly ProviderSite?: OnlineResource; + readonly ServiceContact?: ServiceContact; +}; + +type ServiceContact = { + readonly InvidualName?: string; + readonly PositionName?: string; + readonly ContactInfo?: ContactInfo; + readonly Role?: string; +}; + +type ContactInfo = { + Phone?: Phone; + Address?: ContactInfoAddress; + OnlineResource?: OnlineResource; + HoursOfService?: string; + ContactInstructions?: string; +}; + +type ContactInfoAddress = { + AddressType?: string; + DeliveryPoint?: string; + City?: string; + AdministrativeArea?: string; + PostalCode?: string; + Country?: string; + ElectronicMailAddress?: string; +}; + +type Phone = { + Voice?: string; + Facsimile?: string; +}; + +type Contents = { + readonly Layer: WmtsLayer; + readonly TileMatrixSet: TileMatrixSet; +}; + +type TileMatrixSetLink = { + readonly TileMatrixSet: string; + readonly TileMatrixSetLimits: TileMatrixSetLimits; +}; + +type TileMatrixSetLimits = { + readonly TileMatrixLimits: TileMatrixLimits[]; +}; + +type TileMatrixLimits = { + readonly TileMatrix: any; + readonly MinTileRow: number; + readonly MaxTileRow: number; + readonly MinTileCol: number; + readonly MaxTileCol: number; +}; + +type TileMatrixSet = { + readonly Identifier: string; + readonly Title?: string; + readonly Abstract?: string; + readonly Keyword?: OwsKeywordList; + readonly SupportedCRS?: string; + readonly WellKnowScaleSet?: string; + readonly TileMatrix: TileMatrix[]; +}; + +type TileMatrix = { + readonly Identifier: string; + readonly Title?: string; + readonly Abstract?: string; + readonly Keyword?: OwsKeywordList; + readonly ScaleDenominator: number; + readonly TopLeftCorner: string; // there is a wrong indication of TopLevelPoint in WMTS 1.0.0 specification + readonly TileWidth: number; + readonly TileHeight: number; + readonly MatrixWidth: number; + readonly MatrixHeight: number; +}; + +type BoundingBox = { + LowerCorner: string; + UpperCorner: string; + crs?: string; + dimensions?: string; +}; + +type OwsKeywordList = { + readonly Keyword: string | string[]; + readonly type?: string; +}; + +type UsableTileMatrixSets = { + identifiers: string[]; + tileWidth: number; + tileHeight: number; +}; + +type WmtsCapabilities = { + layers: WmtsLayer[]; + tileMatrixSets: TileMatrixSet[]; +}; diff --git a/src/core/engines/Cesium/index.tsx b/src/core/engines/Cesium/index.tsx index abeb50d7e..dbb1b047d 100644 --- a/src/core/engines/Cesium/index.tsx +++ b/src/core/engines/Cesium/index.tsx @@ -195,5 +195,5 @@ export const engine: Engine = { component: Component, featureComponent: Feature, clusterComponent: Cluster, - delegatedDataTypes: ["czml", "wms", "mvt", "3dtiles", "osm-buildings"], + delegatedDataTypes: ["czml", "wms", "mvt", "3dtiles", "osm-buildings", "wmts"], }; diff --git a/src/core/mantle/types/index.ts b/src/core/mantle/types/index.ts index cdaf4a5a8..bebbffcc5 100644 --- a/src/core/mantle/types/index.ts +++ b/src/core/mantle/types/index.ts @@ -96,7 +96,8 @@ export type DataType = | "shapefile" | "gtfs" | "gml" - | "georss"; + | "georss" + | "wmts"; export type TimeInterval = [start: Date, end?: Date]; From 87d1c23a09bf5a3c57df53a135011344d86f14a5 Mon Sep 17 00:00:00 2001 From: pyshx Date: Fri, 10 Mar 2023 17:52:14 +0530 Subject: [PATCH 2/2] fix: wip --- .storybook/main.ts | 1 + src/config/index.ts | 10 ++-- .../engines/Cesium/Feature/Raster/hooks.ts | 15 +++-- .../Cesium/Feature/Raster/index.stories.tsx | 4 +- .../engines/Cesium/Feature/Raster/index.tsx | 2 +- .../engines/Cesium/Feature/Raster/wmts.ts | 58 +++++++++++++++---- src/core/engines/Cesium/Feature/index.tsx | 1 + src/core/mantle/data/index.test.ts | 12 ++++ 8 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 src/core/mantle/data/index.test.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 3721bf7b3..2a5176455 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -33,6 +33,7 @@ const config: StorybookViteConfig = { } : {}, resolve: { + dedupe: ["@storybook/client-api"], alias: [ { find: "crypto", replacement: "crypto-js" }, // quickjs-emscripten { find: "@reearth", replacement: resolve(__dirname, "..", "src") }, diff --git a/src/config/index.ts b/src/config/index.ts index be0c8fc20..e68fccd04 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,6 +1,6 @@ import { Viewer } from "cesium"; -import { Extensions, loadExtensions } from "./extensions"; +import { Extensions } from "./extensions"; export type Config = { version?: string; @@ -152,10 +152,10 @@ export default async function loadConfig() { ); } - if (config?.extensionUrls) { - const extensions = await loadExtensions(config.extensionUrls); - config.extensions = extensions; - } + // if (config?.extensionUrls) { + // const extensions = await loadExtensions(config.extensionUrls); + // config.extensions = extensions; + // } window.REEARTH_CONFIG = config; } diff --git a/src/core/engines/Cesium/Feature/Raster/hooks.ts b/src/core/engines/Cesium/Feature/Raster/hooks.ts index 03bf15ff2..711e8752a 100644 --- a/src/core/engines/Cesium/Feature/Raster/hooks.ts +++ b/src/core/engines/Cesium/Feature/Raster/hooks.ts @@ -65,21 +65,26 @@ export const useWMS = ({ useImageryProvider(imageryProvider); }; -export const useWMTS = async ({ isVisible, layer }: Pick) => { +export const useWMTS = ({ + isVisible, + layer, + property, +}: Pick) => { + const { credit } = property ?? {}; + const { type, url, layers } = useData(layer); const [imageryProvider, setImageryProvider] = useState(); - console.log("Reached HERE"); - useEffect(() => { const getImageryProvider = async () => { - const provider = await getWMTSImageryProvider(url, layers); + console.log("getImageryProvider called"); + const provider = await getWMTSImageryProvider(url, layers, credit); setImageryProvider(provider); }; if (isVisible && type === "wmts") { getImageryProvider(); } - }, [isVisible, type, url, layers]); + }, [isVisible, type, url, layers, credit]); useImageryProvider(imageryProvider); }; diff --git a/src/core/engines/Cesium/Feature/Raster/index.stories.tsx b/src/core/engines/Cesium/Feature/Raster/index.stories.tsx index 4f694a545..2f1b78a5c 100644 --- a/src/core/engines/Cesium/Feature/Raster/index.stories.tsx +++ b/src/core/engines/Cesium/Feature/Raster/index.stories.tsx @@ -55,8 +55,8 @@ WMTS.args = { type: "simple", data: { type: "wmts", - url: "https://soggy2.zoology.ubc.ca/geoserver/gwc/service/wmts?", - layers: "dem_ssea_27m", + url: "http://basemap.nationalmap.gov/arcgis/rest/services/USGSShadedReliefOnly/MapServer/WMTS", + layers: "USGSShadedReliefOnly", }, raster: { maximumLevel: 100, diff --git a/src/core/engines/Cesium/Feature/Raster/index.tsx b/src/core/engines/Cesium/Feature/Raster/index.tsx index d911549a5..deffc1f1b 100644 --- a/src/core/engines/Cesium/Feature/Raster/index.tsx +++ b/src/core/engines/Cesium/Feature/Raster/index.tsx @@ -14,7 +14,7 @@ function Raster({ evalFeature, onFeatureDelete, }: Props) { - useWMTS({ isVisible, layer }); + useWMTS({ isVisible, layer, property }); useWMS({ isVisible, layer, property }); useMVT({ isVisible, layer, property, evalFeature, onComputedFeatureFetch, onFeatureDelete }); diff --git a/src/core/engines/Cesium/Feature/Raster/wmts.ts b/src/core/engines/Cesium/Feature/Raster/wmts.ts index 55ef0ae58..d544bdc9a 100644 --- a/src/core/engines/Cesium/Feature/Raster/wmts.ts +++ b/src/core/engines/Cesium/Feature/Raster/wmts.ts @@ -1,15 +1,21 @@ -import { WebMapTileServiceImageryProvider, WebMercatorTilingScheme } from "cesium"; +import { + GeographicTilingScheme, + WebMapTileServiceImageryProvider, + WebMercatorTilingScheme, +} from "cesium"; import { XMLParser, X2jOptionsOptional } from "fast-xml-parser"; import { f, isDefined } from "@reearth/core/mantle/data/utils"; -export const getWMTSImageryProvider = async (url?: string, layerCode?: string) => { +export const getWMTSImageryProvider = async (url?: string, layerCode?: string, credit?: string) => { if (!isDefined(url) || !isDefined(layerCode)) return; const capabilitiesUrl = getCapabilitiesUrl(url); - console.log("capablities: ", capabilitiesUrl); if (!isDefined(capabilitiesUrl)) return; const capabilities = await getCapablitiesFromUrl(capabilitiesUrl); + console.log("capablities: ", capabilities); + console.log("layerCode: ", layerCode); const layer = findLayerFromCapablities(capabilities, layerCode); + console.log("layerFromCode: ", layer); const layerIdentifier = layer?.Identifier; if (!isDefined(layer) || !isDefined(layerIdentifier)) { return; @@ -43,24 +49,29 @@ export const getWMTSImageryProvider = async (url?: string, layerCode?: string) = } const style = getStyle(layer); + console.log("obtained style: ", style); + console.log("capabilities.tileMatrixSets: ", capabilities.tileMatrixSets); const tileMatrixSet = getTileMatrixSet(layer, capabilities.tileMatrixSets); + console.log("tileMatrixSet: ", tileMatrixSet); if (!isDefined(tileMatrixSet)) { return; } + console.log("format: ", format); const imageryProvider = new WebMapTileServiceImageryProvider({ url: baseUrl, layer: layerIdentifier, - style: style ?? "default", + style: "default", tileMatrixSetID: tileMatrixSet.id, tileMatrixLabels: tileMatrixSet.labels, minimumLevel: tileMatrixSet.minLevel, maximumLevel: tileMatrixSet.maxLevel, tileWidth: tileMatrixSet.tileWidth, tileHeight: tileMatrixSet.tileHeight, - tilingScheme: new WebMercatorTilingScheme(), + tilingScheme: new GeographicTilingScheme(), format, + credit, }); return imageryProvider; }; @@ -79,18 +90,22 @@ const getCapabilitiesUrl = (uri: string): string | undefined => { const getCapablitiesFromUrl = async (url: string): Promise => { const xmlDataStr = await (await f(url)).text(); + const cleanXml = xmlDataStr.replace(/(ows|psf):/g, ""); + console.log("cleanXMl: ", cleanXml); const options: X2jOptionsOptional = { ignoreAttributes: false, - attributeNamePrefix: "@_", + attributeNamePrefix: "", }; const parser = new XMLParser(options); - const capabilitiesJson = parser.parse(xmlDataStr) as CapabilitiesJson; - - console.log("capabilities: ", capabilitiesJson); + const capabilities = parser.parse(cleanXml) as Capabilities; + console.log("capabilites: ", capabilities); + const capabilitiesJson = capabilities.Capabilities; const layers: WmtsLayer[] = []; const tileMatrixSets: TileMatrixSet[] = []; + console.log("capabilitesJson: ", capabilitiesJson); + const layerElements = capabilitiesJson.Contents?.Layer as Array | WmtsLayer; if (layerElements && Array.isArray(layerElements)) { layers.push(...layerElements); @@ -121,7 +136,9 @@ const findLayerFromCapablities = ( if (capabilities.layers === undefined) { return undefined; } + console.log("capabilities.layers: ", capabilities.layers); let match = capabilities.layers.find(layer => layer.Identifier === name || layer.Title === name); + console.log("match: ", match); if (!match) { const colonIndex = name.indexOf(":"); if (colonIndex >= 0) { @@ -167,6 +184,8 @@ const getTileMatrixSet = ( let tileWidth = 256; let tileHeight = 256; let tileMatrixSetLabels: string[] = []; + console.log("tileMatrixSetLinks: ", tileMatrixSetLinks); + console.log("usableTileMatrixSets: ", usableTileMatrixSets); for (let i = 0; i < tileMatrixSetLinks.length; i++) { const tileMatrixSet = tileMatrixSetLinks[i].TileMatrixSet; if (usableTileMatrixSets?.[tileMatrixSet]) { @@ -215,14 +234,16 @@ const getUsableTileMatrixSets = (matrixSets: TileMatrixSet[]) => { if (matrixSets === undefined) { return; } + console.log("matrixSet: ", matrixSets); for (let i = 0; i < matrixSets.length; i++) { const matrixSet = matrixSets[i]; if ( !matrixSet.SupportedCRS || - (!/EPSG.*900913/.test(matrixSet.SupportedCRS) && !/EPSG.*3857/.test(matrixSet.SupportedCRS)) + (/EPSG.*900913/.test(matrixSet.SupportedCRS) && !/EPSG.*3857/.test(matrixSet.SupportedCRS)) ) { continue; } + console.log("made it here!!"); // Usable tile matrix sets must have a single 256x256 tile at the root. const matrices = matrixSet.TileMatrix; if (!isDefined(matrices) || matrices.length < 1) { @@ -231,6 +252,8 @@ const getUsableTileMatrixSets = (matrixSets: TileMatrixSet[]) => { const levelZeroMatrix = matrices[0]; + console.log("levelZeroMatrix: ", levelZeroMatrix); + if (!isDefined(levelZeroMatrix.TopLeftCorner)) { continue; } @@ -241,12 +264,14 @@ const getUsableTileMatrixSets = (matrixSets: TileMatrixSet[]) => { const rectangleInMeters = standardTilingScheme.rectangleToNativeRectangle( standardTilingScheme.rectangle, ); + console.log("rectangleInMeters: ", rectangleInMeters); if ( Math.abs(startX - rectangleInMeters.west) > 1 || Math.abs(startY - rectangleInMeters.north) > 1 ) { continue; } + console.log("made it here too!!!"); if (isDefined(matrixSet.TileMatrix) && matrixSet.TileMatrix.length > 0) { const ids = matrixSet.TileMatrix.map(function (item: { Identifier: any }) { @@ -266,6 +291,7 @@ const getUsableTileMatrixSets = (matrixSets: TileMatrixSet[]) => { const getStyle = (capabilitiesLayer: WmtsLayer): string | undefined => { const availStyles = getAvailStyles(capabilitiesLayer); + console.log("availStyle: ", availStyles); const layerAvailableStyles = availStyles.find( candidate => candidate.layerName === capabilitiesLayer?.Identifier, )?.styles; @@ -356,7 +382,7 @@ type CapabilitiesStyle = { }; type CapabilitiesJson = { - readonly Version: string; + readonly version: string; readonly Contents?: Contents; readonly ServiceIdentification?: ServiceIdentification; readonly ServiceProvider?: ServiceProvider; @@ -364,6 +390,16 @@ type CapabilitiesJson = { readonly ServiceMetadataURL?: OnlineResource; }; +type Xml = { + version: string; + encoding: string; +}; + +type Capabilities = { + "?xml": Xml; + Capabilities: CapabilitiesJson; +}; + type OperationsMetadata = { readonly Operation: Operation; }; diff --git a/src/core/engines/Cesium/Feature/index.tsx b/src/core/engines/Cesium/Feature/index.tsx index 057117768..1faef7f6b 100644 --- a/src/core/engines/Cesium/Feature/index.tsx +++ b/src/core/engines/Cesium/Feature/index.tsx @@ -47,6 +47,7 @@ const displayConfig: Record = { gtfs: "auto", georss: [], gml: [], + wmts: ["raster"], }; // Some layer that is delegated data is not computed when layer is updated. diff --git a/src/core/mantle/data/index.test.ts b/src/core/mantle/data/index.test.ts new file mode 100644 index 000000000..a0ba3a770 --- /dev/null +++ b/src/core/mantle/data/index.test.ts @@ -0,0 +1,12 @@ +import { test } from "vitest"; + +import { fetchData } from "."; + +test("with header", async () => { + const features = await fetchData({ + type: "georss", + url: "https://dl.dropboxusercontent.com/s/gmna8hrscyn7s0q/combineGeometry.xml?dl=0", + }); + + console.log("features: ", features); +}, 10000);