From b93f62bc3270020a05d8a2a4c43623b837717df6 Mon Sep 17 00:00:00 2001 From: Joris Mancini <53527338+TheMaskedTurtle@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:44:48 +0100 Subject: [PATCH] Revert "Typescript migration (#123)" (#131) This reverts commit b2cedbf4a77a9a2bed60ebd2a50cc05334c9096e. Signed-off-by: Joris Mancini --- demo/src/App.tsx | 2 +- .../network/{geo-data.ts => geo-data.js} | 86 +- .../layers/{arrow-layer.ts => arrow-layer.js} | 163 ++-- ...{fork-line-layer.ts => fork-line-layer.js} | 47 +- ...l-path-layer.ts => parallel-path-layer.js} | 49 +- ...-layer-ext.ts => scatterplot-layer-ext.js} | 27 +- .../network/{line-layer.ts => line-layer.js} | 579 +++++------- .../{map-equipments.ts => map-equipments.js} | 103 +-- .../network/network-map.jsx | 752 ++++++++++++++++ .../network/network-map.tsx | 838 ------------------ ...ubstation-layer.ts => substation-layer.js} | 71 +- .../utils/equipment-types.ts | 123 +-- src/deckgl.d.ts | 88 +- 13 files changed, 1200 insertions(+), 1728 deletions(-) rename src/components/network-map-viewer/network/{geo-data.ts => geo-data.js} (80%) rename src/components/network-map-viewer/network/layers/{arrow-layer.ts => arrow-layer.js} (73%) rename src/components/network-map-viewer/network/layers/{fork-line-layer.ts => fork-line-layer.js} (73%) rename src/components/network-map-viewer/network/layers/{parallel-path-layer.ts => parallel-path-layer.js} (84%) rename src/components/network-map-viewer/network/layers/{scatterplot-layer-ext.ts => scatterplot-layer-ext.js} (61%) rename src/components/network-map-viewer/network/{line-layer.ts => line-layer.js} (65%) rename src/components/network-map-viewer/network/{map-equipments.ts => map-equipments.js} (73%) create mode 100644 src/components/network-map-viewer/network/network-map.jsx delete mode 100644 src/components/network-map-viewer/network/network-map.tsx rename src/components/network-map-viewer/network/{substation-layer.ts => substation-layer.js} (72%) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 3b200f36..b21d035c 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -8,6 +8,7 @@ import { useEffect, useRef } from 'react'; import { createTheme, StyledEngineProvider, ThemeProvider } from '@mui/material/styles'; import { GeoData, NetworkMap, NetworkMapRef } from '../../src'; +import { Equipment } from '../../src/components/network-map-viewer/network/map-equipments'; import { addNadToDemo, addSldToDemo } from './diagram-viewers/add-diagrams'; import DemoMapEquipments from './map-viewer/demo-map-equipments'; @@ -15,7 +16,6 @@ import sposdata from './map-viewer/data/spos.json'; import lposdata from './map-viewer/data/lpos.json'; import smapdata from './map-viewer/data/smap.json'; import lmapdata from './map-viewer/data/lmap.json'; -import { Equipment } from '../../src/components/network-map-viewer/utils/equipment-types'; export default function App() { const INITIAL_ZOOM = 9; diff --git a/src/components/network-map-viewer/network/geo-data.ts b/src/components/network-map-viewer/network/geo-data.js similarity index 80% rename from src/components/network-map-viewer/network/geo-data.ts rename to src/components/network-map-viewer/network/geo-data.js index 050479ff..821572bb 100644 --- a/src/components/network-map-viewer/network/geo-data.ts +++ b/src/components/network-map-viewer/network/geo-data.js @@ -8,49 +8,33 @@ import { computeDestinationPoint, getGreatCircleBearing, getRhumbLineBearing } from 'geolib'; import cheapRuler from 'cheap-ruler'; import { ArrowDirection } from './layers/arrow-layer'; -import { Line, LonLat } from '../utils/equipment-types'; -import { MapEquipments } from './map-equipments'; -export type Coordinate = { - lon: number; - lat: number; -}; - -export type SubstationPosition = { - id: string; - coordinate: Coordinate; -}; - -export type LinePosition = { - id: string; - coordinates: Coordinate[]; -}; - -const substationPositionByIdIndexer = (map: Map, substation: SubstationPosition) => { +const substationPositionByIdIndexer = (map, substation) => { map.set(substation.id, substation.coordinate); return map; }; -const linePositionByIdIndexer = (map: Map, line: LinePosition) => { +const linePositionByIdIndexer = (map, line) => { map.set(line.id, line.coordinates); return map; }; export class GeoData { - substationPositionsById = new Map(); - linePositionsById = new Map(); + substationPositionsById = new Map(); + + linePositionsById = new Map(); - constructor(substationPositionsById: Map, linePositionsById: Map) { + constructor(substationPositionsById, linePositionsById) { this.substationPositionsById = substationPositionsById; this.linePositionsById = linePositionsById; } - setSubstationPositions(positions: SubstationPosition[]) { + setSubstationPositions(positions) { // index positions by substation id this.substationPositionsById = positions.reduce(substationPositionByIdIndexer, new Map()); } - updateSubstationPositions(substationIdsToUpdate: string[], fetchedPositions: SubstationPosition[]) { + updateSubstationPositions(substationIdsToUpdate, fetchedPositions) { fetchedPositions.forEach((pos) => this.substationPositionsById.set(pos.id, pos.coordinate)); // If a substation position is requested but not present in the fetched results, we delete its position. // It allows to cancel the position of a substation when the server can't situate it anymore after a network modification (for example a line deletion). @@ -59,7 +43,7 @@ export class GeoData { .forEach((id) => this.substationPositionsById.delete(id)); } - getSubstationPosition(substationId: string): LonLat { + getSubstationPosition(substationId) { const position = this.substationPositionsById.get(substationId); if (!position) { console.warn(`Position not found for ${substationId}`); @@ -68,12 +52,12 @@ export class GeoData { return [position.lon, position.lat]; } - setLinePositions(positions: LinePosition[]) { + setLinePositions(positions) { // index positions by line id this.linePositionsById = positions.reduce(linePositionByIdIndexer, new Map()); } - updateLinePositions(lineIdsToUpdate: string[], fetchedPositions: LinePosition[]) { + updateLinePositions(lineIdsToUpdate, fetchedPositions) { fetchedPositions.forEach((pos) => { this.linePositionsById.set(pos.id, pos.coordinates); }); @@ -88,7 +72,7 @@ export class GeoData { /** * Get line positions always ordered from side 1 to side 2. */ - getLinePositions(network: MapEquipments, line: Line, detailed = true): LonLat[] { + getLinePositions(network, line, detailed = true) { const voltageLevel1 = network.getVoltageLevel(line.voltageLevelId1); if (!voltageLevel1) { throw new Error(`Voltage level side 1 '${line.voltageLevelId1}' not found`); @@ -117,7 +101,7 @@ export class GeoData { const linePositions = this.linePositionsById.get(line.id); // Is there any position for this line ? if (linePositions) { - const positions = new Array(linePositions.length); + const positions = new Array(linePositions.length); for (const [index, position] of linePositions.entries()) { positions[index] = [position.lon, position.lat]; @@ -130,9 +114,9 @@ export class GeoData { return [substationPosition1, substationPosition2]; } - getLineDistances(positions: LonLat[]) { + getLineDistances(positions) { if (positions !== null && positions.length > 1) { - const cumulativeDistanceArray = [0]; + let cumulativeDistanceArray = [0]; let cumulativeDistance = 0; let segmentDistance; let ruler; @@ -152,13 +136,13 @@ export class GeoData { * along with the remaining distance to travel on this segment to be at the exact wanted distance * (implemented using a binary search) */ - findSegment(positions: LonLat[], cumulativeDistances: number[], wantedDistance: number) { + findSegment(positions, cumulativeDistances, wantedDistance) { let lowerBound = 0; let upperBound = cumulativeDistances.length - 1; let middlePoint; while (lowerBound + 1 !== upperBound) { middlePoint = Math.floor((lowerBound + upperBound) / 2); - const middlePointDistance = cumulativeDistances[middlePoint]; + let middlePointDistance = cumulativeDistances[middlePoint]; if (middlePointDistance <= wantedDistance) { lowerBound = middlePoint; } else { @@ -167,21 +151,21 @@ export class GeoData { } return { idx: lowerBound, - segment: positions.slice(lowerBound, lowerBound + 2) as [LonLat, LonLat], + segment: positions.slice(lowerBound, lowerBound + 2), remainingDistance: wantedDistance - cumulativeDistances[lowerBound], }; } labelDisplayPosition( - positions: LonLat[], - cumulativeDistances: number[], - arrowPosition: number, - arrowDirection: ArrowDirection, - lineParallelIndex: number, - lineAngle: number, - proximityAngle: number, - distanceBetweenLines: number, - proximityFactor: number + positions, + cumulativeDistances, + arrowPosition, + arrowDirection, + lineParallelIndex, + lineAngle, + proximityAngle, + distanceBetweenLines, + proximityFactor ) { if (arrowPosition > 1 || arrowPosition < 0) { throw new Error('Proportional position value incorrect: ' + arrowPosition); @@ -193,7 +177,7 @@ export class GeoData { ) { return null; } - const lineDistance = cumulativeDistances[cumulativeDistances.length - 1]; + let lineDistance = cumulativeDistances[cumulativeDistances.length - 1]; let wantedDistance = lineDistance * arrowPosition; if (cumulativeDistances.length === 2) { @@ -203,7 +187,7 @@ export class GeoData { wantedDistance = wantedDistance - 2 * distanceBetweenLines * arrowPosition * proximityFactor; } - const goodSegment = this.findSegment(positions, cumulativeDistances, wantedDistance); + let goodSegment = this.findSegment(positions, cumulativeDistances, wantedDistance); // We don't have the exact same distance calculation as in the arrow shader, so take some margin: // we move the label a little bit on the flat side of the arrow so that at least it stays @@ -222,9 +206,9 @@ export class GeoData { default: throw new Error('impossible'); } - const remainingDistance = goodSegment.remainingDistance * multiplier; + let remainingDistance = goodSegment.remainingDistance * multiplier; - const angle = this.getMapAngle(goodSegment.segment[0], goodSegment.segment[1]); + let angle = this.getMapAngle(goodSegment.segment[0], goodSegment.segment[1]); const neededOffset = this.getLabelOffset(angle, 20, arrowDirection); const position = { @@ -268,8 +252,8 @@ export class GeoData { return position; } - getLabelOffset(angle: number, offsetDistance: number, arrowDirection: ArrowDirection): [number, number] { - const radiantAngle = (-angle + 90) / (180 / Math.PI); + getLabelOffset(angle, offsetDistance, arrowDirection) { + let radiantAngle = (-angle + 90) / (180 / Math.PI); let direction = 0; switch (arrowDirection) { case ArrowDirection.FROM_SIDE_2_TO_SIDE_1: @@ -292,11 +276,11 @@ export class GeoData { } //returns the angle between point1 and point2 in degrees [0-360) - getMapAngle(point1: LonLat, point2: LonLat) { + getMapAngle(point1, point2) { // We don't have the exact same angle calculation as in the arrow shader, and this // seems to give more approaching results let angle = getRhumbLineBearing(point1, point2); - const angle2 = getGreatCircleBearing(point1, point2); + let angle2 = getGreatCircleBearing(point1, point2); const coeff = 0.1; angle = coeff * angle + (1 - coeff) * angle2; return angle; diff --git a/src/components/network-map-viewer/network/layers/arrow-layer.ts b/src/components/network-map-viewer/network/layers/arrow-layer.js similarity index 73% rename from src/components/network-map-viewer/network/layers/arrow-layer.ts rename to src/components/network-map-viewer/network/layers/arrow-layer.js index 4c22944c..8e9f8b15 100644 --- a/src/components/network-map-viewer/network/layers/arrow-layer.ts +++ b/src/components/network-map-viewer/network/layers/arrow-layer.js @@ -4,61 +4,31 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { picking, project32 } from '@deck.gl/core'; +import { Layer, project32, picking } from '@deck.gl/core'; import GL from '@luma.gl/constants'; -import { FEATURES, Geometry, hasFeatures, isWebGL2, Model, Texture2D } from '@luma.gl/core'; +import { Model, Geometry, Texture2D, FEATURES, hasFeatures, isWebGL2 } from '@luma.gl/core'; + import vs from './arrow-layer-vertex.vert?raw'; import fs from './arrow-layer-fragment.frag?raw'; -import { Accessor, Color, Layer, LayerContext, LayerProps, Position, Texture, UpdateParameters } from 'deck.gl'; -import { Line } from '../../utils/equipment-types'; -import { type UniformValues } from 'maplibre-gl'; -const DEFAULT_COLOR = [0, 0, 0, 255] satisfies Color; +const DEFAULT_COLOR = [0, 0, 0, 255]; // this value has to be consistent with the one in vertex shader const MAX_LINE_POINT_COUNT = 2 ** 15; -export enum ArrowDirection { - NONE = 'none', - FROM_SIDE_1_TO_SIDE_2 = 'fromSide1ToSide2', - FROM_SIDE_2_TO_SIDE_1 = 'fromSide2ToSide1', -} - -export type Arrow = { - line: Line; - distance: number; +export const ArrowDirection = { + NONE: 'none', + FROM_SIDE_1_TO_SIDE_2: 'fromSide1ToSide2', + FROM_SIDE_2_TO_SIDE_1: 'fromSide2ToSide1', }; -export type LayerDataSource = DataType[]; - -type _ArrowLayerProps = { - data: Arrow[]; - sizeMinPixels?: number; - sizeMaxPixels?: number; - getDistance: Accessor; - getLine: (arrow: Arrow) => Line; - getLinePositions: (line: Line) => Position[]; - getSize?: Accessor; - getColor?: Accessor; - getSpeedFactor?: Accessor; - getDirection?: Accessor; - animated?: boolean; - getLineParallelIndex?: Accessor; - getLineAngles?: Accessor; - getDistanceBetweenLines?: Accessor; - maxParallelOffset?: number; - minParallelOffset?: number; - opacity?: number; -} & LayerProps; - -type ArrowLayerProps = _ArrowLayerProps & LayerProps; - const defaultProps = { sizeMinPixels: { type: 'number', min: 0, value: 0 }, // min size in pixels sizeMaxPixels: { type: 'number', min: 0, value: Number.MAX_SAFE_INTEGER }, // max size in pixels - getDistance: { type: 'accessor', value: (arrow: Arrow) => arrow.distance }, - getLine: { type: 'accessor', value: (arrow: Arrow) => arrow.line }, - getLinePositions: { type: 'accessor', value: (line: Line) => line.positions }, + + getDistance: { type: 'accessor', value: (arrow) => arrow.distance }, + getLine: { type: 'accessor', value: (arrow) => arrow.line }, + getLinePositions: { type: 'accessor', value: (line) => line.positions }, getSize: { type: 'accessor', value: 1 }, getColor: { type: 'accessor', value: DEFAULT_COLOR }, getSpeedFactor: { type: 'accessor', value: 1.0 }, @@ -72,13 +42,6 @@ const defaultProps = { opacity: { type: 'number', value: 1.0 }, }; -type LineAttributes = { - distance: number; - positionsTextureOffset: number; - distancesTextureOffset: number; - pointCount: number; -}; - /** * A layer that draws arrows over the lines between voltage levels. The arrows are drawn on a direct line * or with a parallel offset. The initial point is also shifted to coincide with the fork line ends. @@ -89,26 +52,12 @@ type LineAttributes = { * maxParallelOffset: max pixel distance * minParallelOffset: min pixel distance */ -export class ArrowLayer extends Layer> { - static layerName = 'ArrowLayer'; - static defaultProps = defaultProps; - - declare state: { - linePositionsTexture: Texture; - lineDistancesTexture: Texture; - lineAttributes: Map; - model?: Model; - timestamp: number; - stop: boolean; - maxTextureSize: number; - webgl2: boolean; - }; - +export class ArrowLayer extends Layer { getShaders() { return super.getShaders({ vs, fs, modules: [project32, picking] }); } - getArrowLineAttributes(arrow: Arrow): LineAttributes { + getArrowLineAttributes(arrow) { const line = this.props.getLine(arrow); if (!line) { throw new Error('Invalid line'); @@ -131,9 +80,9 @@ export class ArrowLayer extends Layer> { this.state = { maxTextureSize, webgl2: isWebGL2(gl), - } as this['state']; + }; - this.getAttributeManager()?.addInstanced({ + this.getAttributeManager().addInstanced({ instanceSize: { size: 1, transition: true, @@ -221,19 +170,13 @@ export class ArrowLayer extends Layer> { }); } - finalizeState(context: LayerContext) { - super.finalizeState(context); + finalizeState() { + super.finalizeState(); // we do not use setState to avoid a redraw, it is just used to stop the animation this.state.stop = true; } - createTexture2D( - gl: WebGLRenderingContext, - data: Array, - elementSize: number, - format: number, // is it TextureFormat? - dataFormat: number // is it TextureFormat? - ) { + createTexture2D(gl, data, elementSize, format, dataFormat) { const start = performance.now(); // we calculate the smallest square texture that is a power of 2 but less or equals to MAX_TEXTURE_SIZE @@ -278,11 +221,11 @@ export class ArrowLayer extends Layer> { return texture2d; } - createTexturesStructure(props: this['props']) { + createTexturesStructure(props) { const start = performance.now(); - const linePositionsTextureData: number[] = []; - const lineDistancesTextureData: number[] = []; + const linePositionsTextureData = []; + const lineDistancesTextureData = []; const lineAttributes = new Map(); let lineDistance = 0; @@ -298,16 +241,14 @@ export class ArrowLayer extends Layer> { const lineDistancesTextureOffset = lineDistancesTextureData.length; let linePointCount = 0; if (positions.length > 0) { - positions.forEach((position: Position) => { + positions.forEach((position) => { // fill line positions texture linePositionsTextureData.push(position[0]); linePositionsTextureData.push(position[1]); linePointCount++; }); - if (line.cumulativeDistances) { - lineDistancesTextureData.push(...line.cumulativeDistances); - lineDistance = line.cumulativeDistances[line.cumulativeDistances.length - 1]; - } + lineDistancesTextureData.push(...line.cumulativeDistances); + lineDistance = line.cumulativeDistances[line.cumulativeDistances.length - 1]; } if (linePointCount > MAX_LINE_POINT_COUNT) { throw new Error(`Too many line point count (${linePointCount}), maximum is ${MAX_LINE_POINT_COUNT}`); @@ -331,7 +272,7 @@ export class ArrowLayer extends Layer> { }; } - updateGeometry({ props, changeFlags }: UpdateParameters) { + updateGeometry({ props, changeFlags }) { const geometryChanged = changeFlags.dataChanged || (changeFlags.updateTriggersChanged && @@ -365,12 +306,12 @@ export class ArrowLayer extends Layer> { }); if (!changeFlags.dataChanged) { - this.getAttributeManager()?.invalidateAll(); + this.getAttributeManager().invalidateAll(); } } } - updateModel({ changeFlags }: UpdateParameters) { + updateModel({ changeFlags }) { if (changeFlags.extensionsChanged) { const { gl } = this.context; @@ -383,11 +324,11 @@ export class ArrowLayer extends Layer> { model: this._getModel(gl), }); - this.getAttributeManager()?.invalidateAll(); + this.getAttributeManager().invalidateAll(); } } - updateState(updateParams: UpdateParameters) { + updateState(updateParams) { super.updateState(updateParams); this.updateGeometry(updateParams); @@ -406,7 +347,7 @@ export class ArrowLayer extends Layer> { } } - animate(timestamp: number) { + animate(timestamp) { if (this.state.stop) { return; } @@ -420,33 +361,30 @@ export class ArrowLayer extends Layer> { window.requestAnimationFrame((timestamp) => this.animate(timestamp)); } - // TODO find the full type for record values - draw({ uniforms }: { uniforms: Record> }) { + draw({ uniforms }) { const { sizeMinPixels, sizeMaxPixels } = this.props; const { linePositionsTexture, lineDistancesTexture, timestamp, webgl2 } = this.state; - if (this.state.model) { - this.state.model - .setUniforms({ - ...uniforms, - sizeMinPixels, - sizeMaxPixels, - linePositionsTexture, - lineDistancesTexture, - linePositionsTextureSize: [linePositionsTexture.width, linePositionsTexture.height], - lineDistancesTextureSize: [lineDistancesTexture.width, lineDistancesTexture.height], - timestamp, - webgl2, - distanceBetweenLines: this.props.getDistanceBetweenLines, - maxParallelOffset: this.props.maxParallelOffset, - minParallelOffset: this.props.minParallelOffset, - }) - .draw(); - } + this.state.model + .setUniforms(uniforms) + .setUniforms({ + sizeMinPixels, + sizeMaxPixels, + linePositionsTexture, + lineDistancesTexture, + linePositionsTextureSize: [linePositionsTexture.width, linePositionsTexture.height], + lineDistancesTextureSize: [lineDistancesTexture.width, lineDistancesTexture.height], + timestamp, + webgl2, + distanceBetweenLines: this.props.getDistanceBetweenLines, + maxParallelOffset: this.props.maxParallelOffset, + minParallelOffset: this.props.minParallelOffset, + }) + .draw(); } - _getModel(gl: WebGLRenderingContext) { + _getModel(gl) { const positions = [-1, -1, 0, 0, 1, 0, 0, -0.6, 0, 1, -1, 0, 0, 1, 0, 0, -0.6, 0]; return new Model( @@ -468,3 +406,6 @@ export class ArrowLayer extends Layer> { ); } } + +ArrowLayer.layerName = 'ArrowLayer'; +ArrowLayer.defaultProps = defaultProps; diff --git a/src/components/network-map-viewer/network/layers/fork-line-layer.ts b/src/components/network-map-viewer/network/layers/fork-line-layer.js similarity index 73% rename from src/components/network-map-viewer/network/layers/fork-line-layer.ts rename to src/components/network-map-viewer/network/layers/fork-line-layer.js index eaaee0a4..ca8708e1 100644 --- a/src/components/network-map-viewer/network/layers/fork-line-layer.ts +++ b/src/components/network-map-viewer/network/layers/fork-line-layer.js @@ -5,34 +5,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Accessor, LineLayer, LineLayerProps } from 'deck.gl'; -import { DefaultProps } from '@deck.gl/core'; +import { LineLayer } from 'deck.gl'; import GL from '@luma.gl/constants'; -import { UniformValues } from 'maplibre-gl'; -export type ForkLineLayerProps = _ForkLineLayerProps & LineLayerProps; - -type _ForkLineLayerProps = { - getLineParallelIndex: Accessor; - getLineAngle: Accessor; - distanceBetweenLines: Accessor; - maxParallelOffset: Accessor; - minParallelOffset: Accessor; - substationRadius: Accessor; - substationMaxPixel: Accessor; - minSubstationRadiusPixel: Accessor; - getDistanceBetweenLines: Accessor; - getMaxParallelOffset: Accessor; - getMinParallelOffset: Accessor; - getSubstationRadius: Accessor; - getSubstationMaxPixel: Accessor; - getMinSubstationRadiusPixel: Accessor; -}; - -const defaultProps: DefaultProps = { +const defaultProps = { getLineParallelIndex: { type: 'accessor', value: 0 }, getLineAngle: { type: 'accessor', value: 0 }, distanceBetweenLines: { type: 'number', value: 1000 }, + maxParallelOffset: { type: 'number', value: 100 }, + minParallelOffset: { type: 'number', value: 3 }, + substationRadius: { type: 'number', value: 500 }, + substationMaxPixel: { type: 'number', value: 5 }, + minSubstationRadiusPixel: { type: 'number', value: 1 }, }; /** @@ -48,10 +32,7 @@ const defaultProps: DefaultProps = { * substationMaxPixel: max pixel for a voltage level in substation * minSubstationRadiusPixel : min pixel for a substation */ -export default class ForkLineLayer extends LineLayer>> { - static layerName = 'ForkLineLayer'; - static defaultProps = defaultProps; - +export default class ForkLineLayer extends LineLayer { getShaders() { const shaders = super.getShaders(); shaders.inject = { @@ -90,11 +71,11 @@ uniform float minSubstationRadiusPixel; return shaders; } - initializeState() { - super.initializeState(); + initializeState(params) { + super.initializeState(params); const attributeManager = this.getAttributeManager(); - attributeManager?.addInstanced({ + attributeManager.addInstanced({ instanceLineParallelIndex: { size: 1, type: GL.FLOAT, @@ -118,8 +99,7 @@ uniform float minSubstationRadiusPixel; }); } - // TODO find the full type for record values - draw({ uniforms }: { uniforms: Record> }) { + draw({ uniforms }) { super.draw({ uniforms: { ...uniforms, @@ -133,3 +113,6 @@ uniform float minSubstationRadiusPixel; }); } } + +ForkLineLayer.layerName = 'ForkLineLayer'; +ForkLineLayer.defaultProps = defaultProps; diff --git a/src/components/network-map-viewer/network/layers/parallel-path-layer.ts b/src/components/network-map-viewer/network/layers/parallel-path-layer.js similarity index 84% rename from src/components/network-map-viewer/network/layers/parallel-path-layer.ts rename to src/components/network-map-viewer/network/layers/parallel-path-layer.js index d724d036..e100f378 100644 --- a/src/components/network-map-viewer/network/layers/parallel-path-layer.ts +++ b/src/components/network-map-viewer/network/layers/parallel-path-layer.js @@ -4,10 +4,16 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Accessor, PathLayer, PathLayerProps } from 'deck.gl'; -import { DefaultProps } from '@deck.gl/core'; +import { PathLayer } from 'deck.gl'; import GL from '@luma.gl/constants'; -import { UniformValues } from 'maplibre-gl'; + +const defaultProps = { + getLineParallelIndex: { type: 'accessor', value: 0 }, + getLineAngle: { type: 'accessor', value: 0 }, + distanceBetweenLines: { type: 'number', value: 1000 }, + maxParallelOffset: { type: 'number', value: 100 }, + minParallelOffset: { type: 'number', value: 3 }, +}; /** * A layer based on PathLayer allowing to shift path by an offset + angle @@ -21,30 +27,7 @@ import { UniformValues } from 'maplibre-gl'; * maxParallelOffset: max pixel distance * minParallelOffset: min pixel distance */ - -type _ParallelPathLayerProps = { - getLineParallelIndex?: Accessor; - getLineAngle?: Accessor; - distanceBetweenLines?: number; - maxParallelOffset?: number; - minParallelOffset?: number; -}; - -export type ParallelPathLayerProps = _ParallelPathLayerProps & PathLayerProps; - -const defaultProps: DefaultProps = { - getLineParallelIndex: { type: 'accessor', value: 0 }, - getLineAngle: { type: 'accessor', value: 0 }, - distanceBetweenLines: { type: 'number', value: 1000 }, -}; - -export default class ParallelPathLayer extends PathLayer< - DataT, - Required> -> { - static layerName = 'ParallelPathLayer'; - static defaultProps = defaultProps; - +export default class ParallelPathLayer extends PathLayer { getShaders() { const shaders = super.getShaders(); shaders.inject = Object.assign({}, shaders.inject, { @@ -118,11 +101,11 @@ gl_Position += project_common_position_to_clipspace(trans) - project_uCenter; return shaders; } - initializeState() { - super.initializeState(); + initializeState(params) { + super.initializeState(params); const attributeManager = this.getAttributeManager(); - attributeManager?.addInstanced({ + attributeManager.addInstanced({ // too much instances variables need to compact some... instanceExtraAttributes: { size: 4, @@ -132,8 +115,7 @@ gl_Position += project_common_position_to_clipspace(trans) - project_uCenter; }); } - // TODO find the full type for record values - draw({ uniforms }: { uniforms: Record> }) { + draw({ uniforms }) { super.draw({ uniforms: { ...uniforms, @@ -144,3 +126,6 @@ gl_Position += project_common_position_to_clipspace(trans) - project_uCenter; }); } } + +ParallelPathLayer.layerName = 'ParallelPathLayer'; +ParallelPathLayer.defaultProps = defaultProps; diff --git a/src/components/network-map-viewer/network/layers/scatterplot-layer-ext.ts b/src/components/network-map-viewer/network/layers/scatterplot-layer-ext.js similarity index 61% rename from src/components/network-map-viewer/network/layers/scatterplot-layer-ext.ts rename to src/components/network-map-viewer/network/layers/scatterplot-layer-ext.js index e1ea5df3..d7d8fc9d 100644 --- a/src/components/network-map-viewer/network/layers/scatterplot-layer-ext.ts +++ b/src/components/network-map-viewer/network/layers/scatterplot-layer-ext.js @@ -4,29 +4,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Accessor, ScatterplotLayer, ScatterplotLayerProps } from 'deck.gl'; -import { DefaultProps } from '@deck.gl/core'; - +import { ScatterplotLayer } from 'deck.gl'; import GL from '@luma.gl/constants'; -type _ScatterplotLayerExtProps = { - getRadiusMaxPixels: Accessor; -}; -export type ScatterplotLayerExtProps = _ScatterplotLayerExtProps & ScatterplotLayerProps; - -const defaultProps: DefaultProps = { +const defaultProps = { getRadiusMaxPixels: { type: 'accessor', value: 1 }, }; /** * An extended scatter plot layer that allows a radius max pixels to be different for each object. */ -export default class ScatterplotLayerExt extends ScatterplotLayer< - Required<_ScatterplotLayerExtProps> -> { - static layerName = 'ScatterplotLayerExt'; - static defaultProps = defaultProps; - +export default class ScatterplotLayerExt extends ScatterplotLayer { getShaders() { const shaders = super.getShaders(); return Object.assign({}, shaders, { @@ -39,11 +27,11 @@ attribute float instanceRadiusMaxPixels; }); } - initializeState() { - super.initializeState(); + initializeState(params) { + super.initializeState(params); const attributeManager = this.getAttributeManager(); - attributeManager?.addInstanced({ + attributeManager.addInstanced({ instanceRadiusMaxPixels: { size: 1, transition: true, @@ -54,3 +42,6 @@ attribute float instanceRadiusMaxPixels; }); } } + +ScatterplotLayerExt.layerName = 'ScatterplotLayerExt'; +ScatterplotLayerExt.defaultProps = defaultProps; diff --git a/src/components/network-map-viewer/network/line-layer.ts b/src/components/network-map-viewer/network/line-layer.js similarity index 65% rename from src/components/network-map-viewer/network/line-layer.ts rename to src/components/network-map-viewer/network/line-layer.js index 36b1f3e2..56e497ad 100644 --- a/src/components/network-map-viewer/network/line-layer.ts +++ b/src/components/network-map-viewer/network/line-layer.js @@ -5,54 +5,41 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { - CompositeLayer, - TextLayer, - IconLayer, - Position, - Color, - CompositeLayerProps, - LayerContext, - UpdateParameters, - Layer, -} from 'deck.gl'; +import { CompositeLayer, TextLayer, IconLayer } from 'deck.gl'; import PadlockIcon from '../images/lock_black_24dp.svg?react'; import BoltIcon from '../images/bolt_black_24dp.svg?react'; import { PathStyleExtension } from '@deck.gl/extensions'; -import { ArrowLayer, ArrowDirection, Arrow } from './layers/arrow-layer'; +import { ArrowLayer, ArrowDirection } from './layers/arrow-layer'; import ParallelPathLayer from './layers/parallel-path-layer'; import ForkLineLayer from './layers/fork-line-layer'; import { getDistance } from 'geolib'; import { SUBSTATION_RADIUS, SUBSTATION_RADIUS_MAX_PIXEL, SUBSTATION_RADIUS_MIN_PIXEL } from './constants'; -import { getNominalVoltageColor, INVALID_FLOW_OPACITY } from '../../../utils/colors'; -import { Line, LonLat, VoltageLevel } from '../utils/equipment-types'; -import { MapEquipments } from './map-equipments'; -import { GeoData } from './geo-data'; +import { INVALID_FLOW_OPACITY } from '../../../utils/colors'; const DISTANCE_BETWEEN_ARROWS = 10000.0; //Constants for Feeders mode const START_ARROW_POSITION = 0.1; const END_ARROW_POSITION = 0.9; -export enum LineFlowMode { - STATIC_ARROWS = 'staticArrows', - ANIMATED_ARROWS = 'animatedArrows', - FEEDERS = 'feeders', -} +export const LineFlowMode = { + STATIC_ARROWS: 'staticArrows', + ANIMATED_ARROWS: 'animatedArrows', + FEEDERS: 'feeders', +}; -export enum LineFlowColorMode { - NOMINAL_VOLTAGE = 'nominalVoltage', - OVERLOADS = 'overloads', -} +export const LineFlowColorMode = { + NOMINAL_VOLTAGE: 'nominalVoltage', + OVERLOADS: 'overloads', +}; const noDashArray = [0, 0]; const dashArray = [15, 10]; -function doDash(lineConnection: LineConnection) { +function doDash(lineConnection) { return !lineConnection.terminal1Connected || !lineConnection.terminal2Connected; } -function getArrowDirection(p: number) { +function getArrowDirection(p) { if (p < 0) { return ArrowDirection.FROM_SIDE_2_TO_SIDE_1; } else if (p > 0) { @@ -62,22 +49,18 @@ function getArrowDirection(p: number) { } } -export enum LineLoadingZone { - UNKNOWN = 0, - SAFE = 1, - WARNING = 2, - OVERLOAD = 3, -} +export const LineLoadingZone = { + UNKNOWN: 0, + SAFE: 1, + WARNING: 2, + OVERLOAD: 3, +}; -export function getLineLoadingZoneOfSide( - limit: number | undefined, - intensity: number | undefined, - lineFlowAlertThreshold: number -) { +export function getLineLoadingZoneOfSide(limit, intensity, lineFlowAlertThreshold) { if (limit === undefined || intensity === undefined || intensity === 0) { return LineLoadingZone.UNKNOWN; } else { - const threshold = (lineFlowAlertThreshold * limit) / 100; + let threshold = (lineFlowAlertThreshold * limit) / 100; const absoluteIntensity = Math.abs(intensity); if (absoluteIntensity < threshold) { return LineLoadingZone.SAFE; @@ -89,13 +72,13 @@ export function getLineLoadingZoneOfSide( } } -export function getLineLoadingZone(line: Line, lineFlowAlertThreshold: number) { +export function getLineLoadingZone(line, lineFlowAlertThreshold) { const zone1 = getLineLoadingZoneOfSide(line.currentLimits1?.permanentLimit, line.i1, lineFlowAlertThreshold); const zone2 = getLineLoadingZoneOfSide(line.currentLimits2?.permanentLimit, line.i2, lineFlowAlertThreshold); return Math.max(zone1, zone2); } -function getLineLoadingZoneColor(zone: LineLoadingZone): Color { +function getLineLoadingZoneColor(zone) { if (zone === LineLoadingZone.UNKNOWN) { return [128, 128, 128]; // grey } else if (zone === LineLoadingZone.SAFE) { @@ -109,7 +92,7 @@ function getLineLoadingZoneColor(zone: LineLoadingZone): Color { } } -function getLineColor(line: Line, nominalVoltageColor: Color, props: LineLayerProps, lineConnection: LineConnection) { +function getLineColor(line, nominalVoltageColor, props, lineConnection) { if (props.lineFlowColorMode === LineFlowColorMode.NOMINAL_VOLTAGE) { if (!lineConnection.terminal1Connected && !lineConnection.terminal2Connected) { return props.disconnectedLineColor; @@ -124,7 +107,7 @@ function getLineColor(line: Line, nominalVoltageColor: Color, props: LineLayerPr } } -function getLineIcon(lineStatus: LineStatus) { +function getLineIcon(lineStatus) { return { url: lineStatus === 'PLANNED_OUTAGE' ? PadlockIcon : lineStatus === 'FORCED_OUTAGE' ? BoltIcon : undefined, height: 24, @@ -141,7 +124,7 @@ export const ArrowSpeed = { CRAZY: 4, }; -function getArrowSpeedOfSide(limit: number | undefined, intensity: number | undefined) { +function getArrowSpeedOfSide(limit, intensity) { if (limit === undefined || intensity === undefined || intensity === 0) { return ArrowSpeed.STOPPED; } else { @@ -158,13 +141,13 @@ function getArrowSpeedOfSide(limit: number | undefined, intensity: number | unde } } -function getArrowSpeed(line: Line) { +function getArrowSpeed(line) { const speed1 = getArrowSpeedOfSide(line.currentLimits1?.permanentLimit, line.i1); const speed2 = getArrowSpeedOfSide(line.currentLimits2?.permanentLimit, line.i2); return Math.max(speed1, speed2); } -function getArrowSpeedFactor(speed: number) { +function getArrowSpeedFactor(speed) { switch (speed) { case ArrowSpeed.STOPPED: return 0; @@ -181,125 +164,9 @@ function getArrowSpeedFactor(speed: number) { } } -type LineConnection = { - terminal1Connected: boolean; - terminal2Connected: boolean; -}; - -export enum LineStatus { - PLANNED_OUTAGE = 'PLANNED_OUTAGE', - FORCED_OUTAGE = 'FORCED_OUTAGE', - IN_OPERATION = 'IN_OPERATION', -} - -type LinesStatus = { - operatingStatus: LineStatus; -}; - -type CompositeDataLine = { - nominalV: number; - lines: Line[]; - arrows: Arrow[]; - positions: LonLat[]; - cumulativeDistances: number[]; -}; - -type ActivePower = { - p: number | undefined; - printPosition: Position; - offset: [number, number]; - line: Line; -}; - -type OperatingStatus = { - status: LineStatus; - printPosition: Position; - offset: [number, number]; -}; - -export type CompositeData = { - nominalV: number; - mapOriginDestination?: Map>; - lines: Line[]; - lineMap?: Map; - activePower: ActivePower[]; - operatingStatus: OperatingStatus[]; - arrows: Arrow[]; -}; - -type MinProximityFactor = { - lines: Line[]; - start: number; - end: number; -}; - -type _LineLayerProps = { - data: Line[]; - network: MapEquipments; - geoData: GeoData; - getNominalVoltageColor: (voltage: number) => Color; - disconnectedLineColor: Color; - filteredNominalVoltages: number[] | null; - lineFlowMode: LineFlowMode; - lineFlowColorMode: LineFlowColorMode; - lineFlowAlertThreshold: number; - showLineFlow: boolean; - lineFullPath: boolean; - lineParallelPath: boolean; - labelSize: number; - iconSize: number; - distanceBetweenLines: number; - maxParallelOffset: number; - minParallelOffset: number; - substationRadius: number; - substationMaxPixel: number; - minSubstationRadiusPixel: number; - areFlowsValid: boolean; - updatedLines: Line[]; - labelsVisible: boolean; - labelColor: Color; -}; - -export type LineLayerProps = _LineLayerProps & CompositeLayerProps; - -const defaultProps = { - network: null, - geoData: null, - getNominalVoltageColor: { type: 'accessor', value: getNominalVoltageColor }, - disconnectedLineColor: { type: 'color', value: [255, 255, 255] }, - filteredNominalVoltages: null, - lineFlowMode: LineFlowMode.FEEDERS, - lineFlowColorMode: LineFlowColorMode.NOMINAL_VOLTAGE, - lineFlowAlertThreshold: 100, - showLineFlow: true, - lineFullPath: true, - lineParallelPath: true, - labelSize: 12, - iconSize: 48, - distanceBetweenLines: 1000, - maxParallelOffset: 100, - minParallelOffset: 3, - substationRadius: { type: 'number', value: SUBSTATION_RADIUS }, - substationMaxPixel: { type: 'number', value: SUBSTATION_RADIUS_MAX_PIXEL }, - minSubstationRadiusPixel: { - type: 'number', - value: SUBSTATION_RADIUS_MIN_PIXEL, - }, - labelColor: [255, 255, 255], -}; - -export class LineLayer extends CompositeLayer> { - static layerName = 'LineLayer'; - static defaultProps = defaultProps; - - declare state: { - compositeData: CompositeData[]; - linesConnection: Map; - linesStatus: Map; - }; - - initializeState(context: LayerContext) { - super.initializeState(context); +export class LineLayer extends CompositeLayer { + initializeState() { + super.initializeState(); this.state = { compositeData: [], @@ -308,23 +175,17 @@ export class LineLayer extends CompositeLayer> { }; } - getVoltageLevelIndex(voltageLevelId: string) { + getVoltageLevelIndex(voltageLevelId) { const { network } = this.props; const vl = network.getVoltageLevel(voltageLevelId); - if (vl === undefined) { - return undefined; - } const substation = network.getSubstation(vl.substationId); - if (substation === undefined) { - return undefined; - } return ( [ ...new Set( - substation.voltageLevels.map((vl: VoltageLevel) => vl.nominalV) // only one voltage level + substation.voltageLevels.map((vl) => vl.nominalV) // only one voltage level ), ] - .sort((a: number, b: number) => { + .sort((a, b) => { return a - b; // force numerical sort }) .indexOf(vl.nominalV) + 1 @@ -332,16 +193,16 @@ export class LineLayer extends CompositeLayer> { } //TODO this is a huge function, refactor - updateState({ props, oldProps, changeFlags }: UpdateParameters) { - let compositeData: Partial[]; - let linesConnection: Map | undefined; - let linesStatus: Map | undefined; + updateState({ props, oldProps, changeFlags }) { + let compositeData; + let linesConnection; + let linesStatus; if (changeFlags.dataChanged) { compositeData = []; - linesConnection = new Map(); - linesStatus = new Map(); + linesConnection = new Map(); + linesStatus = new Map(); if ( props.network != null && @@ -350,10 +211,10 @@ export class LineLayer extends CompositeLayer> { props.geoData != null ) { // group lines by nominal voltage - const lineNominalVoltageIndexer = (map: Map, line: Line) => { + const lineNominalVoltageIndexer = (map, line) => { const network = props.network; - const vl1 = network.getVoltageLevel(line.voltageLevelId1)!; - const vl2 = network.getVoltageLevel(line.voltageLevelId2)!; + const vl1 = network.getVoltageLevel(line.voltageLevelId1); + const vl2 = network.getVoltageLevel(line.voltageLevelId2); const vl = vl1 || vl2; let list = map.get(vl.nominalV); if (!list) { @@ -365,30 +226,30 @@ export class LineLayer extends CompositeLayer> { } return map; }; - const linesByNominalVoltage = props.data.reduce(lineNominalVoltageIndexer, new Map()); + const linesByNominalVoltage = props.data.reduce(lineNominalVoltageIndexer, new Map()); compositeData = Array.from(linesByNominalVoltage.entries()) - .map(([nominalV, lines]) => { - return { nominalV, lines }; + .map((e) => { + return { nominalV: e[0], lines: e[1] }; }) .sort((a, b) => b.nominalV - a.nominalV); - compositeData.forEach((c) => { + compositeData.forEach((compositeData) => { //find lines with same substations set - const mapOriginDestination = new Map(); - c.mapOriginDestination = mapOriginDestination; - c.lines?.forEach((line) => { - linesConnection?.set(line.id, { + let mapOriginDestination = new Map(); + compositeData.mapOriginDestination = mapOriginDestination; + compositeData.lines.forEach((line) => { + linesConnection.set(line.id, { terminal1Connected: line.terminal1Connected, terminal2Connected: line.terminal2Connected, }); - linesStatus?.set(line.id, { - operatingStatus: line.operatingStatus!, + linesStatus.set(line.id, { + operatingStatus: line.operatingStatus, }); const key = this.genLineKey(line); - const val = mapOriginDestination.get(key); + let val = mapOriginDestination.get(key); if (val == null) { mapOriginDestination.set(key, new Set([line])); } else { @@ -404,12 +265,12 @@ export class LineLayer extends CompositeLayer> { if (props.updatedLines !== oldProps.updatedLines) { props.updatedLines.forEach((line1) => { - linesConnection?.set(line1.id, { + linesConnection.set(line1.id, { terminal1Connected: line1.terminal1Connected, terminal2Connected: line1.terminal2Connected, }); - linesStatus?.set(line1.id, { - operatingStatus: line1.operatingStatus!, + linesStatus.set(line1.id, { + operatingStatus: line1.operatingStatus, }); }); } @@ -422,7 +283,7 @@ export class LineLayer extends CompositeLayer> { props.lineParallelPath !== oldProps.lineParallelPath || props.geoData !== oldProps.geoData)) ) { - this.recomputeParallelLinesIndex(compositeData as CompositeData[], props); + this.recomputeParallelLinesIndex(compositeData, props); } if ( @@ -430,9 +291,9 @@ export class LineLayer extends CompositeLayer> { (changeFlags.propsChanged && (oldProps.lineFullPath !== props.lineFullPath || oldProps.geoData !== props.geoData)) ) { - compositeData.forEach((c) => { - const lineMap = new Map(); - c.lines?.forEach((line) => { + compositeData.forEach((compositeData) => { + let lineMap = new Map(); + compositeData.lines.forEach((line) => { const positions = props.geoData.getLinePositions(props.network, line, props.lineFullPath); const cumulativeDistances = props.geoData.getLineDistances(positions); lineMap.set(line.id, { @@ -441,7 +302,7 @@ export class LineLayer extends CompositeLayer> { line: line, }); }); - c.lineMap = lineMap; + compositeData.lineMap = lineMap; }); } @@ -452,7 +313,7 @@ export class LineLayer extends CompositeLayer> { props.lineParallelPath !== oldProps.lineParallelPath || props.geoData !== oldProps.geoData)) ) { - this.recomputeForkLines(compositeData as CompositeData[], props); + this.recomputeForkLines(compositeData, props); } if ( @@ -463,45 +324,41 @@ export class LineLayer extends CompositeLayer> { props.geoData !== oldProps.geoData)) ) { //add labels - compositeData.forEach((cData) => { - cData.activePower = []; - cData.lines?.forEach((line) => { - const lineData = cData.lineMap?.get(line.id); - const arrowDirection = getArrowDirection(line.p1); - const coordinates1 = lineData - ? props.geoData.labelDisplayPosition( - lineData.positions, - lineData.cumulativeDistances, - START_ARROW_POSITION, - arrowDirection, - line.parallelIndex!, - (line.angle! * 180) / Math.PI, - (line.angleStart! * 180) / Math.PI, - props.distanceBetweenLines, - line.proximityFactorStart! - ) - : null; - const coordinates2 = lineData - ? props.geoData.labelDisplayPosition( - lineData.positions, - lineData.cumulativeDistances, - END_ARROW_POSITION, - arrowDirection, - line.parallelIndex!, - (line.angle! * 180) / Math.PI, - (line.angleEnd! * 180) / Math.PI, - props.distanceBetweenLines, - line.proximityFactorEnd! - ) - : null; + compositeData.forEach((compositeData) => { + compositeData.activePower = []; + compositeData.lines.forEach((line) => { + let lineData = compositeData.lineMap.get(line.id); + let arrowDirection = getArrowDirection(line.p1); + let coordinates1 = props.geoData.labelDisplayPosition( + lineData.positions, + lineData.cumulativeDistances, + START_ARROW_POSITION, + arrowDirection, + line.parallelIndex, + (line.angle * 180) / Math.PI, + (line.angleStart * 180) / Math.PI, + props.distanceBetweenLines, + line.proximityFactorStart + ); + let coordinates2 = props.geoData.labelDisplayPosition( + lineData.positions, + lineData.cumulativeDistances, + END_ARROW_POSITION, + arrowDirection, + line.parallelIndex, + (line.angle * 180) / Math.PI, + (line.angleEnd * 180) / Math.PI, + props.distanceBetweenLines, + line.proximityFactorEnd + ); if (coordinates1 !== null && coordinates2 !== null) { - cData.activePower?.push({ + compositeData.activePower.push({ line: line, p: line.p1, printPosition: [coordinates1.position.longitude, coordinates1.position.latitude], offset: coordinates1.offset, }); - cData.activePower?.push({ + compositeData.activePower.push({ line: line, p: line.p2, printPosition: [coordinates2.position.longitude, coordinates2.position.latitude], @@ -521,40 +378,33 @@ export class LineLayer extends CompositeLayer> { props.geoData !== oldProps.geoData)) ) { //add icons - compositeData.forEach((cData) => { - cData.operatingStatus = []; - cData.lines?.forEach((line) => { - const lineStatus = linesStatus?.get(line.id); + compositeData.forEach((compositeData) => { + compositeData.operatingStatus = []; + compositeData.lines.forEach((line) => { + let lineStatus = linesStatus.get(line.id); if ( lineStatus !== undefined && lineStatus.operatingStatus !== undefined && lineStatus.operatingStatus !== 'IN_OPERATION' ) { - if (cData.lineMap) { - const lineData = cData.lineMap.get(line.id); - if (lineData) { - const coordinatesIcon = props.geoData.labelDisplayPosition( - lineData.positions, - lineData.cumulativeDistances, - 0.5, - ArrowDirection.NONE, - line.parallelIndex!, - (line.angle! * 180) / Math.PI, - (line.angleEnd! * 180) / Math.PI, - props.distanceBetweenLines, - line.proximityFactorEnd! - ); - if (coordinatesIcon !== null) { - cData.operatingStatus?.push({ - status: lineStatus.operatingStatus, - printPosition: [ - coordinatesIcon.position.longitude, - coordinatesIcon.position.latitude, - ], - offset: coordinatesIcon.offset, - }); - } - } + let lineData = compositeData.lineMap.get(line.id); + let coordinatesIcon = props.geoData.labelDisplayPosition( + lineData.positions, + lineData.cumulativeDistances, + 0.5, + ArrowDirection.NONE, + line.parallelIndex, + (line.angle * 180) / Math.PI, + (line.angleEnd * 180) / Math.PI, + props.distanceBetweenLines, + line.proximityFactorEnd + ); + if (coordinatesIcon !== null) { + compositeData.operatingStatus.push({ + status: lineStatus.operatingStatus, + printPosition: [coordinatesIcon.position.longitude, coordinatesIcon.position.latitude], + offset: coordinatesIcon.offset, + }); } } }); @@ -573,12 +423,12 @@ export class LineLayer extends CompositeLayer> { oldProps.lineFlowMode === LineFlowMode.FEEDERS)))) ) { // add arrows - compositeData.forEach((cData) => { - const lineMap = cData.lineMap!; + compositeData.forEach((compositeData) => { + const lineMap = compositeData.lineMap; // create one arrow each DISTANCE_BETWEEN_ARROWS - cData.arrows = cData.lines?.flatMap((line) => { - const lineData = lineMap.get(line.id)!; + compositeData.arrows = compositeData.lines.flatMap((line) => { + let lineData = lineMap.get(line.id); line.cumulativeDistances = lineData.cumulativeDistances; line.positions = lineData.positions; @@ -620,18 +470,22 @@ export class LineLayer extends CompositeLayer> { }); }); } - this.setState({ compositeData, linesConnection, linesStatus }); + this.setState({ + compositeData: compositeData, + linesConnection: linesConnection, + linesStatus: linesStatus, + }); } - genLineKey(line: Line) { + genLineKey(line) { return line.voltageLevelId1 > line.voltageLevelId2 ? line.voltageLevelId1 + '##' + line.voltageLevelId2 : line.voltageLevelId2 + '##' + line.voltageLevelId1; } - recomputeParallelLinesIndex(compositeData: CompositeData[], props: this['props']) { - compositeData.forEach((cData) => { - const mapOriginDestination = cData.mapOriginDestination!; + recomputeParallelLinesIndex(compositeData, props) { + compositeData.forEach((compositeData) => { + const mapOriginDestination = compositeData.mapOriginDestination; // calculate index for line with same substation set // The index is a real number in a normalized unit. // +1 => distanceBetweenLines on side @@ -662,14 +516,11 @@ export class LineLayer extends CompositeLayer> { }); } - recomputeForkLines(compositeData: CompositeData[], props: this['props']) { - const mapMinProximityFactor = new Map(); - compositeData.forEach((cData) => { - cData.lines.forEach((line) => { - const positions = cData?.lineMap?.get(line.id)?.positions; - if (!positions) { - return; - } + recomputeForkLines(compositeData, props) { + const mapMinProximityFactor = new Map(); + compositeData.forEach((compositeData) => { + compositeData.lines.forEach((line) => { + const positions = compositeData.lineMap.get(line.id).positions; //the first and last in positions doesn't depend on lineFullPath line.origin = positions[0]; line.end = positions[positions.length - 1]; @@ -690,8 +541,8 @@ export class LineLayer extends CompositeLayer> { positions[positions.length - 1] ); - const key = this.genLineKey(line); - const val = mapMinProximityFactor.get(key); + let key = this.genLineKey(line); + let val = mapMinProximityFactor.get(key); if (val == null) { mapMinProximityFactor.set(key, { lines: [line], @@ -714,7 +565,7 @@ export class LineLayer extends CompositeLayer> { ); } - getProximityFactor(firstPosition: LonLat, secondPosition: LonLat) { + getProximityFactor(firstPosition, secondPosition) { let factor = getDistance(firstPosition, secondPosition) / (3 * this.props.distanceBetweenLines); if (factor > 1) { factor = 1; @@ -722,7 +573,7 @@ export class LineLayer extends CompositeLayer> { return factor; } - computeAngle(props: this['props'], position1: LonLat, position2: LonLat) { + computeAngle(props, position1, position2) { let angle = props.geoData.getMapAngle(position1, position2); angle = (angle * Math.PI) / 180 + Math.PI; if (angle < 0) { @@ -732,7 +583,7 @@ export class LineLayer extends CompositeLayer> { } renderLayers() { - const layers: Layer[] = []; + const layers = []; const linePathUpdateTriggers = [ this.props.lineFullPath, @@ -741,36 +592,36 @@ export class LineLayer extends CompositeLayer> { ]; // lines : create one layer per nominal voltage, starting from higher to lower nominal voltage - this.state.compositeData.forEach((cData) => { - const nominalVoltageColor = this.props.getNominalVoltageColor(cData.nominalV); + this.state.compositeData.forEach((compositeData) => { + const nominalVoltageColor = this.props.getNominalVoltageColor(compositeData.nominalV); const lineLayer = new ParallelPathLayer( this.getSubLayerProps({ - id: 'LineNominalVoltage' + cData.nominalV, - data: cData.lines, + id: 'LineNominalVoltage' + compositeData.nominalV, + data: compositeData.lines, widthScale: 20, widthMinPixels: 1, widthMaxPixels: 2, - getPath: (line: Line) => + getPath: (line) => this.props.geoData.getLinePositions(this.props.network, line, this.props.lineFullPath), - getColor: (line: Line) => - getLineColor(line, nominalVoltageColor, this.props, this.state.linesConnection.get(line.id)!), + getColor: (line) => + getLineColor(line, nominalVoltageColor, this.props, this.state.linesConnection.get(line.id)), getWidth: 2, - getLineParallelIndex: (line: Line) => line.parallelIndex, - getExtraAttributes: (line: Line) => [ + getLineParallelIndex: (line) => line.parallelIndex, + getExtraAttributes: (line) => [ line.angleStart, line.angle, line.angleEnd, - line.parallelIndex! * 2 + + line.parallelIndex * 2 + 31 + - 64 * (Math.ceil(line.proximityFactorStart! * 512) - 1) + - 64 * 512 * (Math.ceil(line.proximityFactorEnd! * 512) - 1), + 64 * (Math.ceil(line.proximityFactorStart * 512) - 1) + + 64 * 512 * (Math.ceil(line.proximityFactorEnd * 512) - 1), ], distanceBetweenLines: this.props.distanceBetweenLines, maxParallelOffset: this.props.maxParallelOffset, minParallelOffset: this.props.minParallelOffset, visible: !this.props.filteredNominalVoltages || - this.props.filteredNominalVoltages.includes(cData.nominalV), + this.props.filteredNominalVoltages.includes(compositeData.nominalV), updateTriggers: { getPath: linePathUpdateTriggers, getExtraAttributes: [this.props.lineParallelPath, linePathUpdateTriggers], @@ -782,8 +633,7 @@ export class LineLayer extends CompositeLayer> { ], getDashArray: [this.props.updatedLines], }, - getDashArray: (line: Line) => - doDash(this.state.linesConnection!.get(line.id)!) ? dashArray : noDashArray, + getDashArray: (line) => (doDash(this.state.linesConnection.get(line.id)) ? dashArray : noDashArray), extensions: [new PathStyleExtension({ dash: true })], }) ); @@ -791,40 +641,37 @@ export class LineLayer extends CompositeLayer> { const arrowLayer = new ArrowLayer( this.getSubLayerProps({ - id: 'ArrowNominalVoltage' + cData.nominalV, - data: cData.arrows, + id: 'ArrowNominalVoltage' + compositeData.nominalV, + data: compositeData.arrows, sizeMinPixels: 3, sizeMaxPixels: 7, - getDistance: (arrow: Arrow) => arrow.distance, - getLine: (arrow: Arrow) => arrow.line, - getLinePositions: (line: Line) => + getDistance: (arrow) => arrow.distance, + getLine: (arrow) => arrow.line, + getLinePositions: (line) => this.props.geoData.getLinePositions(this.props.network, line, this.props.lineFullPath), - getColor: (arrow: Arrow) => + getColor: (arrow) => getLineColor( arrow.line, nominalVoltageColor, this.props, - this.state.linesConnection.get(arrow.line.id)! + this.state.linesConnection.get(arrow.line.id) ), getSize: 700, - getSpeedFactor: (arrow: Arrow) => getArrowSpeedFactor(getArrowSpeed(arrow.line)), - getLineParallelIndex: (arrow: Arrow) => arrow.line.parallelIndex, - getLineAngles: (arrow: Arrow) => [arrow.line.angleStart, arrow.line.angle, arrow.line.angleEnd], - getProximityFactors: (arrow: Arrow) => [ - arrow.line.proximityFactorStart, - arrow.line.proximityFactorEnd, - ], + getSpeedFactor: (arrow) => getArrowSpeedFactor(getArrowSpeed(arrow.line)), + getLineParallelIndex: (arrow) => arrow.line.parallelIndex, + getLineAngles: (arrow) => [arrow.line.angleStart, arrow.line.angle, arrow.line.angleEnd], + getProximityFactors: (arrow) => [arrow.line.proximityFactorStart, arrow.line.proximityFactorEnd], getDistanceBetweenLines: this.props.distanceBetweenLines, maxParallelOffset: this.props.maxParallelOffset, minParallelOffset: this.props.minParallelOffset, - getDirection: (arrow: Arrow) => { + getDirection: (arrow) => { return getArrowDirection(arrow.line.p1); }, animated: this.props.showLineFlow && this.props.lineFlowMode === LineFlowMode.ANIMATED_ARROWS, visible: this.props.showLineFlow && (!this.props.filteredNominalVoltages || - this.props.filteredNominalVoltages.includes(cData.nominalV)), + this.props.filteredNominalVoltages.includes(compositeData.nominalV)), opacity: this.props.areFlowsValid ? 1 : INVALID_FLOW_OPACITY, updateTriggers: { getLinePositions: linePathUpdateTriggers, @@ -844,20 +691,20 @@ export class LineLayer extends CompositeLayer> { const startFork = new ForkLineLayer( this.getSubLayerProps({ - id: 'LineForkStart' + cData.nominalV, - getSourcePosition: (line: Line) => line.origin, - getTargetPosition: (line: Line) => line.end, - getSubstationOffset: (line: Line) => line.substationIndexStart, - data: cData.lines, + id: 'LineForkStart' + compositeData.nominalV, + getSourcePosition: (line) => line.origin, + getTargetPosition: (line) => line.end, + getSubstationOffset: (line) => line.substationIndexStart, + data: compositeData.lines, widthScale: 20, widthMinPixels: 1, widthMaxPixels: 2, - getColor: (line: Line) => - getLineColor(line, nominalVoltageColor, this.props, this.state.linesConnection.get(line.id)!), + getColor: (line) => + getLineColor(line, nominalVoltageColor, this.props, this.state.linesConnection.get(line.id)), getWidth: 2, - getProximityFactor: (line: Line) => line.proximityFactorStart, - getLineParallelIndex: (line: Line) => line.parallelIndex, - getLineAngle: (line: Line) => line.angleStart, + getProximityFactor: (line) => line.proximityFactorStart, + getLineParallelIndex: (line) => line.parallelIndex, + getLineAngle: (line) => line.angleStart, getDistanceBetweenLines: this.props.distanceBetweenLines, getMaxParallelOffset: this.props.maxParallelOffset, getMinParallelOffset: this.props.minParallelOffset, @@ -866,7 +713,7 @@ export class LineLayer extends CompositeLayer> { getMinSubstationRadiusPixel: this.props.minSubstationRadiusPixel, visible: !this.props.filteredNominalVoltages || - this.props.filteredNominalVoltages.includes(cData.nominalV), + this.props.filteredNominalVoltages.includes(compositeData.nominalV), updateTriggers: { getLineParallelIndex: linePathUpdateTriggers, getSourcePosition: linePathUpdateTriggers, @@ -886,20 +733,20 @@ export class LineLayer extends CompositeLayer> { const endFork = new ForkLineLayer( this.getSubLayerProps({ - id: 'LineForkEnd' + cData.nominalV, - getSourcePosition: (line: Line) => line.end, - getTargetPosition: (line: Line) => line.origin, - getSubstationOffset: (line: Line) => line.substationIndexEnd, - data: cData.lines, + id: 'LineForkEnd' + compositeData.nominalV, + getSourcePosition: (line) => line.end, + getTargetPosition: (line) => line.origin, + getSubstationOffset: (line) => line.substationIndexEnd, + data: compositeData.lines, widthScale: 20, widthMinPixels: 1, widthMaxPixels: 2, - getColor: (line: Line) => - getLineColor(line, nominalVoltageColor, this.props, this.state.linesConnection.get(line.id)!), + getColor: (line) => + getLineColor(line, nominalVoltageColor, this.props, this.state.linesConnection.get(line.id)), getWidth: 2, - getProximityFactor: (line: Line) => line.proximityFactorEnd, - getLineParallelIndex: (line: Line) => -line.parallelIndex!, - getLineAngle: (line: Line) => line.angleEnd! + Math.PI, + getProximityFactor: (line) => line.proximityFactorEnd, + getLineParallelIndex: (line) => -line.parallelIndex, + getLineAngle: (line) => line.angleEnd + Math.PI, getDistanceBetweenLines: this.props.distanceBetweenLines, getMaxParallelOffset: this.props.maxParallelOffset, getMinParallelOffset: this.props.minParallelOffset, @@ -908,7 +755,7 @@ export class LineLayer extends CompositeLayer> { getMinSubstationRadiusPixel: this.props.minSubstationRadiusPixel, visible: !this.props.filteredNominalVoltages || - this.props.filteredNominalVoltages.includes(cData.nominalV), + this.props.filteredNominalVoltages.includes(compositeData.nominalV), updateTriggers: { getLineParallelIndex: [this.props.lineParallelPath], getSourcePosition: linePathUpdateTriggers, @@ -929,24 +776,23 @@ export class LineLayer extends CompositeLayer> { // lines active power const lineActivePowerLabelsLayer = new TextLayer( this.getSubLayerProps({ - id: 'ActivePower' + cData.nominalV, - data: cData.activePower, - getText: (activePower: ActivePower) => - activePower.p !== undefined ? Math.round(activePower.p).toString() : '', + id: 'ActivePower' + compositeData.nominalV, + data: compositeData.activePower, + getText: (activePower) => (activePower.p !== undefined ? Math.round(activePower.p).toString() : ''), // The position passed to this layer causes a bug when zooming and maxParallelOffset is reached: // the label is not correctly positioned on the lines, they are slightly off. // In the custom layers, we clamp the distanceBetweenLines. This is not done in the deck.gl TextLayer // and IconLayer or in the position calculated here. - getPosition: (activePower: ActivePower) => activePower.printPosition, + getPosition: (activePower) => activePower.printPosition, getColor: this.props.labelColor, fontFamily: 'Roboto', getSize: this.props.labelSize, getAngle: 0, - getPixelOffset: (activePower: ActivePower) => activePower.offset.map((x) => x), + getPixelOffset: (activePower) => activePower.offset.map((x) => x), getTextAnchor: 'middle', visible: (!this.props.filteredNominalVoltages || - this.props.filteredNominalVoltages.includes(cData.nominalV)) && + this.props.filteredNominalVoltages.includes(compositeData.nominalV)) && this.props.labelsVisible, opacity: this.props.areFlowsValid ? 1 : INVALID_FLOW_OPACITY, updateTriggers: { @@ -961,20 +807,20 @@ export class LineLayer extends CompositeLayer> { // line status const lineStatusIconLayer = new IconLayer( this.getSubLayerProps({ - id: 'OperatingStatus' + cData.nominalV, - data: cData.operatingStatus, + id: 'OperatingStatus' + compositeData.nominalV, + data: compositeData.operatingStatus, // The position passed to this layer causes a bug when zooming and maxParallelOffset is reached: // the icon is not correctly positioned on the lines, they are slightly off. // In the custom layers, we clamp the distanceBetweenLines. This is not done in the deck.gl TextLayer // and IconLayer or in the position calculated here. - getPosition: (operatingStatus: OperatingStatus) => operatingStatus.printPosition, - getIcon: (operatingStatus: OperatingStatus) => getLineIcon(operatingStatus.status), + getPosition: (operatingStatus) => operatingStatus.printPosition, + getIcon: (operatingStatus) => getLineIcon(operatingStatus.status), getSize: this.props.iconSize, getColor: () => this.props.labelColor, - getPixelOffset: (operatingStatus: OperatingStatus) => operatingStatus.offset, + getPixelOffset: (operatingStatus) => operatingStatus.offset, visible: (!this.props.filteredNominalVoltages || - this.props.filteredNominalVoltages.includes(cData.nominalV)) && + this.props.filteredNominalVoltages.includes(compositeData.nominalV)) && this.props.labelsVisible, updateTriggers: { getPosition: [this.props.lineParallelPath, linePathUpdateTriggers], @@ -990,3 +836,30 @@ export class LineLayer extends CompositeLayer> { return layers; } } + +LineLayer.layerName = 'LineLayer'; + +LineLayer.defaultProps = { + network: null, + geoData: null, + getNominalVoltageColor: { type: 'accessor', value: [255, 255, 255] }, + disconnectedLineColor: { type: 'color', value: [255, 255, 255] }, + filteredNominalVoltages: null, + lineFlowMode: LineFlowMode.FEEDERS, + lineFlowColorMode: LineFlowColorMode.NOMINAL_VOLTAGE, + lineFlowAlertThreshold: 100, + showLineFlow: true, + lineFullPath: true, + lineParallelPath: true, + labelSize: 12, + iconSize: 48, + distanceBetweenLines: 1000, + maxParallelOffset: 100, + minParallelOffset: 3, + substationRadius: { type: 'number', value: SUBSTATION_RADIUS }, + substationMaxPixel: { type: 'number', value: SUBSTATION_RADIUS_MAX_PIXEL }, + minSubstationRadiusPixel: { + type: 'number', + value: SUBSTATION_RADIUS_MIN_PIXEL, + }, +}; diff --git a/src/components/network-map-viewer/network/map-equipments.ts b/src/components/network-map-viewer/network/map-equipments.js similarity index 73% rename from src/components/network-map-viewer/network/map-equipments.ts rename to src/components/network-map-viewer/network/map-equipments.js index fd7ed405..4f0e4a7e 100644 --- a/src/components/network-map-viewer/network/map-equipments.ts +++ b/src/components/network-map-viewer/network/map-equipments.js @@ -5,25 +5,35 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Equipment, EQUIPMENT_TYPES, Line, Substation, VoltageLevel } from '../utils/equipment-types'; +import { EQUIPMENT_TYPES } from '../utils/equipment-types'; -const elementIdIndexer = (map: Map, element: T) => { +const elementIdIndexer = (map, element) => { map.set(element.id, element); return map; }; export class MapEquipments { - substations: Substation[] = []; - substationsById = new Map(); - lines: Line[] = []; - linesById = new Map(); - tieLines: Line[] = []; - tieLinesById = new Map(); - hvdcLines: Line[] = []; - hvdcLinesById = new Map(); - voltageLevels: VoltageLevel[] = []; - voltageLevelsById = new Map(); - nominalVoltages: number[] = []; + substations = []; + + substationsById = new Map(); + + lines = []; + + linesById = new Map(); + + tieLines = []; + + tieLinesById = new Map(); + + hvdcLines = []; + + hvdcLinesById = new Map(); + + voltageLevels = []; + + voltageLevelsById = new Map(); + + nominalVoltages = []; intlRef = undefined; @@ -31,22 +41,22 @@ export class MapEquipments { // dummy constructor, to make children classes constructors happy } - newMapEquipmentForUpdate(): MapEquipments { + newMapEquipmentForUpdate() { /* shallow clone of the map-equipment https://stackoverflow.com/a/44782052 */ return Object.assign(Object.create(Object.getPrototypeOf(this)), this); } - checkAndGetValues(equipments: Equipment[]) { + checkAndGetValues(equipments) { return equipments ? equipments : []; } - completeSubstationsInfos(equipementsToIndex: Substation[]) { + completeSubstationsInfos(equipementsToIndex) { const nominalVoltagesSet = new Set(this.nominalVoltages); if (equipementsToIndex?.length === 0) { this.substationsById = new Map(); this.voltageLevelsById = new Map(); } - const substations: Substation[] = equipementsToIndex?.length > 0 ? equipementsToIndex : this.substations; + const substations = equipementsToIndex?.length > 0 ? equipementsToIndex : this.substations; substations.forEach((substation) => { // sort voltage levels inside substations by nominal voltage @@ -68,7 +78,7 @@ export class MapEquipments { this.nominalVoltages = Array.from(nominalVoltagesSet).sort((a, b) => b - a); } - updateEquipments(currentEquipments: T[], newEquipements: T[]) { + updateEquipments(currentEquipments, newEquipements) { // replace current modified equipments currentEquipments.forEach((equipment1, index) => { const found = newEquipements.filter((equipment2) => equipment2.id === equipment1.id); @@ -85,7 +95,7 @@ export class MapEquipments { return [...currentEquipments, ...eqptsToAdd]; } - updateSubstations(substations: Substation[], fullReload: boolean) { + updateSubstations(substations, fullReload) { if (fullReload) { this.substations = []; } @@ -112,7 +122,7 @@ export class MapEquipments { } }); - if (substationAdded || voltageLevelAdded) { + if (substationAdded === true || voltageLevelAdded === true) { this.substations = [...this.substations]; } @@ -120,7 +130,7 @@ export class MapEquipments { this.completeSubstationsInfos(fullReload ? [] : substations); } - completeLinesInfos(equipementsToIndex: Line[]) { + completeLinesInfos(equipementsToIndex) { if (equipementsToIndex?.length > 0) { equipementsToIndex.forEach((line) => { this.linesById?.set(line.id, line); @@ -130,7 +140,7 @@ export class MapEquipments { } } - completeTieLinesInfos(equipementsToIndex: Line[]) { + completeTieLinesInfos(equipementsToIndex) { if (equipementsToIndex?.length > 0) { equipementsToIndex.forEach((tieLine) => { this.tieLinesById?.set(tieLine.id, tieLine); @@ -140,31 +150,31 @@ export class MapEquipments { } } - updateLines(lines: Line[], fullReload: boolean) { + updateLines(lines, fullReload) { if (fullReload) { this.lines = []; } - this.lines = this.updateEquipments(this.lines, lines); + this.lines = this.updateEquipments(this.lines, lines, EQUIPMENT_TYPES.LINE); this.completeLinesInfos(fullReload ? [] : lines); } - updateTieLines(tieLines: Line[], fullReload: boolean) { + updateTieLines(tieLines, fullReload) { if (fullReload) { this.tieLines = []; } - this.tieLines = this.updateEquipments(this.tieLines, tieLines); + this.tieLines = this.updateEquipments(this.tieLines, tieLines, EQUIPMENT_TYPES.TIE_LINE); this.completeTieLinesInfos(fullReload ? [] : tieLines); } - updateHvdcLines(hvdcLines: Line[], fullReload: boolean) { + updateHvdcLines(hvdcLines, fullReload) { if (fullReload) { this.hvdcLines = []; } - this.hvdcLines = this.updateEquipments(this.hvdcLines, hvdcLines); + this.hvdcLines = this.updateEquipments(this.hvdcLines, hvdcLines, EQUIPMENT_TYPES.HVDC_LINE); this.completeHvdcLinesInfos(fullReload ? [] : hvdcLines); } - completeHvdcLinesInfos(equipementsToIndex: Line[]) { + completeHvdcLinesInfos(equipementsToIndex) { if (equipementsToIndex?.length > 0) { equipementsToIndex.forEach((hvdcLine) => { this.hvdcLinesById?.set(hvdcLine.id, hvdcLine); @@ -174,16 +184,16 @@ export class MapEquipments { } } - removeBranchesOfVoltageLevel(branchesList: Line[], voltageLevelId: string) { + removeBranchesOfVoltageLevel(branchesList, voltageLevelId) { const remainingLines = branchesList.filter( (l) => l.voltageLevelId1 !== voltageLevelId && l.voltageLevelId2 !== voltageLevelId ); - branchesList.filter((l) => !remainingLines.includes(l)).forEach((l) => this.linesById.delete(l.id)); + branchesList.filter((l) => !remainingLines.includes(l)).map((l) => this.linesById.delete(l.id)); return remainingLines; } - removeEquipment(equipmentType: EQUIPMENT_TYPES, equipmentId: string) { + removeEquipment(equipmentType, equipmentId) { switch (equipmentType) { case EQUIPMENT_TYPES.LINE: { this.lines = this.lines.filter((l) => l.id !== equipmentId); @@ -191,15 +201,11 @@ export class MapEquipments { break; } case EQUIPMENT_TYPES.VOLTAGE_LEVEL: { - const substationId = this.voltageLevelsById.get(equipmentId)?.substationId; - if (substationId === undefined) { - return; - } - const substation = this.substationsById.get(substationId); - if (substation == null) { - return; - } - substation.voltageLevels = substation.voltageLevels.filter((l) => l.id !== equipmentId); + const substationId = this.voltageLevelsById.get(equipmentId).substationId; + let voltageLevelsOfSubstation = this.substationsById.get(substationId).voltageLevels; + voltageLevelsOfSubstation = voltageLevelsOfSubstation.filter((l) => l.id !== equipmentId); + this.substationsById.get(substationId).voltageLevels = voltageLevelsOfSubstation; + this.removeBranchesOfVoltageLevel(this.lines, equipmentId); //New reference on substations to trigger reload of NetworkExplorer and NetworkMap this.substations = [...this.substations]; @@ -209,10 +215,7 @@ export class MapEquipments { this.substations = this.substations.filter((l) => l.id !== equipmentId); const substation = this.substationsById.get(equipmentId); - if (substation === undefined) { - return; - } - substation.voltageLevels.forEach((vl) => this.removeEquipment(EQUIPMENT_TYPES.VOLTAGE_LEVEL, vl.id)); + substation.voltageLevels.map((vl) => this.removeEquipment(EQUIPMENT_TYPES.VOLTAGE_LEVEL, vl.id)); this.completeSubstationsInfos([substation]); break; } @@ -224,7 +227,7 @@ export class MapEquipments { return this.voltageLevels; } - getVoltageLevel(id: string) { + getVoltageLevel(id) { return this.voltageLevelsById.get(id); } @@ -232,7 +235,7 @@ export class MapEquipments { return this.substations; } - getSubstation(id: string) { + getSubstation(id) { return this.substationsById.get(id); } @@ -244,7 +247,7 @@ export class MapEquipments { return this.lines; } - getLine(id: string) { + getLine(id) { return this.linesById.get(id); } @@ -252,7 +255,7 @@ export class MapEquipments { return this.hvdcLines; } - getHvdcLine(id: string) { + getHvdcLine(id) { return this.hvdcLinesById.get(id); } @@ -260,7 +263,7 @@ export class MapEquipments { return this.tieLines; } - getTieLine(id: string) { + getTieLine(id) { return this.tieLinesById.get(id); } } diff --git a/src/components/network-map-viewer/network/network-map.jsx b/src/components/network-map-viewer/network/network-map.jsx new file mode 100644 index 00000000..fd1db690 --- /dev/null +++ b/src/components/network-map-viewer/network/network-map.jsx @@ -0,0 +1,752 @@ +/** + * Copyright (c) 2020, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import PropTypes from 'prop-types'; +import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { Box, decomposeColor } from '@mui/system'; +import { MapboxOverlay } from '@deck.gl/mapbox'; +import { Replay } from '@mui/icons-material'; +import { Button, useTheme } from '@mui/material'; +import { FormattedMessage } from 'react-intl'; +import { Map, NavigationControl, useControl } from 'react-map-gl'; +import { getNominalVoltageColor } from '../../../utils/colors'; +import { useNameOrId } from '../utils/equipmentInfosHandler'; +import { GeoData } from './geo-data'; +import DrawControl, { getMapDrawer } from './draw-control'; +import { LineFlowColorMode, LineFlowMode, LineLayer } from './line-layer'; +import { MapEquipments } from './map-equipments'; +import { SubstationLayer } from './substation-layer'; +import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; +import LoaderWithOverlay from '../utils/loader-with-overlay'; +import mapboxgl from 'mapbox-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import maplibregl from 'maplibre-gl'; +import 'maplibre-gl/dist/maplibre-gl.css'; +import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; +import { EQUIPMENT_TYPES } from '../utils/equipment-types'; + +// MouseEvent.button https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button +const MOUSE_EVENT_BUTTON_LEFT = 0; +const MOUSE_EVENT_BUTTON_RIGHT = 2; + +/** + * Represents the draw event types for the network map. + * when a draw event is triggered, the event type is passed to the onDrawEvent callback + * On create, when the user create a new polygon (shape finished) + */ +export const DRAW_EVENT = { + CREATE: 1, + UPDATE: 2, + DELETE: 0, +}; + +// Small boilerplate recommended by deckgl, to bridge to a react-map-gl control declaratively +// see https://deck.gl/docs/api-reference/mapbox/mapbox-overlay#using-with-react-map-gl +const DeckGLOverlay = forwardRef((props, ref) => { + const overlay = useControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + useImperativeHandle(ref, () => overlay, [overlay]); + return null; +}); + +const PICKING_RADIUS = 5; + +const CARTO = 'carto'; +const CARTO_NOLABEL = 'cartonolabel'; +const MAPBOX = 'mapbox'; + +const LIGHT = 'light'; +const DARK = 'dark'; + +const styles = { + mapManualRefreshBackdrop: { + width: '100%', + height: '100%', + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: 'grey', + opacity: '0.8', + zIndex: 99, + fontSize: 30, + }, +}; + +const FALLBACK_MAPBOX_TOKEN = + 'pk.eyJ1IjoiZ2VvZmphbWciLCJhIjoiY2pwbnRwcm8wMDYzMDQ4b2pieXd0bDMxNSJ9.Q4aL20nBo5CzGkrWtxroug'; + +const SUBSTATION_LAYER_PREFIX = 'substationLayer'; +const LINE_LAYER_PREFIX = 'lineLayer'; +const LABEL_SIZE = 12; +const INITIAL_CENTERED = { + lastCenteredSubstation: null, + centeredSubstationId: null, + centered: false, +}; +const DEFAULT_LOCATE_SUBSTATION_ZOOM_LEVEL = 12; + +// get polygon coordinates (features) or an empty object +function getPolygonFeatures() { + return getMapDrawer()?.getAll()?.features[0] ?? {}; +} + +const NetworkMap = forwardRef((props, ref) => { + const [labelsVisible, setLabelsVisible] = useState(false); + const [showLineFlow, setShowLineFlow] = useState(true); + const [showTooltip, setShowTooltip] = useState(true); + const mapRef = useRef(); + const deckRef = useRef(); + const [centered, setCentered] = useState(INITIAL_CENTERED); + const lastViewStateRef = useRef(null); + const [tooltip, setTooltip] = useState({}); + const theme = useTheme(); + const foregroundNeutralColor = useMemo(() => { + const labelColor = decomposeColor(theme.palette.text.primary).values; + labelColor[3] *= 255; + return labelColor; + }, [theme]); + const [cursorType, setCursorType] = useState('grab'); + const [isDragging, setDragging] = useState(false); + + //NOTE these constants are moved to the component's parameters list + //const currentNode = useSelector((state) => state.currentTreeNode); + const { onPolygonChanged, centerOnSubstation, onDrawEvent, shouldDisableToolTip } = props; + + const { getNameOrId } = useNameOrId(props.useName); + + const readyToDisplay = props.mapEquipments !== null && props.geoData !== null && !props.disabled; + + const readyToDisplaySubstations = + readyToDisplay && props.mapEquipments.substations && props.geoData.substationPositionsById.size > 0; + + const readyToDisplayLines = + readyToDisplay && + (props.mapEquipments?.lines || props.mapEquipments?.hvdcLines || props.mapEquipments?.tieLines) && + props.mapEquipments.voltageLevels && + props.geoData.substationPositionsById.size > 0; + + const mapEquipmentsLines = useMemo(() => { + return [ + ...(props.mapEquipments?.lines.map((line) => ({ + ...line, + equipmentType: EQUIPMENT_TYPES.LINE, + })) ?? []), + ...(props.mapEquipments?.tieLines.map((tieLine) => ({ + ...tieLine, + equipmentType: EQUIPMENT_TYPES.TIE_LINE, + })) ?? []), + ...(props.mapEquipments?.hvdcLines.map((hvdcLine) => ({ + ...hvdcLine, + equipmentType: EQUIPMENT_TYPES.HVDC_LINE, + })) ?? []), + ]; + }, [props.mapEquipments?.hvdcLines, props.mapEquipments?.tieLines, props.mapEquipments?.lines]); + + const divRef = useRef(); + + const mToken = !props.mapBoxToken ? FALLBACK_MAPBOX_TOKEN : props.mapBoxToken; + + useEffect(() => { + if (centerOnSubstation === null) { + return; + } + setCentered({ + lastCenteredSubstation: null, + centeredSubstationId: centerOnSubstation?.to, + centered: true, + }); + }, [centerOnSubstation]); + + // TODO simplify this, now we use Map as the camera controlling component + // so we don't need the deckgl ref anymore. The following comments are + // probably outdated, cleanup everything: + // Do this in onAfterRender because when doing it in useEffect (triggered by calling setDeck()), + // it doesn't work in the case of using the browser backward/forward buttons (because in this particular case, + // we get the ref to the deck and it has not yet initialized..) + function onAfterRender() { + // TODO outdated comment + //use centered and deck to execute this block only once when the data is ready and deckgl is initialized + //TODO, replace the next lines with setProps( { initialViewState } ) when we upgrade to 8.1.0 + //see https://github.com/uber/deck.gl/pull/4038 + //This is a hack because it accesses the properties of deck directly but for now it works + if ( + (!centered.centered || + (centered.centeredSubstationId && centered.centeredSubstationId !== centered.lastCenteredSubstation)) && + props.geoData !== null + ) { + if (props.geoData.substationPositionsById.size > 0) { + if (centered.centeredSubstationId) { + const geodata = props.geoData.substationPositionsById.get(centered.centeredSubstationId); + if (!geodata) { + return; + } // can't center on substation if no coordinate. + mapRef.current?.flyTo({ + center: [geodata.lon, geodata.lat], + duration: 2000, + // only zoom if the current zoom is smaller than the new one + zoom: Math.max(mapRef.current?.getZoom(), props.locateSubStationZoomLevel), + essential: true, + }); + setCentered({ + lastCenteredSubstation: centered.centeredSubstationId, + centeredSubstationId: centered.centeredSubstationId, + centered: true, + }); + } else { + const coords = Array.from(props.geoData.substationPositionsById.entries()).map((x) => x[1]); + const maxlon = Math.max.apply( + null, + coords.map((x) => x.lon) + ); + const minlon = Math.min.apply( + null, + coords.map((x) => x.lon) + ); + const maxlat = Math.max.apply( + null, + coords.map((x) => x.lat) + ); + const minlat = Math.min.apply( + null, + coords.map((x) => x.lat) + ); + const marginlon = (maxlon - minlon) / 10; + const marginlat = (maxlat - minlat) / 10; + mapRef.current?.fitBounds( + [ + [minlon - marginlon / 2, minlat - marginlat / 2], + [maxlon + marginlon / 2, maxlat + marginlat / 2], + ], + { animate: false } + ); + setCentered({ + lastCenteredSubstation: null, + centered: true, + }); + } + } + } + } + + function onViewStateChange(info) { + lastViewStateRef.current = info.viewState; + if ( + !info.interactionState || // first event of before an animation (e.g. clicking the +/- buttons of the navigation controls, gives the target + (info.interactionState && !info.interactionState.inTransition) // Any event not part of a animation (mouse panning or zooming) + ) { + if (info.viewState.zoom >= props.labelsZoomThreshold && !labelsVisible) { + setLabelsVisible(true); + } else if (info.viewState.zoom < props.labelsZoomThreshold && labelsVisible) { + setLabelsVisible(false); + } + setShowTooltip(info.viewState.zoom >= props.tooltipZoomThreshold); + setShowLineFlow(info.viewState.zoom >= props.arrowsZoomThreshold); + } + } + + function renderTooltip() { + return ( + tooltip && + tooltip.visible && + !shouldDisableToolTip && + //As of now only LINE tooltip is implemented, the following condition is to be removed or tweaked once other types of line tooltip are implemented + tooltip.equipmentType === EQUIPMENT_TYPES.LINE && ( +
+ {props.renderPopover(tooltip.equipmentId, divRef.current)} +
+ ) + ); + } + + function onClickHandler(info, event, network) { + const leftButton = event.originalEvent.button === MOUSE_EVENT_BUTTON_LEFT; + const rightButton = event.originalEvent.button === MOUSE_EVENT_BUTTON_RIGHT; + if ( + info.layer && + info.layer.id.startsWith(SUBSTATION_LAYER_PREFIX) && + info.object && + (info.object.substationId || info.object.voltageLevels) // is a voltage level marker, or a substation text + ) { + let idVl; + let idSubstation; + if (info.object.substationId) { + idVl = info.object.id; + } else if (info.object.voltageLevels) { + if (info.object.voltageLevels.length === 1) { + let idS = info.object.voltageLevels[0].substationId; + let substation = network.getSubstation(idS); + if (substation && substation.voltageLevels.length > 1) { + idSubstation = idS; + } else { + idVl = info.object.voltageLevels[0].id; + } + } else { + idSubstation = info.object.voltageLevels[0].substationId; + } + } + if (idVl !== undefined) { + if (props.onSubstationClick && leftButton) { + props.onSubstationClick(idVl); + } else if (props.onVoltageLevelMenuClick && rightButton) { + props.onVoltageLevelMenuClick( + network.getVoltageLevel(idVl), + event.originalEvent.x, + event.originalEvent.y + ); + } + } + if (idSubstation !== undefined) { + if (props.onSubstationClickChooseVoltageLevel && leftButton) { + props.onSubstationClickChooseVoltageLevel( + idSubstation, + event.originalEvent.x, + event.originalEvent.y + ); + } else if (props.onSubstationMenuClick && rightButton) { + props.onSubstationMenuClick( + network.getSubstation(idSubstation), + event.originalEvent.x, + event.originalEvent.y + ); + } + } + } + if ( + rightButton && + info.layer && + info.layer.id.startsWith(LINE_LAYER_PREFIX) && + info.object && + info.object.id && + info.object.voltageLevelId1 && + info.object.voltageLevelId2 + ) { + // picked line properties are retrieved from network data and not from pickable object infos, + // because pickable object infos might not be up to date + const line = network.getLine(info.object.id); + const tieLine = network.getTieLine(info.object.id); + const hvdcLine = network.getHvdcLine(info.object.id); + + const equipment = line || tieLine || hvdcLine; + if (equipment) { + const menuClickFunction = + equipment === line + ? props.onLineMenuClick + : equipment === tieLine + ? props.onTieLineMenuClick + : props.onHvdcLineMenuClick; + + menuClickFunction(equipment, event.originalEvent.x, event.originalEvent.y); + } + } + } + + function onMapContextMenu(event) { + const info = + deckRef.current && + deckRef.current.pickObject({ + x: event.point.x, + y: event.point.y, + radius: PICKING_RADIUS, + }); + info && onClickHandler(info, event, props.mapEquipments); + } + + function cursorHandler() { + return isDragging ? 'grabbing' : cursorType; + } + + const layers = []; + + if (readyToDisplaySubstations) { + layers.push( + new SubstationLayer({ + id: SUBSTATION_LAYER_PREFIX, + data: props.mapEquipments?.substations, + network: props.mapEquipments, + geoData: props.geoData, + getNominalVoltageColor: getNominalVoltageColor, + filteredNominalVoltages: props.filteredNominalVoltages, + labelsVisible: labelsVisible, + labelColor: foregroundNeutralColor, + labelSize: LABEL_SIZE, + pickable: true, + onHover: ({ object }) => { + setCursorType(object ? 'pointer' : 'grab'); + }, + getNameOrId: getNameOrId, + }) + ); + } + + if (readyToDisplayLines) { + layers.push( + new LineLayer({ + areFlowsValid: props.areFlowsValid, + id: LINE_LAYER_PREFIX, + data: mapEquipmentsLines, + network: props.mapEquipments, + updatedLines: props.updatedLines, + geoData: props.geoData, + getNominalVoltageColor: getNominalVoltageColor, + disconnectedLineColor: foregroundNeutralColor, + filteredNominalVoltages: props.filteredNominalVoltages, + lineFlowMode: props.lineFlowMode, + showLineFlow: props.visible && showLineFlow, + lineFlowColorMode: props.lineFlowColorMode, + lineFlowAlertThreshold: props.lineFlowAlertThreshold, + lineFullPath: props.geoData.linePositionsById.size > 0 && props.lineFullPath, + lineParallelPath: props.lineParallelPath, + labelsVisible: labelsVisible, + labelColor: foregroundNeutralColor, + labelSize: LABEL_SIZE, + pickable: true, + onHover: ({ object, x, y }) => { + if (object) { + setCursorType('pointer'); + const lineObject = object?.line ?? object; + setTooltip({ + equipmentId: lineObject?.id, + equipmentType: lineObject?.equipmentType, + pointerX: x, + pointerY: y, + visible: showTooltip, + }); + } else { + setCursorType('grab'); + setTooltip(null); + } + }, + }) + ); + } + + const initialViewState = { + longitude: props.initialPosition[0], + latitude: props.initialPosition[1], + zoom: props.initialZoom, + maxZoom: 14, + pitch: 0, + bearing: 0, + }; + + const renderOverlay = () => ( + + ); + + useEffect(() => { + mapRef.current?.resize(); + }, [props.triggerMapResizeOnChange]); + + const getMapStyle = (mapLibrary, mapTheme) => { + switch (mapLibrary) { + case MAPBOX: + if (mapTheme === LIGHT) { + return 'mapbox://styles/mapbox/light-v9'; + } else { + return 'mapbox://styles/mapbox/dark-v9'; + } + case CARTO: + if (mapTheme === LIGHT) { + return 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'; + } else { + return 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'; + } + case CARTO_NOLABEL: + if (mapTheme === LIGHT) { + return 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json'; + } else { + return 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json'; + } + default: + return 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'; + } + }; + + const mapStyle = useMemo(() => getMapStyle(props.mapLibrary, props.mapTheme), [props.mapLibrary, props.mapTheme]); + + const mapLib = + props.mapLibrary === MAPBOX + ? mToken && { + key: 'mapboxgl', + mapLib: mapboxgl, + mapboxAccessToken: mToken, + } + : { + key: 'maplibregl', + mapLib: maplibregl, + }; + + // because the mapLib prop of react-map-gl is not reactive, we need to + // unmount/mount the Map with 'key', so we need also to reset all state + // associated with uncontrolled state of the map + useEffect(() => { + setCentered(INITIAL_CENTERED); + }, [mapLib?.key]); + + const onUpdate = useCallback(() => { + onPolygonChanged(getPolygonFeatures()); + onDrawEvent(DRAW_EVENT.UPDATE); + }, [onDrawEvent, onPolygonChanged]); + + const onCreate = useCallback(() => { + onPolygonChanged(getPolygonFeatures()); + onDrawEvent(DRAW_EVENT.CREATE); + }, [onDrawEvent, onPolygonChanged]); + const getSelectedLines = useCallback(() => { + const polygonFeatures = getPolygonFeatures(); + const polygonCoordinates = polygonFeatures?.geometry; + if (!polygonCoordinates || polygonCoordinates.coordinates < 3) { + return []; + } + //for each line, check if it is in the polygon + const selectedLines = getSelectedLinesInPolygon( + props.mapEquipments, + mapEquipmentsLines, + props.geoData, + polygonCoordinates + ); + return selectedLines.filter((line) => { + return props.filteredNominalVoltages.some((nv) => { + return ( + nv === props.mapEquipments.getVoltageLevel(line.voltageLevelId1).nominalV || + nv === props.mapEquipments.getVoltageLevel(line.voltageLevelId2).nominalV + ); + }); + }); + }, [props.mapEquipments, mapEquipmentsLines, props.geoData, props.filteredNominalVoltages]); + + const getSelectedSubstations = useCallback(() => { + const substations = getSubstationsInPolygon(getPolygonFeatures(), props.mapEquipments, props.geoData); + return ( + substations.filter((substation) => { + return substation.voltageLevels.some((vl) => props.filteredNominalVoltages.includes(vl.nominalV)); + }) ?? [] + ); + }, [props.mapEquipments, props.geoData, props.filteredNominalVoltages]); + + useImperativeHandle( + ref, + () => ({ + getSelectedSubstations, + getSelectedLines, + cleanDraw() { + //because deleteAll does not trigger a update of the polygonFeature callback + getMapDrawer()?.deleteAll(); + onPolygonChanged(getPolygonFeatures()); + onDrawEvent(DRAW_EVENT.DELETE); + }, + getMapDrawer, + }), + [onPolygonChanged, getSelectedSubstations, getSelectedLines, onDrawEvent] + ); + + const onDelete = useCallback(() => { + onPolygonChanged(getPolygonFeatures()); + onDrawEvent(DRAW_EVENT.DELETE); + }, [onPolygonChanged, onDrawEvent]); + + return ( + mapLib && ( + setDragging(true)} + onDragEnd={() => setDragging(false)} + onContextMenu={onMapContextMenu} + > + {props.displayOverlayLoader && renderOverlay()} + {props.isManualRefreshBackdropDisplayed && ( + + + + )} + { + onClickHandler(info, event.srcEvent, props.mapEquipments); + }} + onAfterRender={onAfterRender} // TODO simplify this + layers={layers} + pickingRadius={PICKING_RADIUS} + /> + {showTooltip && renderTooltip()} + {/* visualizePitch true makes the compass reset the pitch when clicked in addition to visualizing it */} + + { + props.onDrawPolygonModeActive(polygon_draw); + }} + onCreate={onCreate} + onUpdate={onUpdate} + onDelete={onDelete} + /> + + ) + ); +}); + +NetworkMap.defaultProps = { + areFlowsValid: true, + arrowsZoomThreshold: 7, + centerOnSubstation: null, + disabled: false, + displayOverlayLoader: false, + filteredNominalVoltages: null, + geoData: null, + initialPosition: [0, 0], + initialZoom: 5, + isManualRefreshBackdropDisplayed: false, + labelsZoomThreshold: 9, + lineFlowAlertThreshold: 100, + lineFlowColorMode: LineFlowColorMode.NOMINAL_VOLTAGE, + lineFlowHidden: true, + lineFlowMode: LineFlowMode.FEEDERS, + lineFullPath: true, + lineParallelPath: true, + mapBoxToken: null, + mapEquipments: null, + mapLibrary: CARTO, + tooltipZoomThreshold: 7, + mapTheme: DARK, + updatedLines: [], + useName: true, + visible: true, + shouldDisableToolTip: false, + locateSubStationZoomLevel: DEFAULT_LOCATE_SUBSTATION_ZOOM_LEVEL, + + onSubstationClick: () => {}, + onSubstationClickChooseVoltageLevel: () => {}, + onSubstationMenuClick: () => {}, + onVoltageLevelMenuClick: () => {}, + onLineMenuClick: () => {}, + onTieLineMenuClick: () => {}, + onHvdcLineMenuClick: () => {}, + onManualRefreshClick: () => {}, + renderPopover: (eId) => { + return eId; + }, + onDrawPolygonModeActive: () => {}, + onPolygonChanged: () => {}, + onDrawEvent: () => {}, +}; + +NetworkMap.propTypes = { + disabled: PropTypes.bool, + geoData: PropTypes.instanceOf(GeoData), + mapBoxToken: PropTypes.string, + mapEquipments: PropTypes.instanceOf(MapEquipments), + mapLibrary: PropTypes.oneOf([CARTO, CARTO_NOLABEL, MAPBOX]), + mapTheme: PropTypes.oneOf([LIGHT, DARK]), + + areFlowsValid: PropTypes.bool, + arrowsZoomThreshold: PropTypes.number, + centerOnSubstation: PropTypes.any, + displayOverlayLoader: PropTypes.bool, + filteredNominalVoltages: PropTypes.array, + initialPosition: PropTypes.arrayOf(PropTypes.number), + initialZoom: PropTypes.number, + isManualRefreshBackdropDisplayed: PropTypes.bool, + labelsZoomThreshold: PropTypes.number, + lineFlowAlertThreshold: PropTypes.number, + lineFlowColorMode: PropTypes.oneOf(Object.values(LineFlowColorMode)), + lineFlowHidden: PropTypes.bool, + lineFlowMode: PropTypes.oneOf(Object.values(LineFlowMode)), + lineFullPath: PropTypes.bool, + lineParallelPath: PropTypes.bool, + renderPopover: PropTypes.func, + tooltipZoomThreshold: PropTypes.number, + // With mapboxgl v2 (not a problem with maplibre), we need to call + // map.resize() when the parent size has changed, otherwise the map is not + // redrawn. It seems like this is autodetected when the browser window is + // resized, but not for programmatic resizes of the parent. For now in our + // app, only study display mode resizes programmatically + // use this prop to make the map resize when needed, each time this prop changes, map.resize() is trigged + triggerMapResizeOnChange: PropTypes.any, + updatedLines: PropTypes.array, + useName: PropTypes.bool, + visible: PropTypes.bool, + shouldDisableToolTip: PropTypes.bool, + locateSubStationZoomLevel: PropTypes.number, + onHvdcLineMenuClick: PropTypes.func, + onLineMenuClick: PropTypes.func, + onTieLineMenuClick: PropTypes.func, + onManualRefreshClick: PropTypes.func, + onSubstationClick: PropTypes.func, + onSubstationClickChooseVoltageLevel: PropTypes.func, + onSubstationMenuClick: PropTypes.func, + onVoltageLevelMenuClick: PropTypes.func, + onDrawPolygonModeActive: PropTypes.func, + onPolygonChanged: PropTypes.func, + onDrawEvent: PropTypes.func, +}; + +export default memo(NetworkMap); + +function getSubstationsInPolygon(features, mapEquipments, geoData) { + const polygonCoordinates = features?.geometry; + if (!polygonCoordinates || polygonCoordinates.coordinates < 3) { + return []; + } + //get the list of substation + const substationsList = mapEquipments?.substations ?? []; + //for each substation, check if it is in the polygon + return substationsList // keep only the sybstation in the polygon + .filter((substation) => { + const pos = geoData.getSubstationPosition(substation.id); + return booleanPointInPolygon(pos, polygonCoordinates); + }); +} + +function getSelectedLinesInPolygon(network, lines, geoData, polygonCoordinates) { + return lines.filter((line) => { + try { + const linePos = geoData.getLinePositions(network, line); + if (!linePos) { + return false; + } + if (linePos.length < 2) { + return false; + } + const extremities = [linePos[0], linePos[linePos.length - 1]]; + return extremities.some((pos) => booleanPointInPolygon(pos, polygonCoordinates)); + } catch (error) { + console.error(error); + return false; + } + }); +} diff --git a/src/components/network-map-viewer/network/network-map.tsx b/src/components/network-map-viewer/network/network-map.tsx deleted file mode 100644 index de5f981d..00000000 --- a/src/components/network-map-viewer/network/network-map.tsx +++ /dev/null @@ -1,838 +0,0 @@ -/** - * Copyright (c) 2020, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { - forwardRef, - memo, - ReactNode, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react'; -import { Box, decomposeColor } from '@mui/system'; -import { MapboxOverlay } from '@deck.gl/mapbox'; -import { Replay } from '@mui/icons-material'; -import { Button, ButtonProps, useTheme } from '@mui/material'; -import { FormattedMessage } from 'react-intl'; -import { Map, MapLib, MapRef, NavigationControl, useControl, ViewState, ViewStateChangeEvent } from 'react-map-gl'; -import { getNominalVoltageColor } from '../../../utils/colors'; -import { useNameOrId } from '../utils/equipmentInfosHandler'; -import { GeoData } from './geo-data'; -import DrawControl, { DRAW_MODES, DrawControlProps, getMapDrawer } from './draw-control'; -import { LineFlowColorMode, LineFlowMode, LineLayer, LineLayerProps } from './line-layer'; -import { MapEquipments } from './map-equipments'; -import { SubstationLayer } from './substation-layer'; -import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; -import LoaderWithOverlay from '../utils/loader-with-overlay'; -import mapboxgl from 'mapbox-gl'; -import 'mapbox-gl/dist/mapbox-gl.css'; -import maplibregl from 'maplibre-gl'; -import 'maplibre-gl/dist/maplibre-gl.css'; -import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; -import { Feature, Polygon } from 'geojson'; -import { - EquimentLine, - Equipment, - EQUIPMENT_TYPES, - HvdcLineEquimentLine, - isLine, - isSubstation, - isVoltageLevel, - Line, - LineEquimentLine, - Substation, - TieLineEquimentLine, - VoltageLevel, -} from '../utils/equipment-types'; -import { PickingInfo } from 'deck.gl'; - -// MouseEvent.button https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button -const MOUSE_EVENT_BUTTON_LEFT = 0; -const MOUSE_EVENT_BUTTON_RIGHT = 2; - -/** - * Represents the draw event types for the network map. - * when a draw event is triggered, the event type is passed to the onDrawEvent callback - * On create, when the user create a new polygon (shape finished) - */ -export enum DRAW_EVENT { - CREATE = 1, - UPDATE = 2, - DELETE = 0, -} - -export type MenuClickFunction = (equipment: T, eventX: number, eventY: number) => void; - -// Small boilerplate recommended by deckgl, to bridge to a react-map-gl control declaratively -// see https://deck.gl/docs/api-reference/mapbox/mapbox-overlay#using-with-react-map-gl -const DeckGLOverlay = forwardRef((props, ref) => { - const overlay = useControl(() => new MapboxOverlay(props)); - overlay.setProps(props); - useImperativeHandle(ref, () => overlay, [overlay]); - return null; -}); - -type TooltipType = { - equipmentId: string; - equipmentType: string; - pointerX: number; - pointerY: number; - visible: boolean; -}; - -const PICKING_RADIUS = 5; - -const CARTO = 'carto'; -const CARTO_NOLABEL = 'cartonolabel'; -const MAPBOX = 'mapbox'; -type MapLibrary = typeof CARTO | typeof CARTO_NOLABEL | typeof MAPBOX; - -const LIGHT = 'light'; -const DARK = 'dark'; -type MapTheme = typeof LIGHT | typeof DARK; - -const styles = { - mapManualRefreshBackdrop: { - width: '100%', - height: '100%', - textAlign: 'center', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - background: 'grey', - opacity: '0.8', - zIndex: 99, - fontSize: 30, - }, -}; - -const FALLBACK_MAPBOX_TOKEN = - 'pk.eyJ1IjoiZ2VvZmphbWciLCJhIjoiY2pwbnRwcm8wMDYzMDQ4b2pieXd0bDMxNSJ9.Q4aL20nBo5CzGkrWtxroug'; - -const SUBSTATION_LAYER_PREFIX = 'substationLayer'; -const LINE_LAYER_PREFIX = 'lineLayer'; -const LABEL_SIZE = 12; - -type Centered = { - lastCenteredSubstation: string | null; - centeredSubstationId?: string | null; - centered: boolean; -}; - -const INITIAL_CENTERED = { - lastCenteredSubstation: null, - centeredSubstationId: null, - centered: false, -} satisfies Centered; - -const DEFAULT_LOCATE_SUBSTATION_ZOOM_LEVEL = 12; - -// get polygon coordinates (features) or an empty object -function getPolygonFeatures(): Feature | Record { - return getMapDrawer()?.getAll()?.features[0] ?? {}; -} - -type NetworkMapProps = { - disabled?: boolean; - geoData?: GeoData | null; - mapBoxToken?: string | null; - mapEquipments?: MapEquipments | null; - mapLibrary?: 'carto' | 'cartonolabel' | 'mapbox'; - mapTheme?: 'light' | 'dark'; - areFlowsValid?: boolean; - arrowsZoomThreshold?: number; - centerOnSubstation?: { to: string } | null; - displayOverlayLoader?: boolean; - filteredNominalVoltages?: number[] | null; - initialPosition?: [number, number]; - initialZoom?: number; - isManualRefreshBackdropDisplayed?: boolean; - labelsZoomThreshold?: number; - lineFlowAlertThreshold?: number; - lineFlowColorMode?: LineFlowColorMode; - lineFlowHidden?: boolean; - lineFlowMode?: LineFlowMode; - lineFullPath?: boolean; - lineParallelPath?: boolean; - renderPopover?: (equipmentId: string, divRef: HTMLDivElement | null) => ReactNode; - tooltipZoomThreshold?: number; - // With mapboxgl v2 (not a problem with maplibre), we need to call - // map.resize() when the parent size has changed, otherwise the map is not - // redrawn. It seems like this is autodetected when the browser window is - // resized, but not for programmatic resizes of the parent. For now in our - // app, only study display mode resizes programmatically - // use this prop to make the map resize when needed, each time this prop changes, map.resize() is trigged - triggerMapResizeOnChange?: unknown; - updatedLines?: LineLayerProps['updatedLines']; - useName?: boolean; - visible?: boolean; - shouldDisableToolTip?: boolean; - locateSubStationZoomLevel?: number; - onHvdcLineMenuClick?: MenuClickFunction; - onLineMenuClick?: MenuClickFunction; - onTieLineMenuClick?: MenuClickFunction; - onManualRefreshClick?: ButtonProps['onClick']; - onSubstationClick?: (idVoltageLevel: string) => void; - onSubstationClickChooseVoltageLevel?: (idSubstation: string, eventX: number, eventY: number) => void; - onSubstationMenuClick?: MenuClickFunction; - onVoltageLevelMenuClick?: MenuClickFunction; - onDrawPolygonModeActive?: DrawControlProps['onDrawPolygonModeActive']; - onPolygonChanged?: (polygoneFeature: Feature | Record) => void; - onDrawEvent?: (drawEvent: DRAW_EVENT) => void; -}; - -export type NetworkMapRef = { - getSelectedSubstations: () => Substation[]; - getSelectedLines: () => Line[]; - cleanDraw: () => void; - getMapDrawer: () => MapboxDraw | undefined; -}; - -const NetworkMap = forwardRef( - ( - { - areFlowsValid = true, - arrowsZoomThreshold = 7, - centerOnSubstation = null, - disabled = false, - displayOverlayLoader = false, - filteredNominalVoltages = null, - geoData = null, - initialPosition = [0, 0], - initialZoom = 5, - isManualRefreshBackdropDisplayed = false, - labelsZoomThreshold = 9, - lineFlowAlertThreshold = 100, - lineFlowColorMode = LineFlowColorMode.NOMINAL_VOLTAGE, - // lineFlowHidden = true, - lineFlowMode = LineFlowMode.FEEDERS, - lineFullPath = true, - lineParallelPath = true, - mapBoxToken = null, - mapEquipments = null, - mapLibrary = CARTO, - tooltipZoomThreshold = 7, - mapTheme = DARK, - triggerMapResizeOnChange = false, - updatedLines = [], - useName = true, - visible = true, - shouldDisableToolTip = false, - locateSubStationZoomLevel = DEFAULT_LOCATE_SUBSTATION_ZOOM_LEVEL, - onSubstationClick = () => {}, - onSubstationClickChooseVoltageLevel = () => {}, - onSubstationMenuClick = () => {}, - onVoltageLevelMenuClick = () => {}, - onLineMenuClick = () => {}, - onTieLineMenuClick = () => {}, - onHvdcLineMenuClick = () => {}, - onManualRefreshClick = () => {}, - renderPopover = (eId) => { - return eId; - }, - onDrawPolygonModeActive = (active: DRAW_MODES) => { - console.log('polygon drawing mode active: ', active ? 'active' : 'inactive'); - }, - onPolygonChanged = () => {}, - onDrawEvent = () => {}, - }, - ref - ) => { - const [labelsVisible, setLabelsVisible] = useState(false); - const [showLineFlow, setShowLineFlow] = useState(true); - const [showTooltip, setShowTooltip] = useState(true); - const mapRef = useRef(null); - const deckRef = useRef(); - const [centered, setCentered] = useState(INITIAL_CENTERED); - const lastViewStateRef = useRef(); - const [tooltip, setTooltip] = useState | null>({}); - const theme = useTheme(); - const foregroundNeutralColor = useMemo(() => { - const labelColor = decomposeColor(theme.palette.text.primary).values as [number, number, number, number]; - labelColor[3] *= 255; - return labelColor; - }, [theme]); - const [cursorType, setCursorType] = useState('grab'); - const [isDragging, setDragging] = useState(false); - - const { getNameOrId } = useNameOrId(useName); - - const readyToDisplay = mapEquipments !== null && geoData !== null && !disabled; - - const readyToDisplaySubstations = - readyToDisplay && mapEquipments.substations && geoData.substationPositionsById.size > 0; - - const readyToDisplayLines = - readyToDisplay && - (mapEquipments?.lines || mapEquipments?.hvdcLines || mapEquipments?.tieLines) && - mapEquipments.voltageLevels && - geoData.substationPositionsById.size > 0; - - const mapEquipmentsLines = useMemo(() => { - return [ - ...(mapEquipments?.lines.map( - (line) => - ({ - ...line, - equipmentType: EQUIPMENT_TYPES.LINE, - } as LineEquimentLine) - ) ?? []), - ...(mapEquipments?.tieLines.map( - (tieLine) => - ({ - ...tieLine, - equipmentType: EQUIPMENT_TYPES.TIE_LINE, - } as TieLineEquimentLine) - ) ?? []), - ...(mapEquipments?.hvdcLines.map( - (hvdcLine) => - ({ - ...hvdcLine, - equipmentType: EQUIPMENT_TYPES.HVDC_LINE, - } as HvdcLineEquimentLine) - ) ?? []), - ]; - }, [mapEquipments?.hvdcLines, mapEquipments?.tieLines, mapEquipments?.lines]) as EquimentLine[]; - - const divRef = useRef(null); - - const mToken = !mapBoxToken ? FALLBACK_MAPBOX_TOKEN : mapBoxToken; - - useEffect(() => { - if (centerOnSubstation === null) { - return; - } - setCentered({ - lastCenteredSubstation: null, - centeredSubstationId: centerOnSubstation?.to, - centered: true, - }); - }, [centerOnSubstation]); - - // TODO simplify this, now we use Map as the camera controlling component - // so we don't need the deckgl ref anymore. The following comments are - // probably outdated, cleanup everything: - // Do this in onAfterRender because when doing it in useEffect (triggered by calling setDeck()), - // it doesn't work in the case of using the browser backward/forward buttons (because in this particular case, - // we get the ref to the deck and it has not yet initialized..) - function onAfterRender() { - // TODO outdated comment - //use centered and deck to execute this block only once when the data is ready and deckgl is initialized - //TODO, replace the next lines with setProps( { initialViewState } ) when we upgrade to 8.1.0 - //see https://github.com/uber/deck.gl/pull/4038 - //This is a hack because it accesses the properties of deck directly but for now it works - if ( - (!centered.centered || - (centered.centeredSubstationId && - centered.centeredSubstationId !== centered.lastCenteredSubstation)) && - geoData !== null - ) { - if (geoData.substationPositionsById.size > 0) { - if (centered.centeredSubstationId) { - const geodata = geoData.substationPositionsById.get(centered.centeredSubstationId); - if (!geodata) { - return; - } // can't center on substation if no coordinate. - mapRef.current?.flyTo({ - center: [geodata.lon, geodata.lat], - duration: 2000, - // only zoom if the current zoom is smaller than the new one - zoom: Math.max(mapRef.current?.getZoom(), locateSubStationZoomLevel), - essential: true, - }); - setCentered({ - lastCenteredSubstation: centered.centeredSubstationId, - centeredSubstationId: centered.centeredSubstationId, - centered: true, - }); - } else { - const coords = Array.from(geoData.substationPositionsById.entries()).map((x) => x[1]); - const maxlon = Math.max.apply( - null, - coords.map((x) => x.lon) - ); - const minlon = Math.min.apply( - null, - coords.map((x) => x.lon) - ); - const maxlat = Math.max.apply( - null, - coords.map((x) => x.lat) - ); - const minlat = Math.min.apply( - null, - coords.map((x) => x.lat) - ); - const marginlon = (maxlon - minlon) / 10; - const marginlat = (maxlat - minlat) / 10; - mapRef.current?.fitBounds( - [ - [minlon - marginlon / 2, minlat - marginlat / 2], - [maxlon + marginlon / 2, maxlat + marginlat / 2], - ], - { animate: false } - ); - setCentered({ - lastCenteredSubstation: null, - centered: true, - }); - } - } - } - } - - function onViewStateChange(info: ViewStateChangeEvent) { - lastViewStateRef.current = info.viewState; - if ( - // @ts-expect-error: TODO fix interactionState - !info.interactionState || // first event of before an animation (e.g. clicking the +/- buttons of the navigation controls, gives the target - // @ts-expect-error: TODO fix interactionState - (info.interactionState && !info.interactionState.inTransition) // Any event not part of a animation (mouse panning or zooming) - ) { - if (info.viewState.zoom >= labelsZoomThreshold && !labelsVisible) { - setLabelsVisible(true); - } else if (info.viewState.zoom < labelsZoomThreshold && labelsVisible) { - setLabelsVisible(false); - } - setShowTooltip(info.viewState.zoom >= tooltipZoomThreshold); - setShowLineFlow(info.viewState.zoom >= arrowsZoomThreshold); - } - } - - function renderTooltip() { - return ( - tooltip && - tooltip.visible && - !shouldDisableToolTip && - //As of now only LINE tooltip is implemented, the following condition is to be removed or tweaked once other types of line tooltip are implemented - tooltip.equipmentType === EQUIPMENT_TYPES.LINE && ( -
- {tooltip.equipmentId && divRef.current && renderPopover(tooltip.equipmentId, divRef.current)} -
- ) - ); - } - - function onClickHandler( - info: PickingInfo, - event: mapboxgl.MapLayerMouseEvent | maplibregl.MapLayerMouseEvent, - network: MapEquipments - ) { - const leftButton = event.originalEvent.button === MOUSE_EVENT_BUTTON_LEFT; - const rightButton = event.originalEvent.button === MOUSE_EVENT_BUTTON_RIGHT; - if ( - info.layer && - info.layer.id.startsWith(SUBSTATION_LAYER_PREFIX) && - info.object && - (isSubstation(info.object) || isVoltageLevel(info.object)) // is a voltage level marker, or a substation text - ) { - let idVl; - let idSubstation; - if (isVoltageLevel(info.object)) { - idVl = info.object.id; - } else if (isSubstation(info.object)) { - if (info.object.voltageLevels.length === 1) { - const idS = info.object.voltageLevels[0].substationId; - const substation = network.getSubstation(idS); - if (substation && substation.voltageLevels.length > 1) { - idSubstation = idS; - } else { - idVl = info.object.voltageLevels[0].id; - } - } else { - idSubstation = info.object.voltageLevels[0].substationId; - } - } - if (idVl !== undefined) { - if (onSubstationClick && leftButton) { - onSubstationClick(idVl); - } else if (onVoltageLevelMenuClick && rightButton) { - onVoltageLevelMenuClick( - network.getVoltageLevel(idVl)!, - event.originalEvent.x, - event.originalEvent.y - ); - } - } - if (idSubstation !== undefined) { - if (onSubstationClickChooseVoltageLevel && leftButton) { - onSubstationClickChooseVoltageLevel(idSubstation, event.originalEvent.x, event.originalEvent.y); - } else if (onSubstationMenuClick && rightButton) { - onSubstationMenuClick( - network.getSubstation(idSubstation)!, - event.originalEvent.x, - event.originalEvent.y - ); - } - } - } - if ( - rightButton && - info.layer && - info.layer.id.startsWith(LINE_LAYER_PREFIX) && - info.object && - isLine(info.object) - ) { - // picked line properties are retrieved from network data and not from pickable object infos, - // because pickable object infos might not be up to date - const line = network.getLine(info.object.id); - const tieLine = network.getTieLine(info.object.id); - const hvdcLine = network.getHvdcLine(info.object.id); - - const equipment = line || tieLine || hvdcLine; - if (equipment) { - const menuClickFunction = - equipment === line - ? onLineMenuClick - : equipment === tieLine - ? onTieLineMenuClick - : onHvdcLineMenuClick; - - menuClickFunction(equipment, event.originalEvent.x, event.originalEvent.y); - } - } - } - - function onMapContextMenu(event: mapboxgl.MapLayerMouseEvent | maplibregl.MapLayerMouseEvent) { - const info = - deckRef.current && - deckRef.current.pickObject({ - x: event.point.x, - y: event.point.y, - radius: PICKING_RADIUS, - }); - info && mapEquipments && onClickHandler(info, event, mapEquipments); - } - - function cursorHandler() { - return isDragging ? 'grabbing' : cursorType; - } - - const layers = []; - - if (readyToDisplaySubstations) { - layers.push( - new SubstationLayer({ - id: SUBSTATION_LAYER_PREFIX, - data: mapEquipments?.substations, - network: mapEquipments, - geoData: geoData, - getNominalVoltageColor: getNominalVoltageColor, - filteredNominalVoltages: filteredNominalVoltages, - labelsVisible: labelsVisible, - labelColor: foregroundNeutralColor, - labelSize: LABEL_SIZE, - pickable: true, - onHover: ({ object }) => { - setCursorType(object ? 'pointer' : 'grab'); - }, - getNameOrId: getNameOrId, - }) - ); - } - - if (readyToDisplayLines) { - layers.push( - new LineLayer({ - areFlowsValid: areFlowsValid, - id: LINE_LAYER_PREFIX, - data: mapEquipmentsLines, - network: mapEquipments, - updatedLines: updatedLines, - geoData: geoData, - getNominalVoltageColor: getNominalVoltageColor, - disconnectedLineColor: foregroundNeutralColor, - filteredNominalVoltages: filteredNominalVoltages, - lineFlowMode: lineFlowMode, - showLineFlow: visible && showLineFlow, - lineFlowColorMode: lineFlowColorMode, - lineFlowAlertThreshold: lineFlowAlertThreshold, - lineFullPath: geoData.linePositionsById.size > 0 && lineFullPath, - lineParallelPath: lineParallelPath, - labelsVisible: labelsVisible, - labelColor: foregroundNeutralColor, - labelSize: LABEL_SIZE, - pickable: true, - onHover: ({ object: lineObject, x, y }: PickingInfo) => { - if (lineObject) { - setCursorType('pointer'); - setTooltip({ - equipmentId: lineObject?.id, - equipmentType: (lineObject as EquimentLine)?.equipmentType, - pointerX: x, - pointerY: y, - visible: showTooltip, - }); - } else { - setCursorType('grab'); - setTooltip(null); - } - }, - }) - ); - } - - const initialViewState = { - longitude: initialPosition[0], - latitude: initialPosition[1], - zoom: initialZoom, - maxZoom: 14, - pitch: 0, - bearing: 0, - }; - - const renderOverlay = () => ( - - ); - - useEffect(() => { - mapRef.current?.resize(); - }, [mapRef, triggerMapResizeOnChange]); - - const getMapStyle = (mapLibrary: MapLibrary, mapTheme: MapTheme) => { - switch (mapLibrary) { - case MAPBOX: - if (mapTheme === LIGHT) { - return 'mapbox://styles/mapbox/light-v9'; - } else { - return 'mapbox://styles/mapbox/dark-v9'; - } - case CARTO: - if (mapTheme === LIGHT) { - return 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'; - } else { - return 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'; - } - case CARTO_NOLABEL: - if (mapTheme === LIGHT) { - return 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json'; - } else { - return 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json'; - } - default: - return 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'; - } - }; - - const mapStyle = useMemo(() => getMapStyle(mapLibrary, mapTheme), [mapLibrary, mapTheme]); - - const key = mapLibrary === MAPBOX && mToken ? 'mapboxgl' : 'maplibregl'; - - const mapLib = - mapLibrary === MAPBOX - ? mToken && { - mapLib: mapboxgl, - mapboxAccessToken: mToken, - } - : { - mapLib: maplibregl, - }; - - // because the mapLib prop of react-map-gl is not reactive, we need to - // unmount/mount the Map with 'key', so we need also to reset all state - // associated with uncontrolled state of the map - useEffect(() => { - setCentered(INITIAL_CENTERED); - }, [key]); - - const onUpdate = useCallback(() => { - onPolygonChanged(getPolygonFeatures()); - onDrawEvent(DRAW_EVENT.UPDATE); - }, [onDrawEvent, onPolygonChanged]); - - const onCreate = useCallback(() => { - onPolygonChanged(getPolygonFeatures()); - onDrawEvent(DRAW_EVENT.CREATE); - }, [onDrawEvent, onPolygonChanged]); - const getSelectedLines = useCallback(() => { - const polygonFeatures = getPolygonFeatures(); - const polygonCoordinates = polygonFeatures?.geometry; - if ( - !polygonCoordinates || - polygonCoordinates.type !== 'Polygon' || - polygonCoordinates.coordinates[0].length < 3 - ) { - return []; - } - //for each line, check if it is in the polygon - const selectedLines = getSelectedLinesInPolygon( - mapEquipments, - mapEquipmentsLines, - geoData, - polygonCoordinates as Polygon - ); - return selectedLines.filter((line: Line) => { - return filteredNominalVoltages!.some((nv) => { - return ( - nv === mapEquipments!.getVoltageLevel(line.voltageLevelId1)!.nominalV || - nv === mapEquipments!.getVoltageLevel(line.voltageLevelId2)!.nominalV - ); - }); - }); - }, [mapEquipments, mapEquipmentsLines, geoData, filteredNominalVoltages]); - - const getSelectedSubstations = useCallback(() => { - const substations = getSubstationsInPolygon(getPolygonFeatures(), mapEquipments, geoData); - if (filteredNominalVoltages === null) { - return substations; - } - return ( - substations.filter((substation) => { - return substation.voltageLevels.some((vl) => filteredNominalVoltages.includes(vl.nominalV)); - }) ?? [] - ); - }, [mapEquipments, geoData, filteredNominalVoltages]); - - useImperativeHandle( - ref, - () => ({ - getSelectedSubstations, - getSelectedLines, - cleanDraw() { - //because deleteAll does not trigger a update of the polygonFeature callback - getMapDrawer()?.deleteAll(); - onPolygonChanged(getPolygonFeatures()); - onDrawEvent(DRAW_EVENT.DELETE); - }, - getMapDrawer, - }), - [onPolygonChanged, getSelectedSubstations, getSelectedLines, onDrawEvent] - ); - - const onDelete = useCallback(() => { - onPolygonChanged(getPolygonFeatures()); - onDrawEvent(DRAW_EVENT.DELETE); - }, [onPolygonChanged, onDrawEvent]); - - return ( - mapLib && ( - setDragging(true)} - onDragEnd={() => setDragging(false)} - onContextMenu={onMapContextMenu} - mapLib={mapLib.mapLib as MapLib} - > - {displayOverlayLoader && renderOverlay()} - {isManualRefreshBackdropDisplayed && ( - - - - )} - { - onClickHandler( - info, - event.srcEvent as mapboxgl.MapLayerMouseEvent | maplibregl.MapLayerMouseEvent, - mapEquipments! - ); - }} - onAfterRender={onAfterRender} // TODO simplify this - layers={layers} - pickingRadius={PICKING_RADIUS} - /> - {showTooltip && renderTooltip()} - {/* visualizePitch true makes the compass reset the pitch when clicked in addition to visualizing it */} - - { - onDrawPolygonModeActive(polygon_draw); - }} - onCreate={onCreate} - onUpdate={onUpdate} - onDelete={onDelete} - /> - - ) - ); - } -); - -export default memo(NetworkMap); - -function getSubstationsInPolygon( - features: Partial, // Feature from geojson - mapEquipments: MapEquipments | null, - geoData: GeoData | null -) { - const polygonCoordinates = features?.geometry; - if ( - !geoData || - !polygonCoordinates || - polygonCoordinates.type !== 'Polygon' || - polygonCoordinates.coordinates[0].length < 3 - ) { - return []; - } - //get the list of substation - const substationsList = mapEquipments?.substations ?? []; - //for each substation, check if it is in the polygon - return substationsList // keep only the sybstation in the polygon - .filter((substation) => { - const pos = geoData.getSubstationPosition(substation.id); - return booleanPointInPolygon(pos, polygonCoordinates); - }); -} - -function getSelectedLinesInPolygon( - network: MapEquipments | null, - lines: Line[], - geoData: GeoData | null, - polygonCoordinates: Polygon -) { - return lines.filter((line) => { - try { - const linePos = network ? geoData?.getLinePositions(network, line) : null; - if (!linePos) { - return false; - } - if (linePos.length < 2) { - return false; - } - const extremities = [linePos[0], linePos[linePos.length - 1]]; - return extremities.some((pos) => booleanPointInPolygon(pos, polygonCoordinates)); - } catch (error) { - console.error(error); - return false; - } - }); -} diff --git a/src/components/network-map-viewer/network/substation-layer.ts b/src/components/network-map-viewer/network/substation-layer.js similarity index 72% rename from src/components/network-map-viewer/network/substation-layer.ts rename to src/components/network-map-viewer/network/substation-layer.js index 34f2de81..e882b412 100644 --- a/src/components/network-map-viewer/network/substation-layer.ts +++ b/src/components/network-map-viewer/network/substation-layer.js @@ -5,15 +5,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Color, CompositeLayer, LayerContext, TextLayer, UpdateParameters } from 'deck.gl'; +import { CompositeLayer, TextLayer } from 'deck.gl'; import ScatterplotLayerExt from './layers/scatterplot-layer-ext'; import { SUBSTATION_RADIUS, SUBSTATION_RADIUS_MAX_PIXEL, SUBSTATION_RADIUS_MIN_PIXEL } from './constants'; -import { Substation, VoltageLevel } from '../utils/equipment-types'; -import { MapEquipments } from './map-equipments'; -import { GeoData } from './geo-data'; -const voltageLevelNominalVoltageIndexer = (map: Map, voltageLevel: VoltageLevel) => { +const voltageLevelNominalVoltageIndexer = (map, voltageLevel) => { let list = map.get(voltageLevel.nominalV); if (!list) { list = []; @@ -23,31 +20,9 @@ const voltageLevelNominalVoltageIndexer = (map: Map, vol return map; }; -type MetaVoltageLevel = { - nominalVoltageIndex: number; - voltageLevels: VoltageLevel[]; -}; - -type MetaVoltageLevelsByNominalVoltage = { - nominalV: number; - metaVoltageLevels: MetaVoltageLevel[]; -}; - -export type SubstationLayerProps = { - data: Substation[]; - network: MapEquipments; - geoData: GeoData; - getNominalVoltageColor: (nominalV: number) => Color; - filteredNominalVoltages: number[] | null; - labelsVisible: boolean; - labelColor: Color; - labelSize: number; - getNameOrId: (infos: Substation) => string | null; -}; - -export class SubstationLayer extends CompositeLayer { - initializeState(context: LayerContext) { - super.initializeState(context); +export class SubstationLayer extends CompositeLayer { + initializeState() { + super.initializeState(); this.state = { compositeData: [], @@ -55,20 +30,16 @@ export class SubstationLayer extends CompositeLayer { }; } - updateState({ - props: { data, filteredNominalVoltages, geoData, getNameOrId, network }, - oldProps, - changeFlags, - }: UpdateParameters) { + updateState({ props, oldProps, changeFlags }) { if (changeFlags.dataChanged) { - const metaVoltageLevelsByNominalVoltage = new Map(); + let metaVoltageLevelsByNominalVoltage = new Map(); - if (network != null && geoData != null) { + if (props.network != null && props.geoData != null) { // create meta voltage levels // a meta voltage level is made of: // - a list of voltage level that belong to same substation and with same nominal voltage // - index of the voltage levels nominal voltage in the substation nominal voltage list - data.forEach((substation) => { + props.data.forEach((substation) => { // index voltage levels of this substation by its nominal voltage (this is because we might // have several voltage levels with the same nominal voltage in the same substation) const voltageLevelsByNominalVoltage = substation.voltageLevels.reduce( @@ -117,17 +88,18 @@ export class SubstationLayer extends CompositeLayer { if ( changeFlags.dataChanged || - getNameOrId !== oldProps.getNameOrId || - filteredNominalVoltages !== oldProps.filteredNominalVoltages + props.getNameOrId !== oldProps.getNameOrId || + props.filteredNominalVoltages !== oldProps.filteredNominalVoltages ) { - let substationsLabels = data; + let substationsLabels = props.data; - if (network != null && geoData != null && filteredNominalVoltages != null) { + if (props.network != null && props.geoData != null && props.filteredNominalVoltages != null) { // we construct the substations where there is at least one voltage level with a nominal voltage // present in the filteredVoltageLevels property, in order to handle correctly the substations labels visibility substationsLabels = substationsLabels.filter( (substation) => - substation.voltageLevels.find((v) => filteredNominalVoltages.includes(v.nominalV)) !== undefined + substation.voltageLevels.find((v) => props.filteredNominalVoltages.includes(v.nominalV)) !== + undefined ); } @@ -139,19 +111,18 @@ export class SubstationLayer extends CompositeLayer { const layers = []; // substations : create one layer per nominal voltage, starting from higher to lower nominal voltage - this.state.metaVoltageLevelsByNominalVoltage.forEach((e: MetaVoltageLevelsByNominalVoltage) => { + this.state.metaVoltageLevelsByNominalVoltage.forEach((e) => { const substationsLayer = new ScatterplotLayerExt( this.getSubLayerProps({ id: 'NominalVoltage' + e.nominalV, data: e.metaVoltageLevels, radiusMinPixels: SUBSTATION_RADIUS_MIN_PIXEL, - getRadiusMaxPixels: (metaVoltageLevel: MetaVoltageLevel) => + getRadiusMaxPixels: (metaVoltageLevel) => SUBSTATION_RADIUS_MAX_PIXEL * (metaVoltageLevel.nominalVoltageIndex + 1), - getPosition: (metaVoltageLevel: MetaVoltageLevel) => + getPosition: (metaVoltageLevel) => this.props.geoData.getSubstationPosition(metaVoltageLevel.voltageLevels[0].substationId), getFillColor: this.props.getNominalVoltageColor(e.nominalV), - getRadius: (voltageLevel: MetaVoltageLevel) => - SUBSTATION_RADIUS * (voltageLevel.nominalVoltageIndex + 1), + getRadius: (voltageLevel) => SUBSTATION_RADIUS * (voltageLevel.nominalVoltageIndex + 1), visible: !this.props.filteredNominalVoltages || this.props.filteredNominalVoltages.includes(e.nominalV), updateTriggers: { @@ -167,8 +138,8 @@ export class SubstationLayer extends CompositeLayer { this.getSubLayerProps({ id: 'Label', data: this.state.substationsLabels, - getPosition: (substation: Substation) => this.props.geoData.getSubstationPosition(substation.id), - getText: (substation: Substation) => this.props.getNameOrId(substation), + getPosition: (substation) => this.props.geoData.getSubstationPosition(substation.id), + getText: (substation) => this.props.getNameOrId(substation), getColor: this.props.labelColor, fontFamily: 'Roboto', getSize: this.props.labelSize, diff --git a/src/components/network-map-viewer/utils/equipment-types.ts b/src/components/network-map-viewer/utils/equipment-types.ts index ca796144..0bc25e77 100644 --- a/src/components/network-map-viewer/utils/equipment-types.ts +++ b/src/components/network-map-viewer/utils/equipment-types.ts @@ -5,8 +5,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { LineStatus } from '../network/line-layer'; - export const EQUIPMENT_INFOS_TYPES = { LIST: { type: 'LIST' }, MAP: { type: 'MAP' }, @@ -15,107 +13,22 @@ export const EQUIPMENT_INFOS_TYPES = { TOOLTIP: { type: 'TOOLTIP' }, }; -export enum EQUIPMENT_TYPES { - SUBSTATION = 'SUBSTATION', - VOLTAGE_LEVEL = 'VOLTAGE_LEVEL', - LINE = 'LINE', - TWO_WINDINGS_TRANSFORMER = 'TWO_WINDINGS_TRANSFORMER', - THREE_WINDINGS_TRANSFORMER = 'THREE_WINDINGS_TRANSFORMER', - HVDC_LINE = 'HVDC_LINE', - GENERATOR = 'GENERATOR', - BATTERY = 'BATTERY', - LOAD = 'LOAD', - SHUNT_COMPENSATOR = 'SHUNT_COMPENSATOR', - TIE_LINE = 'TIE_LINE', - DANGLING_LINE = 'DANGLING_LINE', - STATIC_VAR_COMPENSATOR = 'STATIC_VAR_COMPENSATOR', - HVDC_CONVERTER_STATION = 'HVDC_CONVERTER_STATION', - VSC_CONVERTER_STATION = 'VSC_CONVERTER_STATION', - LCC_CONVERTER_STATION = 'LCC_CONVERTER_STATION', - SWITCH = 'SWITCH', -} - -export type LonLat = [number, number]; - -export type VoltageLevel = { - id: string; - nominalV: number; - substationId: string; - substationName?: string; -}; - -export type Substation = { - id: string; - name: string; - voltageLevels: VoltageLevel[]; -}; - -export const isVoltageLevel = (object: Record): object is VoltageLevel => 'substationId' in object; - -export const isSubstation = (object: Record): object is Substation => 'voltageLevels' in object; - -export type Line = { - id: string; - voltageLevelId1: string; - voltageLevelId2: string; - name: string; - terminal1Connected: boolean; - terminal2Connected: boolean; - p1: number; - p2: number; - i1?: number; - i2?: number; - operatingStatus?: LineStatus; - currentLimits1?: { - permanentLimit: number; - } | null; - currentLimits2?: { - permanentLimit: number; - } | null; - // additionnal from line-layer - origin?: LonLat; - end?: LonLat; - substationIndexStart?: number; - substationIndexEnd?: number; - angle?: number; - angleStart?: number; - angleEnd?: number; - proximityFactorStart?: number; - proximityFactorEnd?: number; - parallelIndex?: number; - cumulativeDistances?: number[]; - positions?: LonLat[]; -}; - -export const isLine = (object: Record): object is Line => - 'id' in object && 'voltageLevelId1' in object && 'voltageLevelId2' in object; - -export type TieLine = { - id: string; -}; - -export enum ConvertersMode { - SIDE_1_RECTIFIER_SIDE_2_INVERTER, - SIDE_1_INVERTER_SIDE_2_RECTIFIER, -} - -export type HvdcLine = { - id: string; - convertersMode: ConvertersMode; - r: number; - nominalV: number; - activePowerSetpoint: number; - maxP: number; -}; - -export type Equipment = Line | Substation | TieLine | HvdcLine; - -// type EquimentLineTypes = EQUIPMENT_TYPES.LINE | EQUIPMENT_TYPES.TIE_LINE | EQUIPMENT_TYPES.HVDC_LINE; -export type LineEquimentLine = Line & { equipmentType: EQUIPMENT_TYPES.LINE }; -export type TieLineEquimentLine = Line & { - equipmentType: EQUIPMENT_TYPES.TIE_LINE; -}; -export type HvdcLineEquimentLine = Line & { - equipmentType: EQUIPMENT_TYPES.HVDC_LINE; +export const EQUIPMENT_TYPES = { + SUBSTATION: 'SUBSTATION', + VOLTAGE_LEVEL: 'VOLTAGE_LEVEL', + LINE: 'LINE', + TWO_WINDINGS_TRANSFORMER: 'TWO_WINDINGS_TRANSFORMER', + THREE_WINDINGS_TRANSFORMER: 'THREE_WINDINGS_TRANSFORMER', + HVDC_LINE: 'HVDC_LINE', + GENERATOR: 'GENERATOR', + BATTERY: 'BATTERY', + LOAD: 'LOAD', + SHUNT_COMPENSATOR: 'SHUNT_COMPENSATOR', + TIE_LINE: 'TIE_LINE', + DANGLING_LINE: 'DANGLING_LINE', + STATIC_VAR_COMPENSATOR: 'STATIC_VAR_COMPENSATOR', + HVDC_CONVERTER_STATION: 'HVDC_CONVERTER_STATION', + VSC_CONVERTER_STATION: 'VSC_CONVERTER_STATION', + LCC_CONVERTER_STATION: 'LCC_CONVERTER_STATION', + SWITCH: 'SWITCH', }; -export type EquimentLine = LineEquimentLine | TieLineEquimentLine | HvdcLineEquimentLine; diff --git a/src/deckgl.d.ts b/src/deckgl.d.ts index 8418d85f..2d990b4b 100644 --- a/src/deckgl.d.ts +++ b/src/deckgl.d.ts @@ -8,95 +8,9 @@ /* eslint-disable */ /* Override for v8 following - * https://deck.gl/docs/get-started/using-with-typescript#deckgl-v8 - * TODO: remove this file when migrating to deck.gl v9 + * https://deck.gl/docs/get-started/using-with-typescript */ declare module 'deck.gl' { //export namespace DeckTypings {} export * from 'deck.gl/typed'; } -declare module '@deck.gl/aggregation-layers' { export * from '@deck.gl/aggregation-layers/typed'; } -declare module '@deck.gl/carto' { export * from '@deck.gl/carto/typed'; } -declare module '@deck.gl/core' { export * from '@deck.gl/core/typed'; } -declare module '@deck.gl/extensions' { export * from '@deck.gl/extensions/typed'; } -declare module '@deck.gl/geo-layers' { export * from '@deck.gl/geo-layers/typed'; } -declare module '@deck.gl/google-maps' { export * from '@deck.gl/google-maps/typed'; } -declare module '@deck.gl/json' { export * from '@deck.gl/json/typed'; } -declare module '@deck.gl/layers' { export * from '@deck.gl/layers/typed'; } -declare module '@deck.gl/mapbox' { export * from '@deck.gl/mapbox/typed'; } -declare module '@deck.gl/mesh-layers' { export * from '@deck.gl/mesh-layers/typed'; } -declare module '@deck.gl/react' { export * from '@deck.gl/react/typed'; } - -/* For @luma.gl v8, the best would be to use @danmarshall/deckgl-typings work, but it conflicts with "@deck.gl//typed"... - * Has we will migrate to deck.gl v9 very soon, it's acceptable to just let typescript not check types temporally. - * TODO: remove this file when migrating to deck.gl v9 - */ -declare module '@luma.gl/core' { - // just shut down tsc with 'any' - export { Model, Geometry } from '@luma.gl/engine'; - export function isWebGL2(gl: any): boolean; - export function hasFeatures(gl: any, features: any): any; - export class Texture2D extends Resource { - static isSupported(gl: any, opts: any): boolean; - constructor(gl: any, props?: {}); - toString(): string; - initialize(props?: {}): this | void; - get handle(): any; - delete({ deleteChildren }?: { deleteChildren?: boolean }): this | void; - getParameter(pname: any, opts?: {}): any; - getParameters(opts?: {}): {}; - setParameter(pname: any, value: any): this; - setParameters(parameters: any): this; - stubRemovedMethods(className: any, version: any, methodNames: any): void; - resize({ height, width, mipmaps }: { height: any; width: any; mipmaps?: boolean }): this; - generateMipmap(params?: {}): this; - setImageData(options: any): this; - setSubImageData(args: { - target?: any; - pixels?: any; - data?: any; - x?: number; - y?: number; - width?: any; - height?: any; - level?: number; - format?: any; - type?: any; - dataFormat?: any; - compressed?: boolean; - offset?: number; - border?: any; - parameters?: {}; - }): void; - copyFramebuffer(opts?: {}): any; - getActiveUnit(): number; - bind(textureUnit?: any): any; - unbind(textureUnit?: any): any; - } - export const FEATURES: { - WEBGL2: string; - VERTEX_ARRAY_OBJECT: string; - TIMER_QUERY: string; - INSTANCED_RENDERING: string; - MULTIPLE_RENDER_TARGETS: string; - ELEMENT_INDEX_UINT32: string; - BLEND_EQUATION_MINMAX: string; - FLOAT_BLEND: string; - COLOR_ENCODING_SRGB: string; - TEXTURE_DEPTH: string; - TEXTURE_FLOAT: string; - TEXTURE_HALF_FLOAT: string; - TEXTURE_FILTER_LINEAR_FLOAT: string; - TEXTURE_FILTER_LINEAR_HALF_FLOAT: string; - TEXTURE_FILTER_ANISOTROPIC: string; - COLOR_ATTACHMENT_RGBA32F: string; - COLOR_ATTACHMENT_FLOAT: string; - COLOR_ATTACHMENT_HALF_FLOAT: string; - GLSL_FRAG_DATA: string; - GLSL_FRAG_DEPTH: string; - GLSL_DERIVATIVES: string; - GLSL_TEXTURE_LOD: string; - }; - //export type TextureFormat = any; - //export type UniformValue = any; -}