sprucegoose
sprucegoose

Reputation: 604

How to zoom to bounds of arcs with deck.gl?

I've created some arcs using deck.gl. When you click on different points/polygons, different arcs appear between countries. When doing this, I want the map to zoom to the bounds of those arcs.

For clarity, here is an example: When clicking on Glasgow, I'd want to zoom to the arc shown (as tightly as possible): map zoomed to arc originating from glasgow

It appears that with WebMercatorViewport, you can call fitBounds (see: https://deck.gl/docs/api-reference/core/web-mercator-viewport#webmercatorviewport)

It's not clear to me how this gets used, though. I've tried to find examples, but have come up short. How can I add this to what I have?

Here is the code for the arcs:

    fetch('countries.json')
    .then(res => res.json())
    .then(data => {

      console.log('data',data)

      const inFlowColors = [
        [0, 55, 255]
      ];

      const outFlowColors = [
        [255, 200, 0]
      ];

      const countyLayer = new deck.GeoJsonLayer({
        id: 'geojson',
        data: data,
        stroked: true,
        filled: true,
        autoHighlight: true,
        lineWidthScale: 20,
        lineWidthMinPixels: 1,
        pointRadiusMinPixels: 10,
        opacity:.5,
        getFillColor: () => [0, 0, 0],
        getLineColor: () => [0,0,0],
        getLineWidth: 1,
        onClick: info => updateLayers(info.object),
        pickable: true
      });

      const deckgl = new deck.DeckGL({
        mapboxApiAccessToken: 'pk.eyJ1IjoidWJlcmRhdGEiLCJhIjoiY2pudzRtaWloMDAzcTN2bzN1aXdxZHB5bSJ9.2bkj3IiRC8wj3jLThvDGdA',
        mapStyle: 'mapbox://styles/mapbox/light-v9',
        initialViewState: {
          longitude: -19.903283,
          latitude: 36.371449,
          zoom: 1.5,
          maxZoom: 15,
          pitch: 0,
          bearing: 0
        },
        controller: true,
        layers: []
      });

      updateLayers(
        data.features.find(f => f.properties.name == 'United States' )
      );

      function updateLayers(selectedFeature) {
        const {exports, centroid, top_exports, export_value} = selectedFeature.properties;

        const arcs = Object.keys(exports).map(toId => {
          const f = data.features[toId];
          return {
            source: centroid,
            target: f.properties.centroid,
            value: exports[toId],
            top_exports: top_exports[toId],
            export_value: export_value[toId]
          };
        });

        arcs.forEach(a => {
          a.vol = a.value;
        });

        const arcLayer = new deck.ArcLayer({
          id: 'arc',
          data: arcs,
          getSourcePosition: d => d.source,
          getTargetPosition: d => d.target,
          getSourceColor: d => [0, 55, 255],
          getTargetColor: d => [255, 200, 0],
          getHeight: 0,
          getWidth: d => d.vol
        });

        deckgl.setProps({
          layers: [countyLayer, arcLayer]
        });

      }

    });

Here it is as a Plunker: https://plnkr.co/edit/4L7HUYuQFM19m9rI

Upvotes: 3

Views: 5075

Answers (1)

lezan
lezan

Reputation: 787

I try to make it simple, starting from a raw implementation with ReactJs then try to translate into vanilla.

In ReactJS I will do something like that.

Import LinearInterpolator and WebMercatorViewport from react-map-gl:

import {LinearInterpolator, WebMercatorViewport} from 'react-map-gl';

Then I define an useEffect for viewport:

const [viewport, setViewport] = useState({
    latitude: 37.7577,
    longitude: -122.4376,
    zoom: 11,
    bearing: 0,
    pitch: 0
});

Then I will define a layer to show:

const layerGeoJson = new GeoJsonLayer({
    id: 'geojson',
    data: someData,
    ...
    pickable: true,
    onClick: onClickGeoJson,
    });

Now we need to define onClickGeoJson:

const onClickGeoJson = useCallback((event) => {
    const feature = event.features[0];
    const [minLng, minLat, maxLng, maxLat] = bbox(feature); // Turf.js
    const viewportWebMercator = new WebMercatorViewport(viewport);
    const {longitude, latitude, zoom} = viewport.fitBounds([[minLng, minLat], [maxLng, maxLat]], {
        padding: 20
    });
    viewportWebMercator = {
        ...viewport,
        longitude,
        latitude,
        zoom,
        transitionInterpolator: new LinearInterpolator({
            around: [event.offsetCenter.x, event.offsetCenter.y]
        }),
        transitionDuration: 1500,
    };
    setViewport(viewportWebMercator);
}, []);

First issue: in this way we are fitting on point or polygon clicked, but what you want is fitting arcs. I think the only way to overcome this kind of issue is to add a reference inside your polygon about bounds of arcs. You can precompute bounds for each feature and storage them inside your geojson (the elements clicked), or you can storage just a reference in feature.properties to point another object where you have your bounds (you can also compute them on the fly).

const dataWithComputeBounds = { 
   'firstPoint': bounds_arc_computed,
   'secondPoint': bounds_arc_computed,
   ....
}

bounds_arc_computed need to be an object

bounds_arc_computed = {
    minLng, minLat, maxLng, maxLat,
}

then on onClick function just take the reference

   const { minLng, minLat, maxLng, maxLat} = dataWithComputedBounds[event.features[0].properties.reference];
   const viewportWebMercator = new WebMercatorViewport(viewport);
   ...

Now just define our main element:

return (
    <DeckGL
        layers={[layerGeoJson]}
        initialViewState={INITIAL_VIEW_STATE}
        controller
    >
        <StaticMap
            reuseMaps
            mapStyle={mapStyle}
            preventStyleDiffing
            mapboxApiAccessToken={YOUR_TOKEN}
        />
    </DeckGL>
);

At this point we are pretty close to what you already linked (https://codepen.io/vis-gl/pen/pKvrGP), but you need to use deckgl.setProps() onClick function instead of setViewport to change your viewport.

Does it make sense to you?

Upvotes: 4

Related Questions