terriajs
Version:
Geospatial data visualization platform.
255 lines (223 loc) • 8.08 kB
text/typescript
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;
};