diff --git a/web/src/components/SimulationsPanel.vue b/web/src/components/SimulationsPanel.vue
index 6102d06d..d96fef1d 100644
--- a/web/src/components/SimulationsPanel.vue
+++ b/web/src/components/SimulationsPanel.vue
@@ -1,6 +1,12 @@
+
+
+
+
+
+ mdi-layers
+
+
+
+
+ Active Layers
+
+
+ mdi-playlist-remove
+
+
+
+ No layers active.
+
+
+
+
+
+ mdi-drag-horizontal-variant
+
+
+
+ {{ getLayerName(element) }}
+
+
+
+
+ mdi-cog
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/map/MapTooltip.vue b/web/src/components/map/MapTooltip.vue
new file mode 100644
index 00000000..ec42f07b
--- /dev/null
+++ b/web/src/components/map/MapTooltip.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+ ID: {{ selectedDataSource.derivedRegion.id }}
+
+ Name: {{ selectedDataSource.derivedRegion.name }}
+
+
+ Source Region IDs:
+ {{ selectedDataSource.derivedRegion.source_regions }}
+
+
+ Creation Operation:
+ {{ selectedDataSource.derivedRegion.source_operation }}
+
+
+
+
+ ID: {{ selectedRegionID }}
+ Name: {{ selectedFeature.get("name") }}
+
+ Zoom To Region
+
+
+
+
+
+
+
+
+ {{
+ regionGroupingType === "intersection"
+ ? "mdi-vector-intersection"
+ : "mdi-vector-union"
+ }}
+
+
+ Ungroup Region
+
+
+
+
+
+
+
+
+ {{
+ regionGroupingType === "intersection"
+ ? "mdi-vector-intersection"
+ : "mdi-vector-union"
+ }}
+
+
+ Add region to grouping
+
+
+
+
+
+
+
+ Begin region intersection
+
+
+
+
+ Begin region union
+
+
+
+
+
+
+
+ {{ k }}: {{ v }}
+
+
+
+ Reactivate Node
+
+
+ Deactivate Node
+
+
+
+
diff --git a/web/src/components/map/OpenLayersMap.vue b/web/src/components/map/OpenLayersMap.vue
new file mode 100644
index 00000000..77d84dcb
--- /dev/null
+++ b/web/src/components/map/OpenLayersMap.vue
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/map/RegionGrouping.vue b/web/src/components/map/RegionGrouping.vue
new file mode 100644
index 00000000..67f2687a
--- /dev/null
+++ b/web/src/components/map/RegionGrouping.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+ mdi-vector-{{ regionGroupingType }}
+ Performing {{ regionGroupingType }} Grouping
+
+
+ Grouping {{ selectedRegions.length }} Regions
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Save
+
+
+
+
+
diff --git a/web/src/data.ts b/web/src/data.ts
new file mode 100644
index 00000000..8c332df1
--- /dev/null
+++ b/web/src/data.ts
@@ -0,0 +1,96 @@
+import { Dataset, DerivedRegion } from "@/types";
+import { currentMapDataSource, activeMapLayerIds } from "@/store";
+import { getUid } from "ol/util";
+import {
+ addDataSourceLayerToMap,
+ getMapLayerFromDataSource,
+ updateVisibleLayers,
+} from "@/layers";
+
+export interface MapDataSourceArgs {
+ dataset?: Dataset;
+ derivedRegion?: DerivedRegion;
+}
+
+const UnexpectedMapDataSourceError = new Error(
+ "Unexpected map data source type"
+);
+
+/** Return an ID that is unique across data source types */
+export function getDatasetUid(id: number) {
+ return `dataset-${id}`;
+}
+
+/** Return an ID that is unique across data source types */
+export function getDerivedRegionUid(id: number) {
+ return `dr-${id}`;
+}
+
+// A unified representation of any data source (datasets, derived regions, etc.)
+export class MapDataSource {
+ dataset?: Dataset;
+ derivedRegion?: DerivedRegion;
+
+ constructor(args: MapDataSourceArgs) {
+ this.dataset = args.dataset;
+ this.derivedRegion = args.derivedRegion;
+ }
+
+ get uid() {
+ if (this.dataset) {
+ return getDatasetUid(this.dataset.id);
+ }
+ if (this.derivedRegion) {
+ return getDerivedRegionUid(this.derivedRegion.id);
+ }
+
+ throw UnexpectedMapDataSourceError;
+ }
+
+ get name() {
+ const name = this.dataset?.name || this.derivedRegion?.name;
+ if (name === undefined) {
+ throw UnexpectedMapDataSourceError;
+ }
+
+ return name;
+ }
+}
+
+export function addDataSourceToMap(dataSource: MapDataSource) {
+ // Check if layer with this dataset already exists
+ const existingLayer = getMapLayerFromDataSource(dataSource);
+
+ // Get either existing layer or create a new one
+ const layer = existingLayer || addDataSourceLayerToMap(dataSource);
+ if (layer === undefined) {
+ throw new Error("No layer returned when adding data source to map");
+ }
+
+ // Put new dataset at front of list, so it shows up above any existing layers
+ activeMapLayerIds.value = [getUid(layer), ...activeMapLayerIds.value];
+
+ // Re-order layers
+ updateVisibleLayers();
+}
+
+export function hideDataSourceFromMap(dataSource: MapDataSource) {
+ const dataSourceId = dataSource.uid;
+
+ // Filter out dataset layer from active map layers
+ const layer = getMapLayerFromDataSource(dataSource);
+ if (layer === undefined) {
+ throw new Error(`Couldn't find layer for data source ${dataSourceId}`);
+ }
+ activeMapLayerIds.value = activeMapLayerIds.value.filter(
+ (layerId) => layerId !== getUid(layer)
+ );
+
+ // Re-order layers
+ updateVisibleLayers();
+
+ // If current data source was the de-selected dataset, un-set it
+ if (currentMapDataSource.value?.uid === dataSourceId) {
+ currentMapDataSource.value = undefined;
+ }
+}
diff --git a/web/src/layers.ts b/web/src/layers.ts
new file mode 100644
index 00000000..9cac1d8b
--- /dev/null
+++ b/web/src/layers.ts
@@ -0,0 +1,280 @@
+import { Layer } from "ol/layer";
+import { getUid } from "ol/util";
+import VectorLayer from "ol/layer/Vector";
+import TileLayer from "ol/layer/Tile";
+import VectorSource from "ol/source/Vector";
+import GeoJSON from "ol/format/GeoJSON.js";
+import XYZSource from "ol/source/XYZ.js";
+import VectorTileLayer from "ol/layer/VectorTile";
+import VectorTileSource from "ol/source/VectorTile.js";
+import Feature from "ol/Feature";
+import { LineString, Point } from "ol/geom";
+import { fromLonLat } from "ol/proj";
+
+import {
+ activeMapLayerIds,
+ networkVis,
+ showMapBaseLayer,
+ availableDataSourcesTable,
+ getMap,
+} from "@/store";
+import { createStyle, cacheRasterData, getNetworkFeatureStyle } from "@/utils";
+import { baseURL } from "@/api/auth";
+import type { Dataset, NetworkNode } from "@/types";
+import type { MapDataSource } from "@/data";
+import BaseLayer from "ol/layer/Base";
+
+export function getMapLayerById(layerId: string): BaseLayer | undefined {
+ return getMap()
+ .getLayers()
+ .getArray()
+ .find((layer) => getUid(layer) === layerId);
+}
+
+export function getDataSourceFromLayerId(
+ layerId: string
+): MapDataSource | undefined {
+ const layer = getMapLayerById(layerId);
+ const dsId: string | undefined = layer?.get("dataSourceId");
+ if (dsId === undefined) {
+ throw new Error(`Data Source ID not present on layer ${layerId}`);
+ }
+
+ return availableDataSourcesTable.value.get(dsId);
+}
+
+export function getMapLayerFromDataSource(
+ source: MapDataSource
+): BaseLayer | undefined {
+ return getMap()
+ .getLayers()
+ .getArray()
+ .find((layer) => layer.get("dataSourceId") === source.uid);
+}
+
+/** Returns if a layer should be enabled based on showMapBaseLayer, activeMapLayerIds, and networkVis */
+export function getLayerEnabled(layer: BaseLayer) {
+ // Check if layer is map base layer
+ if (layer.getProperties().baseLayer) {
+ return showMapBaseLayer.value;
+ }
+
+ // Check if layer is enabled
+ const layerId = getUid(layer);
+ let layerEnabled = activeMapLayerIds.value.includes(layerId);
+
+ // Ensure that if networkVis is enabled, only the network layer is shown (not the original layer)
+ const layerDatasetId = getDataSourceFromLayerId(layerId)?.dataset?.id;
+ if (networkVis.value && networkVis.value.id === layerDatasetId) {
+ layerEnabled = layerEnabled && layer.get("network");
+ }
+
+ return layerEnabled;
+}
+
+/**
+ * Shows/hides layers based on the getLayerEnabled function.
+ *
+ * Note: This only modifies layer visibility. It does not actually enable or disable any map layers directly.
+ * */
+export function updateVisibleLayers() {
+ const layerState = {
+ shown: [] as BaseLayer[],
+ hidden: [] as BaseLayer[],
+ };
+ const allLayers = getMap().getLayers()?.getArray();
+ if (!allLayers) {
+ return layerState;
+ }
+
+ allLayers.forEach((layer) => {
+ const layerEnabled = getLayerEnabled(layer);
+ if (!layerEnabled) {
+ layer.setVisible(false);
+ layerState.hidden.push(layer);
+ return;
+ }
+
+ // Set layer visible and z-index
+ layer.setVisible(true);
+ layerState.shown.push(layer);
+ const layerId = getUid(layer);
+ const layerIndex = activeMapLayerIds.value.findIndex(
+ (id) => id === layerId
+ );
+ layer.setZIndex(
+ layerIndex > -1 ? activeMapLayerIds.value.length - layerIndex : 0
+ );
+ });
+
+ return layerState;
+}
+
+function randomColor() {
+ return (
+ "#" +
+ Math.floor(Math.random() * 16777215)
+ .toString(16)
+ .padStart(6, "0")
+ .slice(0, 6)
+ );
+}
+
+function getObjectProperty(
+ object: Record
,
+ selector: string
+): unknown {
+ return selector
+ .split(".")
+ .filter((x) => x !== "")
+ .reduce((acc, cur) => acc && (acc[cur] as Record), object);
+}
+
+type VectorLayerProps = { colors?: string };
+export function createVectorLayer(
+ url: string,
+ props?: VectorLayerProps
+): Layer {
+ const defaultColors = `${randomColor()},#ffffff`;
+ return new VectorLayer({
+ style: (feature) =>
+ createStyle({
+ type: feature.getGeometry()?.getType(),
+ colors: props?.colors
+ ? getObjectProperty(feature.getProperties(), props.colors)
+ : defaultColors,
+ }),
+ source: new VectorSource({
+ url,
+ format: new GeoJSON(),
+ }),
+ });
+}
+
+export function createVectorTileLayer(dataset: Dataset): Layer {
+ return new VectorTileLayer({
+ source: new VectorTileSource({
+ format: new GeoJSON(),
+ url: `${baseURL}datasets/${dataset.id}/vector-tiles/{z}/{x}/{y}/`,
+ }),
+ style: (feature) =>
+ createStyle({
+ type: feature.getGeometry()?.getType(),
+ colors: feature.get("colors"),
+ }),
+ opacity: dataset.style?.opacity || 1,
+ });
+}
+
+export function createRasterLayer(dataset: Dataset): Layer {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const tileParams: Record = {
+ projection: "EPSG:3857",
+ band: 1,
+ palette: dataset.style?.colormap || "terrain",
+ };
+ if (dataset.style?.colormap_range?.length === 2) {
+ tileParams.min = dataset.style.colormap_range[0];
+ tileParams.max = dataset.style.colormap_range[1];
+ }
+ if (dataset.style?.options?.transparency_threshold !== undefined) {
+ tileParams.nodata = dataset.style.options.transparency_threshold;
+ }
+ const tileParamString = Object.keys(tileParams)
+ .map((key) => key + "=" + tileParams[key])
+ .join("&");
+
+ cacheRasterData(dataset.id);
+ return new TileLayer({
+ source: new XYZSource({
+ url: `${baseURL}datasets/${dataset.id}/tiles/{z}/{x}/{y}.png/?${tileParamString}`,
+ }),
+ opacity: dataset.style?.opacity || 1,
+ });
+}
+
+// TODO: Roll into addDatasetLayerToMap
+export function addNetworkLayerToMap(dataset: Dataset, nodes: NetworkNode[]) {
+ const source = new VectorSource();
+ const features: Feature[] = [];
+ const visitedNodes: number[] = [];
+ nodes.forEach((node) => {
+ // Add point for each node
+ features.push(
+ new Feature(
+ Object.assign(node.properties, {
+ name: node.name,
+ id: node.id,
+ node: true,
+ geometry: new Point(fromLonLat(node.location.slice().reverse())),
+ })
+ )
+ );
+
+ // Add edges between adjacent nodes
+ node.adjacent_nodes.forEach((adjId) => {
+ if (!visitedNodes.includes(adjId)) {
+ const adjNode = nodes.find((n) => n.id === adjId);
+ if (adjNode === undefined) {
+ return;
+ }
+
+ features.push(
+ new Feature({
+ connects: [node.id, adjId],
+ edge: true,
+ geometry: new LineString([
+ fromLonLat(node.location.slice().reverse()),
+ fromLonLat(adjNode.location.slice().reverse()),
+ ]),
+ })
+ );
+ }
+ });
+ visitedNodes.push(node.id);
+ });
+ source.addFeatures(features);
+
+ const layer = new VectorLayer({
+ properties: {
+ network: true,
+ },
+ zIndex: 99,
+ style: getNetworkFeatureStyle(),
+ source,
+ });
+ getMap().addLayer(layer);
+ return layer;
+}
+
+export function addDataSourceLayerToMap(dataSource: MapDataSource) {
+ let layer: Layer | undefined;
+ const { dataset, derivedRegion } = dataSource;
+ if (derivedRegion) {
+ layer = createVectorLayer(
+ `${baseURL}derived_regions/${derivedRegion.id}/as_feature/`
+ );
+ } else if (dataset) {
+ if (dataset.category === "region") {
+ layer = createVectorLayer(
+ `${baseURL}datasets/${dataSource.dataset?.id}/regions`,
+ { colors: "properties.colors" }
+ );
+ } else if (dataset.vector_tiles_file) {
+ layer = createVectorTileLayer(dataset);
+ } else if (dataset.geodata_file) {
+ layer = createVectorLayer(dataset.geodata_file, { colors: "colors" });
+ } else if (dataset.raster_file) {
+ layer = createRasterLayer(dataset);
+ }
+ }
+
+ if (layer === undefined) {
+ throw new Error("Could not add data source to map");
+ }
+
+ // Add this to link layers to data sources
+ layer.setProperties({ dataSourceId: dataSource.uid });
+ getMap().addLayer(layer);
+ return layer;
+}
diff --git a/web/src/store.ts b/web/src/store.ts
index bf5fc192..38d8c2c3 100644
--- a/web/src/store.ts
+++ b/web/src/store.ts
@@ -3,20 +3,89 @@ import TileLayer from "ol/layer/Tile.js";
import OSM from "ol/source/OSM.js";
import * as olProj from "ol/proj";
-import { ref, watch } from "vue";
-import { City, Dataset } from "./types.js";
+import { computed, reactive, ref, watch } from "vue";
+import { City, Dataset, DerivedRegion, Region } from "./types.js";
import { getCities, getDataset } from "@/api/rest";
+import { MapDataSource } from "@/data";
+import { Map as olMap, getUid, Feature } from "ol";
export const loading = ref(false);
export const currentError = ref();
export const cities = ref([]);
export const currentCity = ref();
-export const currentDataset = ref();
-export const selectedDatasetIds = ref([]);
-export const map = ref();
-export const mapLayers = ref();
+export const map = ref();
+export function getMap() {
+ if (map.value === undefined) {
+ throw new Error("Map not yet initialized!");
+ }
+ return map.value;
+}
+
+export const showMapTooltip = ref(false);
+export const selectedFeature = ref();
+export const selectedDataSource = ref();
+
+// Represents the number of layers active and their ordering
+// This is the sole source of truth regarding visible layers
+export const activeMapLayerIds = ref([]);
+
+// All data sources combined into one list
+export const availableMapDataSources = computed(() => {
+ const datasets = currentCity.value?.datasets || [];
+ return [
+ ...availableDerivedRegions.value.map(
+ (derivedRegion) => new MapDataSource({ derivedRegion })
+ ),
+ ...datasets.map((dataset) => new MapDataSource({ dataset })),
+ ];
+});
+
+/** Maps data source IDs to the sources themselves */
+export const availableDataSourcesTable = computed(() => {
+ const dsMap = new Map();
+ availableMapDataSources.value.forEach((ds) => {
+ dsMap.set(ds.uid, ds);
+ });
+
+ return dsMap;
+});
+
+// The currently selected data source (if any)
+export const currentMapDataSource = ref();
+
+/**
+ * Keeps track of which data sources are being actively shown
+ * Maps data source ID to the source itself
+ */
+export const activeDataSources = computed(() => {
+ const dsmap = new Map();
+ if (map.value === undefined) {
+ return dsmap;
+ }
+
+ // Get list of active map layers
+ const activeLayersIdSet = new Set(activeMapLayerIds.value);
+ const allMapLayers = getMap().getLayers().getArray();
+
+ // Get all data source IDs which have an entry in activeMapLayerIds
+ const activeDataSourceIds = new Set(
+ allMapLayers
+ .filter((layer) => activeLayersIdSet.has(getUid(layer)))
+ .map((layer) => layer.get("dataSourceId"))
+ );
+
+ // Filter available data sources to this list
+ availableMapDataSources.value
+ .filter((ds) => activeDataSourceIds.has(ds.uid))
+ .forEach((ds) => {
+ dsmap.set(ds.uid, ds);
+ });
+
+ return dsmap;
+});
+
export const showMapBaseLayer = ref(true);
export const rasterTooltip = ref();
@@ -25,8 +94,24 @@ export const availableCharts = ref([]);
export const activeSimulation = ref();
export const availableSimulations = ref([]);
-export const networkVis = ref();
-export const deactivatedNodes = ref([]);
+// Regions
+export const regionGroupingActive = ref(false);
+export const regionGroupingType = ref<"intersection" | "union" | null>(null);
+export const selectedRegions = ref([]);
+export function cancelRegionGrouping() {
+ selectedRegions.value = [];
+ regionGroupingActive.value = false;
+ regionGroupingType.value = null;
+
+ showMapTooltip.value = false;
+}
+
+export const availableDerivedRegions = ref([]);
+export const selectedDerivedRegionIds = reactive(new Set());
+
+// Network
+export const networkVis = ref();
+export const deactivatedNodes = ref([]);
export const currentNetworkGCC = ref();
export function loadCities() {
@@ -47,21 +132,24 @@ export function loadCities() {
}
export function clearMap() {
- if (!currentCity.value || !map.value) {
+ if (!currentCity.value) {
return;
}
- mapLayers.value = [];
- map.value.setView(
+
+ getMap().setView(
new View({
center: olProj.fromLonLat(currentCity.value.center),
zoom: currentCity.value.default_zoom,
})
);
- map.value.addLayer(
+ getMap().setLayers([
new TileLayer({
source: new OSM(),
- })
- );
+ properties: {
+ baseLayer: true,
+ },
+ }),
+ ]);
}
watch(currentCity, clearMap);
@@ -92,4 +180,4 @@ export function currentDatasetChanged() {
rasterTooltip.value = undefined;
}
-watch(currentDataset, currentDatasetChanged);
+watch(currentMapDataSource, currentDatasetChanged);
diff --git a/web/src/types.ts b/web/src/types.ts
index 1707a756..d65ee87f 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -10,8 +10,27 @@ export interface Dataset {
raster_file: string;
created: string;
modified: string;
- style: object;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ style: { [key: string]: any };
processing: boolean;
+ network: boolean;
+}
+
+export interface Region {
+ id: number;
+ name: string;
+ city: number;
+ dataset: number;
+ properties: { [key: string]: unknown };
+}
+
+export interface DerivedRegion {
+ id: number;
+ name: string;
+ city: number;
+ properties: { [key: string]: unknown };
+ source_operation: "UNION" | "INTERSECTION";
+ source_regions: number[];
}
export interface City {
diff --git a/web/src/utils.js b/web/src/utils.js
index 0f32e2aa..51e46781 100644
--- a/web/src/utils.js
+++ b/web/src/utils.js
@@ -1,22 +1,9 @@
-import TileLayer from "ol/layer/Tile.js";
-import VectorLayer from "ol/layer/Vector";
-import VectorTileLayer from "ol/layer/VectorTile.js";
-import XYZSource from "ol/source/XYZ.js";
-import VectorSource from "ol/source/Vector";
-import VectorTileSource from "ol/source/VectorTile.js";
-import GeoJSON from "ol/format/GeoJSON.js";
import { Fill, Stroke, Circle, Style } from "ol/style.js";
-import { Feature } from "ol";
-import { LineString, Point } from "ol/geom";
-import { fromLonLat } from "ol/proj";
-import { baseURL } from "@/api/auth";
import { getNetworkGCC, getCityCharts, getRasterData } from "@/api/rest";
import {
- map,
- showMapBaseLayer,
+ getMap,
currentCity,
- selectedDatasetIds,
rasterTooltip,
networkVis,
deactivatedNodes,
@@ -49,48 +36,6 @@ export const rasterColormaps = [
var rasterTooltipDataCache = {};
-export function updateVisibleLayers() {
- const layerState = {
- shown: [],
- hidden: [],
- };
- const allLayers = map.value?.getLayers()?.getArray();
- if (allLayers) {
- allLayers.forEach((layer) => {
- const layerDatasetId = layer.getProperties().datasetId;
- let layerEnabled = selectedDatasetIds.value.includes(layerDatasetId);
-
- if (!layerDatasetId) {
- // map base layer does not have dataset id
- layerEnabled = showMapBaseLayer.value;
- }
-
- if (networkVis.value) {
- if (layerDatasetId === networkVis.value.id) {
- layerEnabled = layerEnabled && layer.getProperties().network;
- }
- } else if (layer.getProperties().network) {
- layerEnabled = false;
- }
-
- if (layerEnabled) {
- layer.setVisible(true);
- layerState.shown.push(layer);
- const layerIndex = selectedDatasetIds.value.findIndex(
- (id) => id === layerDatasetId
- );
- layer.setZIndex(
- layerDatasetId ? selectedDatasetIds.value.length - layerIndex : 0
- );
- } else {
- layer.setVisible(false);
- layerState.hidden.push(layer);
- }
- });
- }
- return layerState;
-}
-
export function cacheRasterData(datasetId) {
if (!rasterTooltipDataCache[datasetId]) {
rasterTooltipDataCache[datasetId] = {};
@@ -100,7 +45,7 @@ export function cacheRasterData(datasetId) {
}
}
-function createStyle(args) {
+export function createStyle(args) {
let colors = ["#00000022"];
if (args.colors) {
colors = args.colors.split(",");
@@ -145,119 +90,7 @@ function createStyle(args) {
}
}
-export function addDatasetLayerToMap(dataset, zIndex) {
- if (dataset.processing) {
- return;
- }
- let layer = undefined;
-
- // Add raster data
- if (dataset.raster_file) {
- const tileParams = {
- projection: "EPSG:3857",
- band: 1,
- palette: dataset.style?.colormap || "terrain",
- };
- if (
- dataset.style?.colormap_range !== undefined &&
- dataset.style?.colormap_range.length === 2
- ) {
- tileParams.min = dataset.style.colormap_range[0];
- tileParams.max = dataset.style.colormap_range[1];
- }
- if (dataset.style?.options?.transparency_threshold !== undefined) {
- tileParams.nodata = dataset.style.options.transparency_threshold;
- }
- const tileParamString = Object.keys(tileParams)
- .map((key) => key + "=" + tileParams[key])
- .join("&");
-
- layer = new TileLayer({
- properties: {
- datasetId: dataset.id,
- dataset,
- },
- source: new XYZSource({
- url: `${baseURL}datasets/${dataset.id}/tiles/{z}/{x}/{y}.png/?${tileParamString}`,
- }),
- opacity: dataset.style?.opacity || 1,
- zIndex,
- });
- cacheRasterData(dataset.id);
- }
-
- // Use tiled GeoJSON if it exists
- else if (dataset.vector_tiles_file) {
- layer = new VectorTileLayer({
- source: new VectorTileSource({
- format: new GeoJSON(),
- url: `${baseURL}datasets/${dataset.id}/vector-tiles/{z}/{x}/{y}/`,
- }),
- properties: {
- datasetId: dataset.id,
- dataset,
- },
- style: (feature) =>
- createStyle({
- type: feature.getGeometry().getType(),
- colors: feature.get("colors"),
- }),
- opacity: dataset.style?.opacity || 1,
- zIndex,
- });
-
- // Use VectorLayer if dataset category is "region"
- if (dataset.category === "region") {
- layer = new VectorLayer({
- properties: {
- datasetId: dataset.id,
- dataset,
- },
- zIndex,
- style: (feature) =>
- createStyle({
- type: feature.getGeometry().getType(),
- colors: feature.get("properties").colors,
- }),
- source: new VectorSource({
- format: new GeoJSON(),
- url: `${baseURL}datasets/${dataset.id}/regions`,
- }),
- });
- }
- // Add to map
- map.value.addLayer(layer);
- }
-
- // Default to vector layer
- else {
- let dataURL = dataset.geodata_file;
- if (dataset.category === "region") {
- dataURL = `${baseURL}datasets/${dataset.id}/regions`;
- }
- layer = new VectorLayer({
- properties: {
- datasetId: dataset.id,
- dataset,
- },
- zIndex,
- style: (feature) =>
- createStyle({
- type: feature.getGeometry().getType(),
- colors: feature.getProperties().colors,
- }),
- source: new VectorSource({
- format: new GeoJSON(),
- url: dataURL,
- }),
- });
- }
-
- // Add to map
- map.value.addLayer(layer);
-}
-
-function getNetworkFeatureStyle(alpha = "ff", highlight = false) {
+export function getNetworkFeatureStyle(alpha = "ff", highlight = false) {
const fill = new Fill({
color: `#ffffff${alpha}`,
});
@@ -295,7 +128,7 @@ function getNetworkFeatureStyle(alpha = "ff", highlight = false) {
}
export function updateNetworkStyle() {
- map.value
+ getMap()
.getLayers()
.getArray()
.forEach((layer) => {
@@ -352,149 +185,6 @@ export function updateNetworkStyle() {
});
}
-export function addNetworkLayerToMap(dataset, nodes) {
- const source = new VectorSource();
- const features = [];
- const visitedNodes = [];
- nodes.forEach((node) => {
- features.push(
- new Feature(
- Object.assign(node.properties, {
- name: node.name,
- id: node.id,
- node: true,
- geometry: new Point(fromLonLat(node.location.toReversed())),
- })
- )
- );
- node.adjacent_nodes.forEach((adjId) => {
- if (!visitedNodes.includes(adjId)) {
- const adjNode = nodes.find((n) => n.id === adjId);
- features.push(
- new Feature({
- connects: [node.id, adjId],
- edge: true,
- geometry: new LineString([
- fromLonLat(node.location.toReversed()),
- fromLonLat(adjNode.location.toReversed()),
- ]),
- })
- );
- }
- });
- visitedNodes.push(node.id);
- });
- source.addFeatures(features);
-
- const layer = new VectorLayer({
- properties: {
- datasetId: dataset.id,
- dataset,
- network: true,
- },
- zIndex: 99,
- style: getNetworkFeatureStyle(),
- source,
- });
- map.value.addLayer(layer);
-}
-
-function renderRegionTooltip(tooltipDiv, feature) {
- tooltipDiv.innerHTML = `
- ID: ${feature.get("pk")}
- Name: ${feature.get("name")}
- `;
-
- // Create button
- const cropButton = document.createElement("BUTTON");
- cropButton.classList = "v-btn v-btn--variant-outlined pa-2";
- cropButton.appendChild(document.createTextNode("Zoom to Region"));
- cropButton.onclick = () => {
- // Set map zoom to match bounding box of region
- map.value.getView().fit(feature.getGeometry(), {
- size: map.value.getSize(),
- duration: 300,
- });
- };
-
- // Add button to tooltip
- tooltipDiv.appendChild(cropButton);
-}
-
-function renderNetworkTooltip(tooltipDiv, feature) {
- // Add data
- const properties = Object.fromEntries(
- Object.entries(feature.values_).filter(([k, v]) => k && v)
- );
- ["colors", "geometry", "type", "id", "node", "edge"].forEach(
- (prop) => delete properties[prop]
- );
- let prettyString = JSON.stringify(properties)
- .replaceAll('"', "")
- .replaceAll("{", "")
- .replaceAll("}", "")
- .replaceAll(",", "
");
- prettyString += "
";
- tooltipDiv.innerHTML = prettyString;
-
- // Add activation button
- const nodeId = feature.get("id");
- const deactivateButton = document.createElement("button");
- if (deactivatedNodes.value.includes(nodeId)) {
- deactivateButton.innerHTML = "Reactivate Node";
- } else {
- deactivateButton.innerHTML = "Deactivate Node";
- }
- deactivateButton.onclick = function () {
- toggleNodeActive(nodeId, deactivateButton);
- };
- deactivateButton.classList = "v-btn v-btn--variant-outlined pa-2";
- tooltipDiv.appendChild(deactivateButton);
-}
-
-export function displayFeatureTooltip(evt, tooltip, overlay) {
- if (rasterTooltip.value) return;
-
- // Clear tooltip values in event of no feature clicked
- tooltip.value.innerHTML = "";
- tooltip.value.style.display = "";
-
- // Check if any features are clicked, exit if not
- let res = map.value.forEachFeatureAtPixel(evt.pixel, (feature, layer) => [
- feature,
- layer,
- ]);
- if (!res) {
- tooltip.value.style.display = "none";
- return;
- }
-
- // Get feature and layer, exit if dataset isn't provided through the layer
- const [feature, layer] = res;
- const dataset = layer.get("dataset");
- if (!dataset) {
- return;
- }
-
- // Create div in which tooltip contents will live
- const tooltipDiv = document.createElement("div");
-
- // Handle region dataset
- if (dataset.category === "region") {
- renderRegionTooltip(tooltipDiv, feature);
- // Handle network dataset
- } else if (networkVis.value && dataset.network) {
- renderNetworkTooltip(tooltipDiv, feature);
- } else {
- // No defined behavior, quit and render nothing
- return;
- }
-
- // Set tooltip contents and position
- tooltip.value.appendChild(tooltipDiv);
- overlay.setPosition(evt.coordinate);
-}
-
export function displayRasterTooltip(evt, tooltip, overlay) {
if (rasterTooltip.value) {
if (rasterTooltipDataCache[rasterTooltip.value]) {