2.5.1 • Published 2 years ago

@citydna/maps v2.5.1

Weekly downloads
204
License
MIT
Repository
-
Last release
2 years ago

@citydna/maps

City of Melbourne's mapping component library. Creates handy wrappers that manage map position and state for you, providing an easy hooks-based API for manipulating maps. Additionally, provides a deck.gl wrapper and API for layer management.

Usage

  1. add a REACT_APP_MAPBOX_API_ACCESS_TOKEN=<YOUR_MAPBOX_TOKEN> into your .env file. Why?
  2. Wrap your app with <MapProvider> at a minimum.
  3. Start developing!

What's the benefit? Why not just use react-map-gl directly?

Having built many apps for City of Melbourne, I found myself repeating a lot of boilerplate code just to get things working with react-map-gl and deck.gl together. I've used that experience and abstracted away the niggly bits so you can focus on building your apps.

Instead, you can wrap your app with providers, drop in a map component and use hooks to access layers, viewport, and even the mapbox instance directly.

TOC

Available providers

There are two providers you can use from this package, <MapProvider> and <DeckGLProvider>. These are separated in case you want to build maps without DeckGL, using only react-map-gl.

<MapProvider />

The <MapProvider /> component is used to wrap your app and provide access to viewport access and update from anywhere in the app. The hooks useViewport, useUpdateViewport and useMapboxRef consume this provider (click for more info).

Here's a quick example:

// index.ts - wrap you app in the provider
import React from "react"
import ReactDOM from "react-dom"
import { MapProvider } from "@citydna/maps"
import { App } from "./App"

ReactDOM.render(
  <MapProvider>
    <App />
  </MapProvider>,
  document.getElementById("root")
)
// App.ts - Use WrappedInteractiveMap where you'd normally use an InteractiveMap (from react-map-gl)
import React from "react"
import { WrappedInteractiveMap } from "@citydna/maps"
import { MyMapControl } from "./MyMapControl"

export const App = () => {
  return (
    <>
      <MyMapControl />
      <WrappedInteractiveMap />
    </>
  )
}
// MyMapControl.ts - use hooks exposed by the package to interact with the map
import React from "react"
import { useUpdateViewport } from "@citydna/maps"

const MyMapControl = () => {
  // grab the setViewport function and use it below
  const { setViewport } = useUpdateViewport()

  // set the initial viewport on mount as it starts at 0,0
  useEffect(() => {
    setViewport(melbourneViewport)
  }, [setViewport])

  // use setviewport in conjunction with UI elements to control the map
  return (
    <>
      <button onClick={() => setViewport(melbourneViewport)}>
        Go to Melbourne
      </button>
      <button onClick={() => setViewport(sydneyViewport)}>Go to Sydney</button>
    </>
  )
}

Under the hood, the <DeckGLMapboxMap />, <WrappedInteractiveMap /> and <WrappedStaticMap /> use a similar hooks, useViewport, to get the value you have set here and change the map accordingly.

<DeckGLProvider />

A lightweight context that stores current deck.gl layers in a useReducer hook. Use this provider in conjunction with <MapProvider /> and <DeckGLMapboxMap /> when you want to create maps that interleave deck.gl layers and mapbox layers. This is helpful when you want deck.gl data layers to sit behind mapbox labels such as street and suburb labels.

Quick usage example:

// index.ts - wrap you app in the provider
import React from "react"
import ReactDOM from "react-dom"
import { DeckGLProvider, MapProvider } from "@citydna/maps"
import { App } from "./App"

ReactDOM.render(
  <DeckGLProvider>
    <MapProvider>
      <App />
    </MapProvider>
  </DeckGLProvider>,
  document.getElementById("root")
)
// App.ts - Use the DeckGLMapboxMap to show a map
import React from "react"
import { WrappedInteractiveMap } from "@citydna/maps"
import { MyMapControl } from "./MyMapControl"

export const App = () => {
  return (
    <>
      <MyMapControl />
      <DeckGLMapboxMap />
    </>
  )
}
// MyMapControl.ts - use hooks exposed by the package to interact with the map
import React from "react"
import { useDeckGL, useUpdateViewport } from "@citydna/maps"
import { GeoJsonLayer } from '@deck.gl/layers'

const MyMapControl = () => {

  // set the initial viewport on mount as it starts at 0,0
  const { setViewport } = useUpdateViewport()
  useEffect(() => {
    setViewport(melbourneViewport)
  }, [setViewport])

  // use the hook to grab access to the global layer state
  const [, setLayers] = useDeckGL();
  const addLayer = () => setLayers([
    new GeoJsonLayer({
      id: "my-layer",
      ...
      // use this to place the layer at an index in mapbox (similar to map.addLayer secord arg of mapbox API)
      beforeId: "place-suburb"
    })
  ])

  // show a button for interactivity
  return (
    <button onClick={addLayer}>
      Add layer
    </button>
  )
}

<LegendProvider />

This is used by the CityDNA App Platform to allow apps to change the legend projected onto the city map. Docs coming soon...

Available hooks

There are serveral hooks that give you access to. These are described below.

useDeckGL()

Use this hook to access and set the deck.gl layers active across your app. It can be used in the same way as React.useState() - either by passing a value (must be an array of deck layers) or a function that returns an array of deck layers.

const [layers, setLayers] = useDeckGL();

// pass value directly
setLayers([
  new GeoJsonLayer({...})
  ])

// Use preview value. This is handy if you don't want to overwrite layers from elsewhere in your app:
setLayers(
  layers => [...layers, new GeoJsonLayer({...})]
)

You can also pass a beforeId attribute to your deck.gl Layers. This is a non-standard attribute that <DeckGLMapboxMap /> uses to figure out where to insert your layer. If it doesn't exist, it'll just drop it on top:

const [, setLayers] = useDeckGL();

// place the layer before the 'place-suburb' layer (suburb labels).
setLayers([
  new GeoJsonLayer({..., beforeId: "place-suburb"})
])

Note: you may have noticed I omitted the first destructure argument: const [, setLayers] = useDeckGL(). This is perfectly valid and helps avoid linting messages talking about unused variables.

useLegend()

This hook gives you access to the legend displayed on the citydna platform model projection from your 3rd party app. Docs coming soon...

useMapboxRef()

Gives you access to the underlying mapbox-gl instance. Use this, for example, when you want to add layers directly to the mapbox map:

const AddToMapWhenDataAvailable = () => {
  const [mapboxRef] = useMapboxRef()
  useEffect(() => {
    // may be undefined while map initialises
    if (mapboxRef && data) {
      // Mapbox GL JS docs here: https://docs.mapbox.com/mapbox-gl-js/api/
      mapboxRef.addLayer({
        id: "my-layer",
        source: "mapbox://...",
        type: "circle",
        paint: {
          "circle-color": "#e50e56",
          "circle-radius": 5,
        },
      })
    }
  }, [mapboxRef, data])

  // show the map
  return <WrappedInteractiveMap />
}

useUpdateViewport()

Gives you the ability to set your map viewport from anywhere within your app.

You're probably only concerned with:

  • setViewport
  • fitViewportToFeature
  • setGlobalBounds
const {
  fitViewportToFeature,
  setBearingAdjustment,
  setGlobalBounds,
  setViewport,
  setViewportWithoutBounds,
  setViewportInBounds,
  setZoomAdjustment,
} = useUpdateViewport()

fitViewportToFeature

A helper utility to fitting the entire map to a particular feature. This uses setViewport directly under the hood and will not respect globalBounds you've set.

const { fitViewportToFeature } = useUpdateViewport()
// padding is in pixels
fitViewportToFeature(geojsonFeature, { padding: 20 })

setViewport

setViewport is an alias for setViewportInBounds.

setViewportWithoutBounds

This uses the raw, unclamped setState for the viewport state variable under the hood. If you have bounds set and use this, it will pan the map to where you set.

setViewportInBounds

See setGlobalBounds.

This is fine in most cases:

const { setViewport } = useUpdateViewport()
setViewport({
  latitude: -37,
  longitude: 144,
  zoom: 10,
  transitionDuration: 500,
})

You can pass bounds as a second argument if you would like to override globalBounds:

const { setViewport } = useUpdateViewport()
setViewport(
  {
    latitude: -40,
    longitude: 150,
    zoom: 10,
  },
  [
    [144.9, -37.85], // Southwest coordinates
    [144.99, -37.78], // Northeast coordinates
  ]
) // viewport clamped to -37.85, 144.99

setGlobalBounds

You can use setGlobalBounds If you want to clamp the map's ability to pan to certain bounds

const { setGlobalBounds, setViewport } = useUpdateViewport()

// be sure to set your bounds somewhere and only once
useEffect(() => {
  setGlobalBounds([
    [0, 0],
    [1, 1],
  ])
}, [])

// will clamp underlying viewport to 1,1.
setViewport({
  latitude: 2,
  longitude: 2,
})

setBearingAdjustment and setZoomAdjustment

Useful when you want to slightly adjust the final projected viewport. This is a helper utility for the citydna model calibration to allow slight adjustments to fit the map.

const { setBearingAdjustment, setZoomAdjustment } = useUpdateViewport();

setViewport({
  latitude: 1,
  longitude: 1,
  zoom: 1,
  bearing: 1
})

setBearingAdjustment(0.01)
setViewportAdjustment(0.01)

const {adjustedViewport} = useViewport();
console.log(adjustedViewport.bearing, adjustedViewport.zoom) => 1.01, 1.01

useViewport()

Use this when you want to access the current viewport. This is used under the hood by the Map components to consume the global viewport settings:

const {
  viewport,
  adjustedViewport,
  globalBounds,
  zoomAdjustment,
  bearingAdjustment,
} = useViewport()

Available components

Note on tokens

react-map-gl needs a mapbox API token in order to render the map. There are a few ways you can provider this.

1. .env file

Create a .env file in the root of your project:

REACT_APP_MAPBOX_API_ACCESS_TOKEN=<<your token>>
# or
STORYBOOK_MAPBOX_API_ACCESS_TOKEN=<<your token>>

<DeckGLMapboxMap />, <WrappedInteractiveMap /> and <StaticMap /> look out for these environment variables and insert the token for you.

Never check this file into version control. Add .env to your .gitignore file. When building your app, be sure that the build process has these environment variables set too.

2. mapboxApiAccessToken prop

Alternatively, you can pass mapboxApiAccessToken as a prop directly as you would with react-map-gl:

<WrappedInteractiveMap mapboxApiAccessToken="<<your token>>" />

Note: Check <DeckGLMapboxMap /> for using staticMapProps. Note: this is discouraged as you will be checking your API key into bitbucket.

<DeckGLMapboxMap />

An all-in-one component for quickly creating maps that use deck.gl on top of react-map-gl;

<WrappedInteractiveMap />

Wraps react-map-gl's Interactive Map component with MapProvider context, controlling viewport.

<WrappedStaticMap />

Wraps react-map-gl's Static Map component with MapProvider context, controlling viewport.

Guides

Simple map

DeckGL Layers

2.5.1-develop.0

2 years ago

2.2.0

2 years ago

2.4.1

2 years ago

2.4.0

2 years ago

2.4.2

2 years ago

2.2.1-develop.0

2 years ago

2.3.1-develop.0

2 years ago

2.4.1-develop.0

2 years ago

2.2.0-develop.2

2 years ago

2.3.0-develop.2

2 years ago

2.3.0

2 years ago

2.5.0

2 years ago

2.5.1

2 years ago

2.4.3-develop.0

2 years ago

2.2.0-develop.7

2 years ago

2.4.2-develop.0

2 years ago

2.5.2-develop.0

2 years ago

2.0.0

2 years ago

2.1.1-develop.0

2 years ago

2.0.1-develop.1

2 years ago

2.0.1-develop.0

2 years ago

2.1.0-develop.4

2 years ago

2.0.0-develop.0

2 years ago

2.1.0

2 years ago

2.0.0-develop.4

2 years ago

1.12.1-develop.0

2 years ago

1.12.0

2 years ago

1.11.1-develop.0

2 years ago

1.11.0-develop.0

2 years ago

1.11.0

2 years ago

1.10.0

2 years ago

1.9.0

2 years ago

1.9.1-develop.0

2 years ago

1.9.0-develop.29

3 years ago

1.9.0-develop.21

3 years ago

1.9.0-develop.2

3 years ago

1.9.0-develop.0

3 years ago

1.8.0

3 years ago

1.7.2

3 years ago

1.7.1

3 years ago

1.7.0

3 years ago

1.5.0

4 years ago

1.4.0

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.41

4 years ago

1.0.40

4 years ago

1.0.37

4 years ago

1.0.36

4 years ago

1.0.35

4 years ago

1.0.34

4 years ago

1.0.32

4 years ago

1.0.31

4 years ago

1.0.28

4 years ago

1.0.27

4 years ago

1.0.26

4 years ago

1.0.24

4 years ago

1.0.23

4 years ago

1.0.22

4 years ago

1.0.20

4 years ago

1.0.19

4 years ago

1.0.18

4 years ago

1.0.17

4 years ago

1.0.16

4 years ago

1.0.13

4 years ago

1.0.12

4 years ago

1.0.11

4 years ago

1.0.10

4 years ago

1.0.9

4 years ago

0.0.1

4 years ago