@wq/map
======

[@wq/map]

**@wq/map** is a plugin for [@wq/app] that adds mapping capabilities.  @wq/map can leverage the [wq configuration object][config] to generate interactive maps for pages rendered via @wq/app.  The generated maps can automatically download and display GeoJSON data rendered by [wq.db's REST API][wq.db] or any third party service.

@wq/map is meant to be used with [@wq/mapbox] or [@wq/leaflet], which respectively provide integration with [Mapbox] and [Leaflet] mapping APIs.


# Installation

## wq.app for PyPI

```bash
python3 -m venv venv      # create virtual env (if needed)
. venv/bin/activate       # activate virtual env
python3 -m pip install wq # install wq framework (wq.app, wq.db, etc.)
# pip install wq.app      # install wq.app only
```

## @wq/map for npm

```bash
npm install @wq/mapbox # install @wq/mapbox, @wq/map, and deps
# npm install @wq/map  # install only @wq/map and deps
```

# API

@wq/map should be registered with @wq/app as a plugin, either directly or indirectly via [@wq/mapbox] or [@wq/leaflet].

```javascript
import app from '@wq/app';
import mapbox from '@wq/mapbox';

app.use(mapbox);  // Automatically registers @wq/map

app.init(...);
```

@wq/map includes a selection of placeholder [components](#components) (which are not exported), and exports a selection of [hooks](#hooks).

# Configuration

@wq/map has two main types of configuration.  The [global configuration](#global-configuration) applies to every map generated through the plugin.  The [page configuration](#page-configuration) specifies the layers for each route.

## Global Configuration

When used with [@wq/app], `map.init()` is called automatically by `app.init()` with the contents of `config.map`.  The configuration object includes default settings for rendered maps including initial bounds.

name | default | purpose
-----|---------|---------
`bounds` | `[[-4,-4],[4,4]]` | Default extent for initially rendered map.  This is specified as a bounds rather than a center and zoom, to ensure the full intended extent is visible regardless of screen size.
`autoZoom` | Object | By default, rendered maps will automatically zoom (and pan) to the extent of their embedded GeoJSON feature layers using the following options.  To disable auto-zooming entirely, set `autoZoom` to `false`.
`autoZoom.animate` | `true` | Whether to animate the auto-zooming.  Incorporating animation is valuable as it gives the user a chance to visually orient the rendered features in relation to the original zoom level.
`autoZoom.wait` | `0.5` | How long to wait before triggering auto-zooming, in seconds.  Waiting gives the map a chance to settle and makes the animation more salient.
`autoZoom.sticky` | `true` | Whether to save the last zoom and center (from auto-zooming and/or regular panning) for use in the next map (`true`) or to always start out new maps from the default zoom and center (`false`).  Particularly useful in maintaining visual consistency for the user when they are quickly navigating between a series of list or detail pages in succession.
`autoZoom.maxZoom` | `13` | The maximum zoom level to use when auto-zooming.  (Useful to avoid zooming in too far when the only feature is a single point)
`maps.basemaps` | Array | Basemap configuration(s) to use on every map.  The first entry the array will be used as the default basemap. Each object should have a `name` attribute (for the [Legend](#general-components)) and a `type` specifying which [basemap component](#basemap-components) to use.  All other object props will be passed to the component.

## Page Configuration

Each page in the [wq configuration object][config] can optionally have one or more configured maps.  These should be specified via the `map` property for the page:

```javascript
{
  "pages": {
    "item": {
       "url": "items",
       "list": true,
       "form": [ ... ],
       "map": [
         {"mode": "list", "layers": [...]},
         {"mode": "detail", "layers": [...]},
         {"mode": "edit", "layers": [...]}
      ]           
    }
  }
}
```

name | default | purpose
-----|---------|---------
`mode` | `defaults` | [Route mode][@wq/router] that this configuration applies to.  Typically one of `list`, `detail`, or `edit`.  If set to `all` or `defaults`, the defined configuration will be mixed together with any other applicable configuration when rendering mode-specific maps.  You can also use `all` or `defaults` to define maps for simple (non-list) pages, which do not use rendering modes.
`map` | `main` | Whether this configuration applies to the default (main) map or to a secondary map on the same screen.
`layers` | See `autoLayers` | Array of overlay configurations to apply to the map.  Each object should have a `name` attribute (for the [Legend](#general-components)) and a `type` specifying which [overlay component](#overlay-components) to use.  All other object props will be passed to the component.
`autoLayers` | `true` | If `true`, the maps created for the page will automatically include a default `"geojson"` layer corresponding to [the page URL and mode][url-structure], as well as any explicitly defined layers.
`autoZoom` | global setting | Set to `false` to disable auto-zooming on a per-map basis.
`minBounds` | none | Minimum bounds to set when auto-zooming.
# Components

@wq/map provides placeholder components for defining a [model-driven][config] map with overlays and basemaps.  Like [@wq/react], the components are grouped into categories:

plugin key | description
--|--
[components](#general-components) | High-level map components (Map, Legend, etc.)
[basemaps](#basemap-components) | Basemap layers, typically tiled imagery or road network
[overlays](#overlay-components) | Overlay layers, such as GeoJSON vectors
[inputs](#input-components) | Geospatial input components for [geo field types][field-types]

To override components in any of these categories, register a custom plugin with the corresponding key.  As with [@wq/react][react-components], the value should be an object mapping component names to components.  A plugin name is not required, so it can be convenient to create an `[category]/index.js` that exports all componenents, then register it via an anonymous object:

```javascript
// src/index.js
import app from '@wq/app';
import mapbox from '@wq/mapbox';
import components from './components';
import overlays from './overlays';

app.use(mapbox)
app.use({ components, overlays });
app.init(...);

// src/components/index.js
import Legend from './CustomLegend';
export default {
    Legend
}

// src/overlays/index.js
import Geojson from './CustomGeoJSONLayer';
export default {
    Geojson,
}
```

## General Components

The components below should not generally need to be overridden except in advanced cases.  Instead, most customization be done through [configuration](#configuration) or by registering custom [Basemap](#basemap-components) and [Overlay](overlay-components) components.

name | details
--|--
[AutoMap] | Reads the [map configuration](#configuration) corresponding to the current route and renders a `<Map>` with the appropriate inputs and controls
[AutoBasemap] | Selects the appropriate basemap component based on the [global basemaps definition](#global-configuration)
[AutoOverlay] | Selects the appropriate overlay component based on the [layer configuration](#page-configuration)
[Map]* | Top level component that renders the root component from the map engine
[Legend]* | Renders the map engine's Legend component (if applicable), with BasemapToggle/OverlayToggle as children
[BasemapToggle][Legend]* | Wraps AutoBasemap with a radio switch that controls active state
[OverlayToggle][Legend]* | Wraps AutoOverlay with a checkbox that controls active state
MapInteraction* | Placeholder for map tools and effects (such as zoom controls)

Components marked with * are placeholders for implementations provided by [@wq/mapbox] and [@wq/leaflet].  The typical component hierarchy generated by `<AutoMap/>` is as follows:

```jsx
<AutoMap>
  <Map>
    <MapInteraction />
    <Legend>
      <BasemapToggle>
        <AutoBasemap>
          <Tile />
        </AutoBasemap>
      </BasemapToggle>
      <OverlayToggle>
        <AutoOverlay>
          <Geojson />
        </AutoOverlay>
      </OverlayToggle>
    </Legend>
  </Map>
</AutoMap>
```

## Basemap Components

Basemap layers are specified as part of the [global configuration](#global-configuration).  Only one Basemap component can be active at a time, and it is drawn below all other layers.  @wq/map provides a placeholder `Tile` component which is overridden by [@wq/mapbox] and [@wq/leaflet].  [@wq/mapbox] also provides a `VectorTile` basemap component, while [@wq/leaflet]'s plugins include other basemaps for use with specific mapping services.

config name | component | details
--|--|--
tile | Tile | Raster tile layer, typically with 256x256 tile images in "Web Mercator" projection
vector-tile | VectorTile | Vector tile layer (@wq/mapbox only)
empty | Empty | Provides an option to completely disable the basemap.
group | Group | Treats a group of related layers as a single basemap.  The configuration for the group should specify a `layers` array containing one or more basemap layer configurations.

## Overlay Components

Overlays are specified as part of the [page configuration](#page-configuration).  All overlays are active initially (unless disabled), and are drawn over the basemap.  @wq/map provides placeholder overlay components which are overridden by [@wq/mapbox] and [@wq/leaflet].

config name | component | details
--|--|--
geojson | Geojson | GeoJSON overlay.  If a URL is provided it will be retrieved and loaded.
empty | Empty | Non-rendered layer that essentially is just to provide a toggle-able entry in the legend.  (Typically used with a custom component somewhere else in the tree that calls [`useMapState()`](#hooks) and renders accordingly)
group | Group | Treats a group of related layers as a single overlay.  The configuration for the group should specify a `layers` array containing one or more overlay configurations.
n/a | Highlight | GeoJSON overlay with preset highlight styles, which renders the contents (if any) of `useMapState().highlight`
n/a | Draw | Loads the corresponding map engine's "Draw" library

## Input Components

@wq/map provides an implementation of the geospatial [XLSForm field types][field-types].  The components are essentially just [Formik]-aware wrappers for the `Draw` component provided by the map engine.

Name | XLSForm Types | Notes
--|--|--
[Geo] | geopoint, geotrace, geoshape | For integration with [@wq/material] and other [@wq/react]-based renderers.
[EmbeddedGeo] | geopoint, geotrace, geoshape | For integration with [@wq/jquery-mobile] and other non-React renderers.

> Note: The applicable component for the selected UI renderer is automatically registered via `app.use(map)`, so no extra configuration is needed.  (Specifically, the [@wq/map plugin] specifies `inputs: { Geopoint: Geo, ... }` for [@wq/react], and a `run($page)` for [@wq/jquery-mobile] that attaches a React root with `EmbeddedGeo` within the jQuery Mobile page.)   

# Hooks

@wq/map exports a few [React hooks] that can be used to access various parts of the application state and plugin framework.

```javascript
import { useEffect } from 'react';
import { useMapInstance } from '@wq/map';

export default function CustomMapInteraction() {
    const mapInstance = useMapInstance();
    useEffect(() => {
        if (!mapInstance) {
            return;
        }
        instance.on('moveend', handleMove);
        function handleMove() {
            // ...
        }
        return () => mapInstance.off('movend', handleMove);
    }, [mapInstance]);
}
```

hook | description | example
--|--|--
useMapState() | Current basemap, active overlays, and highlight | `mapState.overlays.map({name} => <Text...>)`
useMapInstance() | Current instance of map engine.  Note that the available attributes depend on the underlying engine | `mapInstance.on(...)`
useGeoJSON(url) | Load GeoJSON data from the specified URL (via `useEffect()`), triggering a second render when the data loads | `geojson && geojson.features.map(...)`

[@wq/map]: https://github.com/wq/wq.app/tree/master/packages/map
[@wq/map plugin]: https://github.com/wq/wq.app/tree/master/packages/map/src/map.js

[@wq/app]: https://wq.io/docs/app-js
[@wq/router]: https://wq.io/docs/router-js
[@wq/react]: https://github.com/wq/wq.app/tree/master/packages/react
[@wq/material]: https://github.com/wq/wq.app/tree/master/packages/material
[@wq/mapbox]: https://github.com/wq/wq.app/tree/master/packages/mapbox
[@wq/leaflet]: https://github.com/wq/wq.app/tree/master/packages/leaflet
[@wq/jquery-mobile]: https://github.com/wq/wq.app/tree/master/packages/jquery-mobile

[Mapbox]: https://docs.mapbox.com/mapbox-gl-js/
[Leaflet]: https://leafletjs.com/
[wq.db]: https://wq.io/wq.db
[config]: https://wq.io/docs/config
[react-components]: https://github.com/wq/wq.app/tree/master/packages/react#components
[field-types]: https://wq.io/docs/field-types
[url-structure]: https://wq.io/docs/url-structure

[React]: https://reactjs.org
[React hooks]: https://reactjs.org/docs/hooks-overview.html
[Formik]: https://formik.org

[Map]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/Map.js
[AutoMap]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/AutoMap.js
[AutoBasemap]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/AutoBasemap.js
[AutoOverlay]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/AutoOverlay.js
[Legend]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/Legend.js
[Geo]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/inputs/Geo.js
[EmbeddedGeo]: https://github.com/wq/wq.app/blob/master/packages/map/src/components/inputs/EmbeddedGeo.js
