Home

@wq/map

wq version: 1.1 1.2/1.3
Docs > wq.app: Modules

wq/map.js

wq/map.js

wq/map.js is a wq/app.js plugin that adds mapping capabilities. wq/map.js can leverage the wq configuration object to generate Leaflet maps for pages rendered via wq/app.js. The generated maps can automatically download and display GeoJSON data rendered by wq.db's REST API.

Overview

As a plugin for wq/app.js, wq/map.js utilizes similar concepts and conventions, with corresponding constraints on how it can be used. In particular, each map generated by wq/map.js always corresponds to a page defined in the wq configuration object with a "map" property defined. For model-backed pages ("list": true), wq/map.js' maps can distinguish between "list" views, "detail" views, and "edit" views.

wq/map.js leverages three configuration objects:

A default map configuration can be assigned by setting map: true in the page configuration. Each map will automatically get a single overlay configured with the GeoJSON equivalent of the page:

For example, the content you are reading is a Doc instance with an identifier of map-js, rendered in a "detail" view for the docs list page. docs is registered in the wq configuration for this website as "map": true. Since wq/map.js is loaded for this website, the map at the top of this document should have automatically loaded /docs/map-js.geojson when the page opened.

The default GeoJSON layers should work as long as your webserver is running wq.db or a service with a compatible URL Structure.

Configuration

The map plugin requires both a global configuration and a per-page configuration for pages that need maps.

// myapp/main.js
define(['wq/app', 'wq/map', './config'],
function(app, map, config) {

// In myapp/config.js or in wq.db.rest registration:
// config.map = { ... }
// config.pages[page].map = { ... }

app.use(map);

app.init(config).then(function() {
    app.jqmInit();
    app.prefetchAll();
});

});

The map module provides the following configuration options.

Global Configuration

map.init() is called automatically by app.init() with the contents of config.map. The configuration object 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)
icon Object Default icon settings for use with map.createIcon(). The "default" default icon settings correspond to the default icon created by Leaflet (L.Icon.Default).
basemaps Array Basemap configuration to use on every generated map. The name attribute will be used as the basemap name in the layers control, while the type specifies the name of a layer creation function to use when creating the basemap. All other options will be passed to the layer creation function. One basemap type is preregistered: tile (which corresponds to L.tileLayer()).

Customizing the Basemap

The default basemap configuration uses the free Stamen Terrain layer:

config.map.basemaps = [
    {
        'name': "Stamen Terrain",
        'type': 'tile',
        'url': '//stamen-tiles-{s}.a.ssl.fastly.net/{layer}/{z}/{x}/{y}.jpg',
        'layer': 'terrain',
        'attribution': 'Map tiles by Stamen Design ...'
    }
];

If you would like to change the basemap (for example, to incorporate aerial imagery), you will need to customize this configuration to incorporate map tiles from another source. There are a number of services available (some free for small projects), but nearly all now require an API key. For example, after obtaining an API key from MapBox, you might do something like this:

var attrib = 'Map data ...',
    token = '(insert API key)',
    cdn = 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}';

config.map = {
    'basemaps': [{
        'name': 'MapBox Streets',
        'type': 'tile',
        'url': cdn,
        'id': 'mapbox.streets',
        'accessToken': token,
        'attribution': attrib
    }, {
        'name': 'MapBox Satellite',
        'type': 'tile',
        'url': cdn,
        'id': 'mapbox.satellite',
        'accessToken': token,
        'attribution': attrib
    }]
};

If you want to keep your API token out of your version control, you can put it in a separate unversioned AMD module and/or include it in your local_settings.py and register it with your router (as in the code for this website).

If you have an ArcGIS license, you can integrate ESRI basemaps by loading wq/mapserv.js rather than wq/map.js. wq/mapserv.js is a variant of wq/map.js with additional basemap types corresponding to Esri Leaflet layer creation functions. For example, to register the ESRI Topograpic, Streets, and Imagery layers, you could do something like the following:

config.map = {
    "basemaps": [
        {   
            "name": "ESRI Topographic",
            "type": "esri-basemap",
            "layer": "Topographic",
        },
        {   
            "name": "ESRI Streets",
            "type": "esri-basemap",
            "layer": "Streets",
        },
        {   
            "name": "ESRI Imagery",
            "type": "esri-basemap",
            "layer": "Imagery",
            "labels": true,
        }
    ]
}

Additional basemap types (e.g. from other Leaflet plugins) can be incorporated with map.addBasemapType() (see below).

Full Example

// config.js
define(['data/config'], function(config) {

// (set template defaults, transitions, store)
// ...

// set map config defaults
config.map = {
    'bounds': [
        [44.78, -93.1],
        [45.18, -93.5]
    ],
    'basemaps': [
        // custom basemap definition
    ]
});

return config;
});
// myapp.js

define(['wq/app', 'wq/map', './config'],
// to enable ESRI layers:
// define(['wq/app', 'wq/mapserv', './config'],

function(app, map, config) {
    app.use(map);
    app.init(config);
});

Individual Map Configuration

After initialization, map.config.maps will be populated with map configurations for each page in the wq configuration object with a "map" property defined. If a page's map property is defined an object, that object will be used as the map configuration. If it is an array of objects, multiple map configurations will be defined (see below). If the property is simply true, a default set of map configurations will be created with URL-based GeoJSON overlays (as described above).

Multiple Configurations

wq/map.js supports defining separate map configurations for the "list", "detail", and "edit" modes of list pages. As of version 1.0, wq/map.js also supports custom page modes, as well as placing multiple maps in different locations on the same screen.

To make it easier to manage all of these possible variations, the map configuration is now defined as an array rather than as a deeply nested object. Each object in the array can have the following attributes:

name default purpose
mode defaults Template rendering mode for which 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.

Configuration Options

In addition to the mode and map attributes, each map configuration can have the following options:

name default purpose
layers See below An array of layer configurations to use for this rendering mode.
autoLayers true If true, the maps created for the page will automatically include a default "geojson" layer as discussed above, 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. Should be a Leaflet LatLngBounds or compatible array.
noLayerControl false Set to true to disable the default layer control.
onshow none Function to call after map is created with map.createMap(). The function will be passed the newly created L.Map object.
div [page]-map The id of the <div> tag to place the Leaflet map into. The div should be present in the template in order for the automatic map creation to work. By default the expected div id will be [page]-map for list views, [page]-[itemid]-map for detail views, and [page]-[itemid]-edit-map for edit views. Like all Leaflet maps, the height of the div should be explicitly specified in an attribute or in CSS.

Example

/* main.css */
.my-map-class {
  border: 1px solid black;
  height: 300px;
}
<!-- model_list.html -->
<div id="model-map" class="my-map-class"></div>
<!-- model_detail.html -->
<div id="model-{{id}}-map" class="my-map-class"></div>

Layer Configuration

Each map configuration should have one or more layer configurations added to it. The layer configurations are defined as layers, an array property on the map configuration. Layers can also be added programmatically via map.addLayerConf(), which takes the name of an existing map and a layer configuration to add to it. However, it is recommended to define all layers as part of the initial layer configuration. With wq.db.rest, this can be done by specifying a map property when registering the model:

# myapp/rest.py
from .models import MyModel
app.router.register_model(
    MyModel,
    fields="__all__",
    map=[{
        'mode': 'all',
        ...
    }, {
        'mode': 'list',
        'layers': [...],
    }, {
        'mode': 'detail',
        'layers': [...]
    }]
)

A layer configuration consists of the following options:

name purpose
name The name of the layer to show in the layer list
type The type of overlay to use. The default is "geojson", which is the only built-in type. Other overlay types can be registered via map.addOverlayType() (see below).

The built-in "geojson" layer type recognizes the following options:

name purpose
url The path to a geojson file to download (including the .geojson extension). This can be defined via a template syntax (as seen in the example below).
icon The name of an icon to use, or a template that will compile to an icon name, or a function returning an icon name. If a template or a function, it will be called with the feature.properties for each feature in the dataset. Icon names should first be registered via map.createIcon() (see below).
popup The name of a popup template to use when rendering marker popups. See map.renderPopup() below
cluster Boolean indicating whether or not to cluster markers. The default for auto-generated layers is true as long as the Leaflet.markercluster plugin is present. A copy of the plugin is included with wq.app but is not imported by default.
draw Options to use for layer editing via Leaflet.draw. See Map Editing below.
geometryField The name of a hidden field to save edited geometry to. See Map Editing below.
style A function to define styles based on the properties of each feature in the GeoJSON. The function should take a feature and return a style object. Available style options are listed in the documentation for L.Path. Equivalent to L.GeoJSON's style option.
oneach A function to call for each feature in the GeoJSON. The function should take a Leaflet layer object and a GeoJSON feature. map.renderPopup() can automatically create a compatible function that will attach a templated popup to each layer using the properties in the GeoJSON feature. Equivalent to L.GeoJSON's onEachFeature option.
clusterIcon CSS class to use when creating the cluster icon <div>. Can be a plain string, a template definition, or a function. If a template or a function, a context of the form {'count': count, [size]: true} will be provided, where [size] is one of large (> 100), medium (> 10), or small.

Example

config.pages[pagename].map = [
    {
        "mode": "list",
        // "autoLayers": true,
        "layers": [{
            'name': pagename,
            'type': 'geojson',
            'url': '{{{url}}}.geojson',
            'popup': pagename,
            'cluster': true
        }]
    }, {
        "mode": "detail":,
        // "autoLayers": true,
        "layers": [{
            'name': pagename,
            'type': 'geojson',
            'url': pageurl + '/{{{id}}}.geojson',
            'popup': page
        }]
    }, {
        "mode": "edit",
        // "autoLayers": true,
        "layers": [{
            'name': pagename,
            'type': 'geojson',
            'url': pageurl + '/{{{id}}}/edit.geojson',
            'draw': {
                'polygon': {},
                'polyline': {},
                'marker': {},
                'rectangle': {},
                'circle': false
            }
        }]
    }
];

Map Editing

wq/map.js supports editing layers via the Leaflet.draw plugin. This functionality can be enabled by setting a draw attribute on the map's edit mode configuration. The draw configuration will be passed on to the Leaflet draw control to enable different drawing types.

The draw functionality is meant to work in close integration with wq.db.rest - particularly to edit LocatedModels and other models with geometry fields. The basic workflow is like this:

  1. User navigates to /mymodel/1234/edit
  2. wq/map.js loads /mymodel/1234/edit.geojson and displays it on a Leaflet.draw-enabled map
  3. User makes edits, which are serialized as a FeatureCollection to a hidden geometry or locations field in the form (this can be customized with the geometryField layer configuration option).
  4. User posts form to server, which parses and stores the geometry field, potentially splitting the geometries into multiple Location instances.

Note that there is an asymmetry between how the geographic data is initially loaded (edit.geojson) and how it is saved (form field). This is primarily to avoid needing to store the entire geographic dataset in offline storage. However, there are workarounds available if offline geographic data storage is needed.

Advanced Usage

It is often necessary to define additional layer or icon types for use with the map configuration above. These options need to be configured via JavaScript.

map.addBasemapType(name, function)

map.addBasemapType can be used to add custom basemap types in addition to the built in "tile" type. The provided function should accept a basemap configuration and return a Leaflet layer instance. For example, the built-in "tile" layer type is registered as follows:

map.addBasemapType('tile', function(layerConf) {
    return L.tileLayer(layerConf.url, layerConf);
});

The wq/mapserv.js module provides additional examples of custom basemap types.

map.addOverlayType(name, function)

map.addOverlayType can be used to add custom overlay types in addition to the built in "geojson" type. The provided function should accept an overlay configuration and return a Leaflet layer instance. For example, to create a WMS overlay with leaflet.wms you could do the following:

map.addOverlayType('wms', function(layerConf) {
    var wmsSource = wms.source("/url/to/wms", {
        'format': 'image/png',
    });
    return wmsSource.getLayer(layerconf.layer);
});

The wq/mapserv.js module provides additional examples of custom overlay types.

map.createIcon(name, options)

map.createIcon defines and names an L.Icon for later use in layer configurations. The function accepts a string name and an object containing options for the icon. Options are the same as those for L.Icon, but with a number of built-in defaults. These defaults are optimized to make it trivial to define icons that have the same dimensions and shadow as Leaflet's default icon:

name default
iconSize [25, 41]
iconAnchor [12, 41]
popupAnchor [1, -34]
shadowSize [41, 41]
shadowUrl L.Icon.Default.imagePath + '/marker-shadow.png'

Example

map.createIcon("green", {'iconUrl': "/images/green.png"});

map.createBasemaps()

map.createBasemaps() gemerates the actual basemap instances for use in every map generated by wq/map.js. The function returns an object where the keys are layer names and the values are layer objects (usually L.TileLayer). The basemaps will show up in the layer control generated for each map. Generally, you shouldn't need to call or override this function directly - instead, customize the global basemaps configuration and/or register custom basemap types with addBasemapType().

map.createLayerControl(basemaps, layers)

map.createLayerControl() is a simple hook to allow customization of the default layer control added to every map generated by wq/map.js. The function takes two arguments; the basemaps from map.createBasemaps(), and the GeoJSON layers created by map.createMap() (using information from map.getLayerConfs()). The default implementation simply passes the arguments on to, and returns, a L.Control.Layers instance.

map.renderPopup(page)

map.renderPopup(page) generates a callback function suitable for use as the oneach argument to a layer configuration, or as the onEachFeature option in L.GeoJSON. (Note that a configuration of oneach: map.renderPopup(page) can be replaced with popup: page.) The function will render a popup using the template with the name [page]_popup. The template should be included among the templates provided to wq/app.js' init() function. The template context will be the GeoJSON properties on each feature. The example map at the top of the page uses the following template:

<!-- doc_popup.html -->
<h3>{{label}}</h3>
<table>
  <tr><th>Chapter:</th><td>{{chapter_label}}</td></tr>
  <tr><th>Last Updated:</th><td>{{updated_label}}</td></tr>
  <tr>
    <th>Interactive:</th>
    <td>
      {{#interactive}}&check;{{/interactive}}
      {{^interactive}}&cross;{{/interactive}}
    </td>
  </tr>
</table>

map.loadLayer(url, callback)

map.loadLayer() is used retrieve the actual GeoJSON data for "geojson" overlay types. The default implementation caches each GeoJSON object, so you can call map.loadLayer() to prefetch layers that you expect to appear in later maps. The url argument is assumed to be relative to the webservice root used by wq/store.js.

@wq/app
@wq/model