UNPKG

terriajs

Version:

Geospatial data visualization platform.

255 lines (223 loc) 8.08 kB
import { action, runInAction } from "mobx"; import { GeomType, PolygonSymbolizer, Feature as ProtomapsFeature } from "protomaps-leaflet"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import TimeInterval from "terriajs-cesium/Source/Core/TimeInterval"; import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo"; import ImageryProvider from "terriajs-cesium/Source/Scene/ImageryProvider"; import isDefined from "../Core/isDefined"; import { isJsonNumber } from "../Core/Json"; import ProtomapsImageryProvider from "../Map/ImageryProvider/ProtomapsImageryProvider"; import { TerriaFeatureData } from "../Models/Feature/FeatureData"; import TableStyle from "./TableStyle"; import { isConstantStyleMap } from "./TableStyleMap"; import isReadOnlyArray from "../Core/isReadOnlyArray"; export default function createRegionMappedImageryProvider( style: TableStyle, currentTime: JulianDate | undefined ): ImageryProvider | undefined { if (!style.isRegions()) { return undefined; } const regionColumn = style.regionColumn; const regionType = regionColumn.regionType; if (regionType === undefined) { return undefined; } const terria = style.tableModel.terria; const colorColumn = style.colorColumn; const valueFunction = colorColumn !== undefined ? colorColumn.valueFunctionForType : () => null; const colorMap = style.colorMap; const valuesAsRegions = regionColumn.valuesAsRegions; const outlineStyleMap = style.outlineStyleMap.styleMap; let currentTimeRows: number[] | undefined; // If time varying, get row indices which match if (currentTime && style.timeIntervals && style.moreThanOneTimeInterval) { currentTimeRows = style.timeIntervals.reduce<number[]>( (rows, timeInterval, index) => { if (timeInterval && TimeInterval.contains(timeInterval, currentTime!)) { rows.push(index); } return rows; }, [] ); } const getRowNumber = (_zoom: number, f?: ProtomapsFeature) => { const regionId = f?.props[regionType.uniqueIdProp]; return isJsonNumber(regionId) ? getImageryLayerFilteredRow( style, currentTimeRows, valuesAsRegions.regionIdToRowNumbersMap.get(regionId) ) : undefined; }; const showFeature = (zoom: number, f?: ProtomapsFeature) => isDefined(getRowNumber(zoom, f)); const getColorValue = (zoom: number, f?: ProtomapsFeature) => { const rowNumber = getRowNumber(zoom, f); return colorMap .mapValueToColor(isDefined(rowNumber) ? valueFunction(rowNumber) : null) .toCssColorString(); }; const getOutlineColorValue = (zoom: number, f?: ProtomapsFeature) => { const defaultOutlineColor = runInAction(() => terria.baseMapContrastColor); const rowNumber = getRowNumber(zoom, f); if (!isDefined(rowNumber)) return defaultOutlineColor; return ( (isConstantStyleMap(outlineStyleMap) ? outlineStyleMap.style.color : outlineStyleMap.mapRowIdToStyle(rowNumber ?? -1).color) ?? defaultOutlineColor ); }; const getOutlineWidthValue = (zoom: number, f?: ProtomapsFeature) => { const rowNumber = getRowNumber(zoom, f); if (!isDefined(rowNumber)) return 1; return ( (isConstantStyleMap(outlineStyleMap) ? outlineStyleMap.style.width : outlineStyleMap.mapRowIdToStyle(rowNumber ?? -1).width) ?? 1 ); }; return new ProtomapsImageryProvider({ terria, // Use the URL as the id, this is needed for backward compatibility with MapboxImageryProvider, for when picking features (as it uses the URL as the id) id: regionType.server, data: regionType.server, minimumZoom: regionType.serverMinZoom, maximumNativeZoom: regionType.serverMaxNativeZoom, maximumZoom: regionType.serverMaxZoom, idProperty: regionType.uniqueIdProp, paintRules: [ // Polygon features { dataLayer: regionType.layerName, symbolizer: new PolygonSymbolizer({ fill: getColorValue, stroke: getOutlineColorValue, width: getOutlineWidthValue }), minzoom: 0, maxzoom: Infinity, filter: (zoom, feature) => { return ( feature?.geomType === GeomType.Polygon && showFeature(zoom, feature) ); } } ], labelRules: [], processPickedFeatures: async (features) => features .map((feature) => getImageryLayerFeatureInfo(style, feature, currentTimeRows) ) .filter(isDefined) }); } /** * Filters row numbers by time (if applicable) - for a given region mapped ImageryLayer */ const getImageryLayerFilteredRow = action( ( style: TableStyle, currentTimeRows: number[] | undefined, rowNumbers: number | readonly number[] | undefined ): number | undefined => { if (!isDefined(rowNumbers)) return; if (!isDefined(currentTimeRows)) { return isReadOnlyArray(rowNumbers) ? rowNumbers[0] : rowNumbers; } if ( typeof rowNumbers === "number" && currentTimeRows.includes(rowNumbers) ) { return rowNumbers; } else if (isReadOnlyArray(rowNumbers)) { const matchingTimeRows: number[] = rowNumbers.filter((row) => currentTimeRows!.includes(row) ); if (matchingTimeRows.length <= 1) { return matchingTimeRows[0]; } // In a time-varying dataset, intervals may // overlap at their endpoints (i.e. the end of one interval is the start of the next). // In that case, we want the later interval to apply. return matchingTimeRows.reduce((latestRow, currentRow) => { const currentInterval = style.timeIntervals?.[currentRow]?.stop; const latestInterval = style.timeIntervals?.[latestRow]?.stop; if ( currentInterval && latestInterval && JulianDate.lessThan(latestInterval, currentInterval) ) { return currentRow; } return latestRow; }, matchingTimeRows[0]); } } ); /** * Get ImageryLayerFeatureInfo for a given ImageryLayer input and feature. */ const getImageryLayerFeatureInfo = ( style: TableStyle, feature: ImageryLayerFeatureInfo, currentTimeRows: number[] | undefined ) => { if (isDefined(style.regionColumn?.regionType?.uniqueIdProp)) { const regionType = style.regionColumn!.regionType!; const regionRows = style.regionColumn!.valuesAsRegions.regionIdToRowNumbersMap.get( feature.properties[regionType.uniqueIdProp] ) ?? []; const rowId = getImageryLayerFilteredRow( style, currentTimeRows, regionRows ); if (!isDefined(rowId)) return; // eslint-disable-next-line @typescript-eslint/no-unused-expressions style.tableModel.tableColumns; const rowObject = style.tableModel.tableColumns.reduce<{ [key: string]: string | number | null; }>((obj, column) => { obj[column.name] = column.valueFunctionForType(rowId); return obj; }, {}); // Preserve values from d and insert feature properties after entries from d const featureData = Object.assign( {}, rowObject, feature.properties, rowObject ); if (isDefined(regionType.nameProp)) { feature.name = featureData[regionType.nameProp] as string; } featureData.id = feature.properties[regionType.uniqueIdProp]; feature.properties = featureData; const terriaFeatureData: TerriaFeatureData = { rowIds: isJsonNumber(regionRows) ? [regionRows] : [...regionRows], type: "terriaFeatureData" }; feature.data = terriaFeatureData; feature.configureDescriptionFromProperties(featureData); feature.configureNameFromProperties(featureData); // Set name of feature to region name if it exists if ( isDefined(regionType.nameProp) && typeof featureData[regionType.nameProp] === "string" ) feature.name = featureData[regionType.nameProp]; return feature; } return undefined; };