map-js
@wq/map
@wq/map is a @wq/app plugin that adds mapping capabilities. @wq/map can leverage the wq configuration object to generate Leaflet maps for pages rendered via @wq/app. The generated maps can automatically download and display GeoJSON data rendered by wq.db's REST API.
Overview
As a plugin for @wq/app, @wq/map utilizes similar concepts and conventions, with corresponding constraints on how it can be used. In particular, each map generated by @wq/map always corresponds to a page defined in the wq configuration object with a "map" property defined. For model-backed pages ("list": true), @wq/map' maps can distinguish between "list" views, "detail" views, and "edit" views.
@wq/map leverages three configuration objects:
Global defaults, such as basemaps and initial geographic bounds. See Global Configuration below for options.
Map configurations, which each correspond to a page configuration. See Map Configuration below for options.
Layer configurations, which describe individual overlay layers in a map configuration. See Layer Configuration below for options.
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:
The default layer for a simple page (e.g /simple) will be /simple.geojson
The default layer for a list page's list view (e.g. /list/) will be /list.geojson
The default layer for a list page's detail view (e.g. /list/itemid) will be /list/itemid.geojson
The default layer for a list page's edit view (e.g. /list/itemid/edit) will be /list/itemid/edit.geojson
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 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.
Installation
wq.app for PyPI
@wq/app for npm
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
npm install @wq/map
API
Initialization
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();
});
// src/index.js
import app from '@wq/app';
import map from '@wq/map';
import config from './config';
// In src/config.js or in wq.db.rest registration:
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':
'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 =
config.map = {
'basemaps': [{
'name': 'MapBox Streets',
'url': cdn,
'id': 'mapbox.streets',
'accessToken': token,
'attribution': attrib
}, {
'name': 'MapBox Satellite',
'id': 'mapbox.satellite',
}]
};
If you want to keep your API token out of your version control, you can put it in a separate unversioned JavaScript 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/map:mapserv rather than @wq/map. @wq/map:mapserv is a variant of @wq/map 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:
"basemaps": [
"name": "ESRI Topographic",
"type": "esri-basemap",
"layer": "Topographic",
},
"name": "ESRI Streets",
"layer": "Streets",
"name": "ESRI Imagery",
"layer": "Imagery",
"labels": true,
]
Additional basemap types (e.g. from other Leaflet plugins) can be incorporated with map.addBasemapType() (see below).
Full Example
// myapp/config.js
define(['data/config'], function(config) {
// (set template defaults, transitions, store)
// ...
// set map config defaults
'bounds': [
],
'basemaps': [
// custom basemap definition
return config;
// to enable ESRI layers:
// define(['wq/app', 'wq/mapserv', './config'],
app.init(config);
// src/config.js
import config from './data/config';
export default config;
// import {mapserv as map} from '@wq/map';
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 supports defining separate map configurations for the "list", "detail", and "edit" modes of list pages. As of version 1.0, @wq/map 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:
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:
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',
'mode': 'detail',
)
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:
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.
config.pages[pagename].map = [
"mode": "list",
// "autoLayers": true,
"layers": [{
'name': pagename,
'type': 'geojson',
'url': '{{{url}}}.geojson',
'popup': pagename,
'cluster': true
"mode": "detail":,
'url': pageurl + '/{{{id}}}.geojson',
'popup': page
"mode": "edit",
'url': pageurl + '/{{{id}}}/edit.geojson',
'draw': {
'polygon': {},
'polyline': {},
'marker': {},
'rectangle': {},
'circle': false
Map Editing
@wq/map 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 models with geometry fields. The basic workflow is like this:
User navigates to /mymodel/1234/edit
@wq/map loads /mymodel/1234/edit.geojson and displays it on a Leaflet.draw-enabled map
User makes edits, which are serialized as a FeatureCollection to a hidden geometry field in the form (this can be customized with the geometryField layer configuration option).
User posts form to server, which parses and stores the geometry field.
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/map:mapserv 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/map:mapserv 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'
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. 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. 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' 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 -->
<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.
@wq/map:locate
@wq/map:locate is a @wq/app plugin providing utilities for requesting the user's latitude and longitude, a common use case in many VGI, citizen science, and crowdsourcing applications. @wq/map/locate is designed to be used together with @wq/map.
Once registered, the locate plugin populates form <input>s from a Leaflet map to facilitate multiple ways of providing location information (e.g. GPS or a map click).
define(['wq/app', 'wq/map', 'wq/locate', './config'],
function(app, map, locate, config) {
// config.locate = { ... };
// config.pages[page].map = { ... };
// config.pages[page].locate = true;
app.use(locate); // Should be registered after map
import map, { locate } from '@wq/map';
Usage
The Locator widget provides three modes for entering location information:
gps: Request position using device GPS or network location, if available. Also returns GPS "accuracy" in meters. To get more accurate measurements, the gps mode of the Locator widget continually polls location until the form is saved or a different mode is selected.
interactive: Allow user to click the map directly. Accuracy is computed based on zoom level.
manual: Allow user to manually enter their latitude and longitude.
The entered coordinates are automatically displayed on a Leaflet map. "Accuracy" is represented as a circle with the radius of the accuracy.
Tip: Save Accuracy In Your Database!
The accuracy number is a critical part of the location information - so don't discard it and only save the latitude and longitude. Remember that accuracy is fundamentally different than precision: just because a GPS device returns latitude and longitude specified out to 9 decimal places, does not mean the user is actually at that exact location. In fact, the initial measurement returned by many consumer GPS devices will often be "precise" but inaccurate, perhaps by several kilometers. The accuracy measurement is thus an imperfect, but useful metric for evaluating the location information. Keeping the GPS on for as long as possible is one way to get more accurate measurements, but it is still important to save the information in the database for future reference.
Similarly, having a user tap the map to specify their location is practically guaranteed to provide inaccurate results unless they zoom in first. For this reason, the interactive mode approximates an accuracy measurement based on zoom level (accuracy = 2 pixels of screen space converted to meters based on the zoom level). Accuracy can serve as a reminder to the user to zoom in, or even to enforce a minimum level of accuracy in your form processing logic.
The Locator widget searches for the following fields in the form. All fields are technically optional, though you will probably want at least latitude and longitude and/or geometry be present.
field name | purpose
latitude | A text input that will receive (or provide) the latitude
longitude | A text input that will receive (or provide) the longitude
geometry | A hidden input that will receive booth coordinates as a simple GeoJSON point
accuracy | A text input that will receive the computed accuracy
toggle | A set of radio buttons (or a select menu) that will change the widget mode. The values for each option should be one or more of the modes listed above.
mode | A hidden input that will receive the selected mode (in the case where toggle field is not used or is not saved)
source | A text input that will recieve information about the source of the GPS coordinate, if known. (For use with "gps" mode in PhoneGap/Cordova applications with cordova-plugin-bluetooth-geolocation installed).
If any of these fields are named differently in your application, define config.locate.fieldNames as follows:
config.locate = {
"fieldNames": {
"toggle": "toggle-btn",
"latitude": "lat",
"longitude": "lng",
"accuracy": "accuracy"
config.locate can also be used to register up to three callback functions that will be executed at various points in the process:
onSetMode(mode): Called whenever the locator mode changes, e.g. in response to the user clicking the toggle button.
onUpdate(location, accuracy): Called when ever a new location is determined.
onError(event): Called when GPS lookup fails.
GPS Interactive Manual
Latitude
Longitude
Accuracy (m)
JS
app.use(locate);
// Custom handler for location updates
'onUpdate': function(loc, accuracy) {
if (accuracy > 1000) {
$('#message').html(
"Note: your location accuracy appears to be off by more than 1km."
);
} else {
HTML
<fieldset data-role="controlgroup" data-type="horizontal">
<input type='radio' value='gps' id='loc-gps' name='toggle'>
<label for='loc-gps'>GPS</label>
<input type='radio' value='interactive' id='loc-interactive' name='toggle'>
<label for='loc-interactive'>Interactive</label>
<input type='radio' value='manual' id='loc-manual' name='toggle'>
<label for='loc-manual'>Manual</label>
</fieldset>
<div id='map-div-id'></div>
<div class='ui-grid-b'>
<div class='ui-block-a ui-content'>
<input name="latitude" type="number" step="0.0001">
</div>
<div class='ui-block-b ui-content'>
<input name="longitude" type="number" step="0.0001">
<div class='ui-block-c'>
<input name="accuracy" type="number" step="0.0001">
Tip: Keep GPS running for better results!
When requesting the user's location in a web app, it's generally better to use Geolocation.watchPosition() than Geolocation.getCurrentPosition(), even when you only need a single point and not a GPS trace. The reason for this is that the first result returned by the GPS may be inaccurate, and the longer the GPS is on, the more time it has to lock on to the satellites. For this reason, @wq/map/locate continues requesting the GPS location until the user saves the form and/or navigates to another page. This is accomplished by setting watch: true in the underlying call to L.Map.locate().
@wq/map is a @wq/app plugin that adds mapping capabilities.
@wq/map ====== [@wq/map] **@wq/map** is a [@wq/app plugin] that adds mapping capabilities. @wq/map can leverage the [wq configuration object] to generate [Leaflet] maps for pages rendered via @wq/app. The generated maps can automatically download and display GeoJSON data rendered by [wq.db]'s [REST API].
## Overview As a plugin for [@wq/app], @wq/map utilizes similar concepts and conventions, with corresponding constraints on how it can be used. In particular, each map generated by @wq/map always corresponds to a page defined in the [wq configuration object] with a `"map"` property defined. For model-backed pages (`"list": true`), @wq/map' maps can distinguish between "list" views, "detail" views, and "edit" views. @wq/map leverages three configuration objects: * Global *defaults*, such as basemaps and initial geographic bounds. See Global Configuration below for options. * *Map configurations*, which each correspond to a page configuration. See Map Configuration below for options. * *Layer configurations*, which describe individual overlay layers in a map configuration. See Layer Configuration below for options. 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: * The default layer for a simple page (e.g `/simple`) will be `/simple.geojson` * The default layer for a list page's list view (e.g. `/list/`) will be `/list.geojson` * The default layer for a list page's detail view (e.g. `/list/itemid`) will be `/list/itemid.geojson` * The default layer for a list page's edit view (e.g. `/list/itemid/edit`) will be `/list/itemid/edit.geojson` > 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 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]. ## 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/app for npm ```bash npm install @wq/map ``` ## API ### Initialization The map plugin requires both a global configuration and a per-page configuration for pages that need maps. ### wq.app for PyPI ```javascript // 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(); }); }); ``` ### @wq/app for npm ```javascript // src/index.js import app from '@wq/app'; import map from '@wq/map'; import config from './config'; // In src/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: ```javascript config.map.basemaps = [ { 'name': "Stamen Terrain", 'type': 'tile', 'url': '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: ```javascript var attrib = 'Map data ...', token = '(insert API key)', cdn = 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 JavaScript 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/map:mapserv` rather than `@wq/map`. `@wq/map:mapserv` is a variant of `@wq/map` 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: ```javascript 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 ### wq.app for PyPI ```javascript // myapp/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/main.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); }); ``` ### @wq/app for npm ```javascript // src/config.js import config from './data/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 ] }); export default config; // src/index.js import app from '@wq/app'; import map from '@wq/map'; // to enable ESRI layers: // import {mapserv as map} from '@wq/map'; import config from './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 supports defining separate map configurations for the "list", "detail", and "edit" modes of list pages. As of version 1.0, @wq/map 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 `
` 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 ```css /* main.css */ .my-map-class { border: 1px solid black; height: 300px; } ``` ```xml
``` ```xml
``` ### 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: ```python # 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 `
`. 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 ```javascript 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 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 models with `geometry` fields. The basic workflow is like this: 1. User navigates to /mymodel/1234/edit 2. @wq/map 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` 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. 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: ```javascript map.addBasemapType('tile', function(layerConf) { return L.tileLayer(layerConf.url, layerConf); }); ``` The [@wq/map:mapserv] 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: ```javascript map.addOverlayType('wms', function(layerConf) { var wmsSource = wms.source("/url/to/wms", { 'format': 'image/png', }); return wmsSource.getLayer(layerconf.layer); }); ``` The [@wq/map:mapserv] 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 ```javascript 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. 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. 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]' `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: ```xml
{{label}}
Chapter: | {{chapter_label}}
Last Updated: | {{updated_label}}
Interactive: | {{#interactive}}✓{{/interactive}} {{^interactive}}✗{{/interactive}}
``` ### `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]. [@wq/map]: https://github.com/wq/wq.app/blob/master/packages/map [@wq/app plugin]: https://wq.io/docs/app-plugins [wq configuration object]: https://wq.io/docs/config [Leaflet]: http://leafletjs.com [wq.db]: https://wq.io/wq.db [REST API]: https://wq.io/docs/about-rest [@wq/app]: https://wq.io/docs/app-js [router.addRoute()]: https://wq.io/docs/router-js [@wq/app config object]: https://wq.io/docs/app-js [URL Structure]: https://wq.io/docs/url-structure [LatLngBounds]: http://leafletjs.com/reference.html#latlngbounds [L.Path]: http://leafletjs.com/reference.html#path [style]: http://leafletjs.com/reference.html#geojson-style [onEachFeature]: http://leafletjs.com/reference.html#geojson-oneachfeature [iconCreateFunction]: [L.Icon]: http://leafletjs.com/reference.html#icon [MapQuest-OSM and MapQuest Open Aerial]: http://developer.mapquest.com/web/products/open/map [L.Control.Layers]: http://leafletjs.com/reference.html#control-layers [@wq/store]: https://wq.io/docs/store-js [wq.db.rest]: https://wq.io/docs/about-rest [leaflet.wms]: https://github.com/heigeo/leaflet.wms [Leaflet.draw]: https://github.com/leaflet/leaflet.draw [Stamen Terrain]: http://maps.stamen.com/terrain/ [MapBox]: https://mapbox.com/ [code for this website]: [Esri Leaflet]: https://esri.github.io/esri-leaflet/ [@wq/map:mapserv]: @wq/map:locate ========= [@wq/map:locate] **@wq/map:locate** is a [@wq/app plugin] providing utilities for requesting the user's latitude and longitude, a common use case in many VGI, citizen science, and crowdsourcing applications. @wq/map/locate is designed to be used together with [@wq/map]. ## API Once registered, the locate plugin populates form ``s from a Leaflet map to facilitate multiple ways of providing location information (e.g. GPS or a map click). ### wq.app for PyPI ```javascript // myapp/main.js define(['wq/app', 'wq/map', 'wq/locate', './config'], function(app, map, locate, config) { // In myapp/config.js or in wq.db.rest registration: // config.locate = { ... }; // config.pages[page].map = { ... }; // config.pages[page].locate = true; app.use(map); app.use(locate); // Should be registered after map app.init(config).then(function() { app.jqmInit(); app.prefetchAll(); }); }); ``` ### @wq/app for npm ```javascript // src/index.js import app from '@wq/app'; import map, { locate } from '@wq/map'; import config from './config'; // In src/config.js or in wq.db.rest registration: // config.locate = { ... }; // config.pages[page].map = { ... }; // config.pages[page].locate = true; app.use(map); app.use(locate); // Should be registered after map app.init(config).then(function() { app.jqmInit(); app.prefetchAll(); }); ``` ## Usage The Locator widget provides three modes for entering location information: * `gps`: Request position using device GPS or network location, if available. Also returns GPS "accuracy" in meters. To get more accurate measurements, the `gps` mode of the Locator widget continually polls location until the form is saved or a different mode is selected. * `interactive`: Allow user to click the map directly. Accuracy is computed based on zoom level. * `manual`: Allow user to manually enter their latitude and longitude. The entered coordinates are automatically displayed on a Leaflet map. "Accuracy" is represented as a circle with the radius of the accuracy. > **Tip: Save Accuracy In Your Database!** > > The accuracy number is a critical part of the location information - so don't discard it and only save the latitude and longitude. Remember that accuracy is fundamentally different than precision: just because a GPS device returns latitude and longitude specified out to 9 decimal places, does not mean the user is actually at that exact location. In fact, the initial measurement returned by many consumer GPS devices will often be "precise" but inaccurate, perhaps by several kilometers. The accuracy measurement is thus an imperfect, but useful metric for evaluating the location information. Keeping the GPS on for as long as possible is one way to get more accurate measurements, but it is still important to save the information in the database for future reference. > > Similarly, having a user tap the map to specify their location is practically guaranteed to provide inaccurate results unless they zoom in first. For this reason, the `interactive` mode approximates an accuracy measurement based on zoom level (accuracy = 2 pixels of screen space converted to meters based on the zoom level). Accuracy can serve as a reminder to the user to zoom in, or even to enforce a minimum level of accuracy in your form processing logic. The Locator widget searches for the following fields in the form. All fields are technically optional, though you will probably want at least `latitude` and `longitude` and/or `geometry` be present. field name | purpose -----------|------------------------ `latitude` | A text input that will receive (or provide) the latitude `longitude` | A text input that will receive (or provide) the longitude `geometry` | A hidden input that will receive booth coordinates as a simple GeoJSON point `accuracy` | A text input that will receive the computed accuracy `toggle` | A set of radio buttons (or a select menu) that will change the widget mode. The values for each option should be one or more of the modes listed above. `mode` | A hidden input that will receive the selected mode (in the case where toggle field is not used or is not saved) `source` | A text input that will recieve information about the source of the GPS coordinate, if known. (For use with `"gps"` mode in PhoneGap/Cordova applications with [cordova-plugin-bluetooth-geolocation] installed). If any of these fields are named differently in your application, define `config.locate.fieldNames` as follows: ```javascript config.locate = { "fieldNames": { "toggle": "toggle-btn", "latitude": "lat", "longitude": "lng", "accuracy": "accuracy" } }; ``` `config.locate` can also be used to register up to three callback functions that will be executed at various points in the process: * `onSetMode(mode)`: Called whenever the locator mode changes, e.g. in response to the user clicking the `toggle` button. * `onUpdate(location, accuracy)`: Called when ever a new location is determined. * `onError(event)`: Called when GPS lookup fails. ### Example
#### JS ### wq.app for PyPI ```javascript // myapp/main.js define(['wq/app', 'wq/map', 'wq/locate', './config'], function(app, map, locate, config) { app.use(map); app.use(locate); config.locate = { // Custom handler for location updates 'onUpdate': function(loc, accuracy) { if (accuracy > 1000) { $('#message').html( "Note: your location accuracy appears to be off by more than 1km." ); } else { $('#message').html(""); } } } app.init(config).then(function() { app.jqmInit(); app.prefetchAll(); }); }); ``` ### @wq/app for npm ```javascript // src/index.js import app from '@wq/app'; import map, { locate } from '@wq/map'; import config from './config'; app.use(map); app.use(locate); config.locate = { // Custom handler for location updates 'onUpdate': function(loc, accuracy) { if (accuracy > 1000) { $('#message').html( "Note: your location accuracy appears to be off by more than 1km." ); } else { $('#message').html(""); } } } app.init(config).then(function() { app.jqmInit(); app.prefetchAll(); }); ``` #### HTML ```xml
``` > **Tip: Keep GPS running for better results!** > > When requesting the user's location in a web app, it's generally better to use `Geolocation.watchPosition()` than `Geolocation.getCurrentPosition()`, even when you only need a single point and not a GPS trace. The reason for this is that the first result returned by the GPS may be inaccurate, and the longer the GPS is on, the more time it has to lock on to the satellites. For this reason, @wq/map/locate continues requesting the GPS location until the user saves the form and/or navigates to another page. This is accomplished by setting `watch: true` in the underlying call to [L.Map.locate()]. [@wq/map:locate]: [@wq/app plugin]: https://wq.io/docs/app-js [@wq/map]: https://wq.io/docs/map-js [L.Map.locate()]: http://leafletjs.com/reference.html#map-locate-options [cordova-plugin-bluetooth-geolocation]:
@wq/model
@wq/app
wq.app: Modules