Vizzuality/landgriffon

View on GitHub
cookie-traceability/src/lib/flowmap/layers/FlowMapLayer.ts

Summary

Maintainability
D
2 days
Test Coverage
// @ts-nocheck
/*
 * Copyright 2022 FlowmapBlue
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { CompositeLayer } from '@deck.gl/core/typed';
import { ScatterplotLayer } from '@deck.gl/layers/typed';
import {
  colorAsRgba,
  FlowLinesLayerAttributes,
  FlowMapData,
  FlowMapDataAccessors,
  FlowMapDataProvider,
  getFlowLineAttributesByIndex,
  getFlowMapColors,
  getOuterCircleRadiusByIndex,
  getLocationCentroidByIndex,
  isFlowMapData,
  isFlowMapDataProvider,
  LayersData,
  LocalFlowMapDataProvider,
  LocationFilterMode,
  ViewportProps,
  FlowMapAggregateAccessors,
  ClusterNode,
  AggregateFlow,
} from '../data';
import AnimatedFlowLinesLayer from './AnimatedFlowLinesLayer';
import FlowCirclesLayer from './FlowCirclesLayer';
import FlowLinesLayer from './FlowLinesLayer';
import { FlowLayerPickingInfo, LayerProps, PickingInfo, PickingType } from './types';

export type FlowMapLayerProps<L, F> = {
  data: FlowMapData<L, F> | FlowMapDataProvider<L, F>;
  locationTotalsEnabled?: boolean;
  adaptiveScalesEnabled?: boolean;
  animationEnabled?: boolean;
  clusteringEnabled?: boolean;
  clusteringLevel?: number;
  fadeEnabled?: boolean;
  clusteringAuto?: boolean;
  darkMode?: boolean;
  fadeAmount?: number;
  colorScheme?: string;
  onHover?: (info: FlowLayerPickingInfo<L, F> | undefined, event: SourceEvent) => void;
  onClick?: (info: FlowLayerPickingInfo<L, F>, event: SourceEvent) => void;
} & Partial<FlowMapDataAccessors<L, F>> &
  LayerProps;

enum HighlightType {
  LOCATION = 'location',
  FLOW = 'flow',
}

type HighlightedLocationObject = {
  type: HighlightType.LOCATION;
  centroid: [number, number];
  radius: number;
};

type HighlightedFlowObject = {
  type: HighlightType.FLOW;
  lineAttributes: FlowLinesLayerAttributes;
};

type HighlightedObject = HighlightedLocationObject | HighlightedFlowObject;

type State<L, F> = {
  accessors: FlowMapAggregateAccessors<L, F>;
  dataProvider: FlowMapDataProvider<L, F>;
  layersData: LayersData | undefined;
  highlightedObject: HighlightedObject | undefined;
};

export type SourceEvent = { srcEvent: MouseEvent };

export default class FlowMapLayer<L, F> extends CompositeLayer {
  static defaultProps = {
    darkMode: true,
    fadeAmount: 50,
    locationTotalsEnabled: true,
    animationEnabled: false,
    clusteringEnabled: true,
    fadeEnabled: true,
    clusteringAuto: true,
    clusteringLevel: undefined,
    adaptiveScalesEnabled: true,
    colorScheme: 'Teal',
  };
  state: State<L, F> | undefined;

  public constructor(props: FlowMapLayerProps<L, F>) {
    super({
      ...props,
      onHover: (info: PickingInfo<any>, event: SourceEvent) => {
        // TODO: if (lastHoverEventStartTimeRef > startTime) {
        //   // Skipping, because this is not the latest hover event
        //   return;
        // }
        this.setState({ highlightedObject: this._getHighlightedObject(info) });
        const { onHover } = props;
        if (onHover) {
          this._getFlowLayerPickingInfo(info).then((info) => onHover(info, event));
        }
      },
      onClick: (info: PickingInfo<any>, event: SourceEvent) => {
        const { onClick } = props;
        if (onClick) {
          this._getFlowLayerPickingInfo(info).then((info) => {
            if (info) {
              onClick(info, event);
            }
          });
        }
      },
    });
  }

  initializeState() {
    this.state = {
      accessors: new FlowMapAggregateAccessors<L, F>(this.props),
      dataProvider: this._makeDataProvider(),
      layersData: undefined,
      highlightedObject: undefined,
    };
  }

  private _updateAccessors() {
    this.state?.dataProvider?.setAccessors(this.props);
    this.setState({ accessors: new FlowMapAggregateAccessors(this.props) });
  }

  private _makeDataProvider() {
    const { data } = this.props;
    if (isFlowMapDataProvider<L, F>(data)) {
      return data;
    } else if (isFlowMapData<L, F>(data)) {
      const dataProvider = new LocalFlowMapDataProvider<L, F>(this.props);
      dataProvider.setFlowMapData(data);
      return dataProvider;
    }
    throw new Error('FlowMapLayer: data must be a FlowMapDataProvider or FlowMapData');
  }

  private _updateDataProvider() {
    this.setState({ dataProvider: this._makeDataProvider() });
  }

  shouldUpdateState(params: Record<string, any>): boolean {
    const { changeFlags } = params;
    // if (this._viewportChanged()) {
    //   return true;
    // }
    if (changeFlags.viewportChanged) {
      return true;
    }
    return super.shouldUpdateState(params);
    // TODO: be smarter on when to update
    // (e.g. ignore viewport changes when adaptiveScalesEnabled and clustering are false)
  }

  updateState({ oldProps, props, changeFlags }: Record<string, any>): void {
    const { dataProvider, highlightedObject } = this.state || {};
    if (!dataProvider) {
      return;
    }

    if (changeFlags.propsChanged) {
      this._updateAccessors();
    }
    if (changeFlags.dataChanged) {
      this._updateDataProvider();
    }

    if (changeFlags.viewportChanged || changeFlags.propsOrDataChanged) {
      dataProvider.setFlowMapState(this._getFlowMapState());

      (async () => {
        const layersData = await dataProvider.getLayersData();
        this.setState({ layersData });
      })();
    }
  }

  private _getSettingsState() {
    const {
      locationTotalsEnabled,
      adaptiveScalesEnabled,
      animationEnabled,
      clusteringEnabled,
      clusteringLevel,
      fadeEnabled,
      clusteringAuto,
      darkMode,
      fadeAmount,
      colorScheme,
    } = this.props;
    return {
      locationTotalsEnabled,
      adaptiveScalesEnabled,
      animationEnabled,
      clusteringEnabled,
      clusteringLevel,
      fadeEnabled,
      clusteringAuto,
      darkMode,
      fadeAmount,
      colorScheme,
    };
  }

  private _getFlowMapState() {
    return {
      viewport: asViewState(this.context.viewport),
      filterState: {
        selectedLocations: undefined,
        locationFilterMode: LocationFilterMode.ALL,
        selectedTimeRange: undefined,
      },
      settingsState: this._getSettingsState(),
    };
  }

  private async _getFlowLayerPickingInfo(
    info: Record<string, any>,
  ): Promise<FlowLayerPickingInfo<L, F> | undefined> {
    const { index, sourceLayer } = info;
    const { dataProvider, accessors } = this.state || {};
    if (!dataProvider || !accessors) {
      return undefined;
    }
    const commonInfo = {
      // ...info,
      layer: info.layer,
      index: info.index,
      x: info.x,
      y: info.y,
      coordinate: info.coordinate,
    };
    if (sourceLayer instanceof FlowLinesLayer || sourceLayer instanceof AnimatedFlowLinesLayer) {
      const flow = index === -1 ? undefined : await dataProvider.getFlowByIndex(index);
      if (flow) {
        const origin = await dataProvider.getLocationById(accessors.getFlowOriginId(flow));
        const dest = await dataProvider.getLocationById(accessors.getFlowDestId(flow));
        if (origin && dest) {
          return {
            ...commonInfo,
            type: PickingType.FLOW,
            object: flow,
            origin: origin,
            dest: dest,
            count: accessors.getFlowMagnitude(flow),
          };
        }
      }
    } else if (sourceLayer instanceof FlowCirclesLayer) {
      const location = index === -1 ? undefined : await dataProvider.getLocationByIndex(index);

      if (location) {
        const id = accessors.getLocationId(location);
        const name = accessors.getLocationName(location);
        const totals = await dataProvider.getTotalsForLocation(id);
        const { circleAttributes } = this.state?.layersData || {};
        if (totals && circleAttributes) {
          const circleRadius = getOuterCircleRadiusByIndex(circleAttributes, info.index);
          return {
            ...commonInfo,
            type: PickingType.LOCATION,
            object: location,
            id,
            name,
            totals,
            circleRadius: circleRadius,
            event: undefined,
          };
        }
      }
    }

    return undefined;
  }

  private _getHighlightedObject(info: Record<string, any>): HighlightedObject | undefined {
    const { index, sourceLayer } = info;
    if (index < 0) return undefined;
    if (sourceLayer instanceof FlowLinesLayer || sourceLayer instanceof AnimatedFlowLinesLayer) {
      const { lineAttributes } = this.state?.layersData || {};
      if (lineAttributes) {
        return {
          type: HighlightType.FLOW,
          lineAttributes: getFlowLineAttributesByIndex(lineAttributes, index),
        };
      }
    } else if (sourceLayer instanceof FlowCirclesLayer) {
      const { circleAttributes } = this.state?.layersData || {};
      if (circleAttributes) {
        return {
          type: HighlightType.LOCATION,
          centroid: getLocationCentroidByIndex(circleAttributes, index),
          radius: getOuterCircleRadiusByIndex(circleAttributes, index),
        };
      }
    }
    return undefined;
  }

  renderLayers(): Array<any> {
    const layers = [];
    if (this.state?.layersData) {
      const { layersData, highlightedObject } = this.state;
      const { circleAttributes, lineAttributes } = layersData || {};
      if (circleAttributes && lineAttributes) {
        const flowMapColors = getFlowMapColors(this._getSettingsState());
        const outlineColor = colorAsRgba(
          flowMapColors.outlineColor || (this.props.darkMode ? '#000' : '#fff'),
        );
        const commonLineLayerProps = {
          data: lineAttributes,
          parameters: {
            // prevent z-fighting at non-zero bearing/pitch
            depthTest: false,
          },
        };
        if (this.props.animationEnabled) {
          layers.push(
            // @ts-ignore
            new AnimatedFlowLinesLayer({
              ...this.getSubLayerProps({
                ...commonLineLayerProps,
                id: 'animated-flow-lines',
                drawOutline: false,
                thicknessUnit: 10,
              }),
            }),
          );
        } else {
          layers.push(
            new FlowLinesLayer({
              ...this.getSubLayerProps({
                ...commonLineLayerProps,
                id: 'flow-lines',
                drawOutline: true,
                outlineColor: outlineColor,
              }),
            }),
          );
        }
        // layers.push(
        //   new FlowCirclesLayer(
        //     this.getSubLayerProps({
        //       id: 'circles',
        //       data: circleAttributes,
        //       emptyColor: [0, 0, 0, 255],
        //       emptyOutlineColor: [0, 0, 0, 255],
        //     }),
        //   ),
        // );
        if (highlightedObject) {
          switch (highlightedObject.type) {
            case HighlightType.LOCATION:
              layers.push(
                new ScatterplotLayer({
                  id: 'location-highlight',
                  data: [highlightedObject],
                  stroked: true,
                  filled: false,
                  lineWidthUnits: 'pixels',
                  getLineWidth: 2,
                  radiusUnits: 'pixels',
                  getRadius: (d: HighlightedLocationObject) => d.radius,
                  getLineColor: (d: HighlightedLocationObject) => colorAsRgba('orange'),
                  getPosition: (d: HighlightedLocationObject) => d.centroid,
                }),
              );
              break;
            case HighlightType.FLOW:
              layers.push(
                new FlowLinesLayer({
                  id: 'flow-highlight',
                  data: highlightedObject.lineAttributes,
                  drawOutline: false,
                  // outlineColor: colorAsRgba('orange'),
                  // outlineThickness: 1,
                }),
              );
              break;
          }
        }
      }
    }

    return layers;
  }
}

function asViewState(viewport: Record<string, any>): ViewportProps {
  const { width, height, longitude, latitude, zoom, pitch, bearing } = viewport;
  return {
    width,
    height,
    longitude,
    latitude,
    zoom,
    pitch,
    bearing,
  };
}