Search code examples
javascriptreactjsdeck.gl

Are there specific requirements to use the standalone GeoJson example code in another component?


I am working on a React application which should be used to visualize different geographical information, mostly described in GeoJSON format. In more detail, I am currently developing a map component which should be included in the overall frontend part later on.

For this purpose I started to started evaluating Deck.gl by using their code for the GeoJSON example app (https://github.com/uber/deck.gl/tree/master/examples/website/geojson). This worked perfectly fine. So I started adding some more layers for additional information:

  • TileLayer + BitmapLayer for integration of a different base map
  • IconLayer to visualize different points of interest
  • nebula.gl SelectionLayer to pick multiple elements within the map

This also worked perfectly fine. So I wanted to encapsulate this solution properly so it could be easily used as a child component. Therefore I used create-react-app for scaffolding and started migrating the code.

The resulting code structure looks like this (only relevant parts):

  • public
    • index.html
  • src

    • components
      • Map.js
    • data

      • atlas.png
      • mapping.json
      • icons.json
      • geodata.json
    • index.js

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>deck.gl Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style>
      body {
        margin: 0;
        font-family: sans-serif;
        width: 100vw;
        height: 100vh;
        overflow: hidden;
      }
      .tooltip {
        pointer-events: none;
        position: absolute;
        z-index: 9;
        font-size: 12px;
        padding: 8px;
        background: #000;
        color: #fff;
        min-width: 160px;
        max-height: 240px;
        overflow-y: hidden;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

index.js:

import React from "react";
import ReactDOM from "react-dom";

import Map from "./components/Map";

ReactDOM.render(<Map />, document.querySelector("#app"));

Map.js:

import React, { Component } from "react";
import DeckGL, {
  GeoJsonLayer,
  TileLayer,
  BitmapLayer,
  IconLayer
} from "deck.gl";
import myJson from "../data/geodata.json";
import iconLocations from "../data/icons.json";
import { SelectionLayer, SELECTION_TYPE } from "nebula.gl";

const INITIAL_VIEW_STATE = {
  latitude: SOME_LAT,
  longitude: SOME_LONG,
  zoom: 14,
  maxZoom: 19,
  pitch: 0,
  bearing: 0
};

export default class Map extends Component {
  state = {
    hoveredObject: null
  };

  _onHover = ({ x, y, object }) => {
    this.setState({ x, y, hoveredObject: object });
  };

  _renderLayers() {
    const {
      iconMapping = "../data/mapping.json",
      iconAtlas = "../data/atlas.png",
      viewState
    } = this.props;

    let data = myJson;
    data.features = data.features.filter(
      feature => feature.geometry.coordinates.length > 0
    );
    let size = viewState ? Math.min(Math.pow(1.5, viewState.zoom - 10), 1) : 1;

    return [
      new TileLayer({
        opacity: 1,
        minZoom: 0,
        maxZoom: 30,

        renderSubLayers: props => {
          const {x, y, z, bbox} = props.tile;
          const {west, south, east, north} = bbox;
          const base = 1 + ((x + y) % 4);
          return new BitmapLayer(props, {
            image: `http://${base}.base.maps.cit.api.here.com/maptile/2.1/maptile/newest/normal.day/${z}/${x}/${y}/512/png`,
            bounds: [west, south, east, north]
          });
        }
      }),
      new GeoJsonLayer({
        id: "geojson",
        data,
        opacity: 1,
        stroked: false,
        lineWidthMinPixels: 1,
        getLineColor: [255, 0, 0],
        pickable: true,
        autoHighlight: true,
        highlightColor: [0, 100, 255, 80],
        onHover: this._onHover
      }),
      new SelectionLayer({
        id: "selection",
        selectionType: SELECTION_TYPE.RECTANGLE,
        onSelect: ({ pickingInfos }) => {
          this.setState({
            selectedFeatureIndexes: pickingInfos.map(pi => pi.index)
          });
          console.log(pickingInfos);
        },
        layerIds: ["geojson"],

        getTentativeFillColor: () => [255, 0, 255, 100],
        getTentativeLineColor: () => [0, 0, 255, 255],
        getTentativeLineDashArray: () => [0, 0],
        lineWidthMinPixels: 3
      }),
      new IconLayer({
        id: "icon",
        data: iconLocations,
        wrapLongitude: true,
        getPosition: d => d.coordinates,
        iconAtlas,
        iconMapping,
        getIcon: d => d.type,
        getSize: size,
        sizeScale: 50
      })
    ];
  }

  _renderTooltip = () => {
    const { x, y, hoveredObject } = this.state;
    return (
      hoveredObject && (
        // some JSX for tooltip ...
      )
    );
  };

  render() {
    const { viewState, controller = true } = this.props;

    return (
      <DeckGL
        layers={this._renderLayers()}
        initialViewState={INITIAL_VIEW_STATE}
        viewState={viewState}
        controller={controller}
      >
        {this._renderTooltip}
      </DeckGL>
    );
  }
}

The code used in Map.js is actually exactly the same as used when expanding the example code (which worked as intented), only the way it would get rendered changed a little. I would expect it to work the same way, but I get the following output: https://i.sstatic.net/TW6nm.jpg

If I remove the TileLayer + BitmapLayer, the first error will disappear and at least the GeoJSON data will be correctly displayed, just without the base map. The IconLayer does also not work, while the SelectionLayer causes no problems, just as the GeojsonLayer.

I am quite new to React and Deck.gl, so is there anything I forgot to migrate the example code properly?

UPDATE:

I refactored the code a little and got the icons working. I also got rid of the error message when using the TileLayer by removing the propagation of props to the BitmapLayer (props.data is null, which seems to be no problem when used in the example code, but somehow causes problems here), but the bitmaps are not displayed at all, even though the image link and the bounds are correct.

import React from "react";
import DeckGL from "@deck.gl/react";
import { GeoJsonLayer, IconLayer, BitmapLayer } from "@deck.gl/layers";
import { TileLayer } from "@deck.gl/geo-layers";
import { SelectionLayer, SELECTION_TYPE } from "nebula.gl";

// test data
import jsonTestFile from "../data/testJson.json";
import signLocations from "../data/signs.json";
import iconAtlas from "../data/trafficSignAtlas.png";
import iconMapping from "../data/trafficSignMapping.json";

// Initial viewport settings
const initialViewState = {
  latitude: 48.872578,
  longitude: 11.431032,
  zoom: 14,
  pitch: 0,
  bearing: 0
};

const LayerEnum = Object.freeze({
  GEOJSON: 1,
  TILE: 2,
  SELECTION: 3,
  ICON: 4
});

export default class Map extends React.Component {
  state = {
    selectedFeatureIndexes: []
  };

  renderLayer = ({ layerType, options }) => {
    switch (layerType) {
      case LayerEnum.GEOJSON:
        return new GeoJsonLayer({
          id: "geojson",
          opacity: 1,
          stroked: false,
          lineWidthMinPixels: 1,
          getLineColor: [255, 0, 0],
          pickable: true,
          autoHighlight: true,
          highlightColor: [0, 100, 255, 80],
          ...options
        });
      case LayerEnum.TILE:
        return new TileLayer({
          opacity: 1,
          // https://wiki.openstreetmap.org/wiki/Zoom_levels
          minZoom: 0,
          maxZoom: 30,

          renderSubLayers: ({ id, tile }) => {
            const { x, y, z, bbox } = tile;
            const { west, south, east, north } = bbox;
            const base = 1 + ((x + y) % 4);
            console.log(tile);
            return new BitmapLayer({
              id,
              image: `http://${base}.base.maps.cit.api.here.com/maptile/2.1/maptile/newest/normal.day/${z}/${x}/${y}/512/png`,
              bounds: [west, south, east, north]
            });
          }
        });
      case LayerEnum.SELECTION:
        return new SelectionLayer({
          id: "selection",
          selectionType: SELECTION_TYPE.RECTANGLE,
          onSelect: ({ pickingInfos }) => {
            this.setState({
              selectedFeatureIndexes: pickingInfos.map(pi => pi.index)
            });
            console.log(pickingInfos);
          },
          layerIds: ["geojson"],

          getTentativeFillColor: () => [255, 0, 255, 100],
          getTentativeLineColor: () => [0, 0, 255, 255],
          getTentativeLineDashArray: () => [0, 0],
          lineWidthMinPixels: 3,
          ...options
        });
      case LayerEnum.ICON:
        return new IconLayer({
          id: "icon",
          wrapLongitude: true,
          getPosition: d => d.coordinates,
          getIcon: d => d.type,
          getSize: 1,
          sizeScale: 50,
          ...options
        });
      default:
        console.error("Unknown errer type detected!");
        return null;
    }
  };

  renderLayers = layers => {
    return layers.map(this.renderLayer);
  };

  render() {
    // preprocess test data
    let data = jsonTestFile;
    data.features = data.features.filter(
      feature => feature.geometry.coordinates.length > 0
    );

    const layers = this.renderLayers([
      {
        layerType: LayerEnum.GEOJSON,
        options: { data }
      },
      {
        layerType: LayerEnum.SELECTION,
        options: {}
      },
      {
        layerType: LayerEnum.ICON,
        options: {
          data: signLocations,
          iconAtlas,
          iconMapping
        }
      },
      {
        layerType: LayerEnum.TILE,
        options: {}
      }
    ]);

    const { viewState, controller = true } = this.props;
    return (
      <DeckGL
        initialViewState={initialViewState}
        viewState={viewState}
        controller={controller}
        layers={layers}
      />
    );
  }
}


Solution

  • As it turns out it seems to be a dependency version problem. The example code comes with [email protected] and I was using [email protected]. Starting at version 7.2.0 this code does not work anymore, 7.1.11 seems to be the latest possible version for this usecase.