Map widgets¶
Two Leaflet based widgets sharing one JavaScript engine
(gentelella/js/base/maplib.js):
MapPointInput– a form field that asks the user for a single GPS point.DJMap– a dashboard map that draws many points fetched from an API, the sibling of theDJGraphchart widget.
Tiles come from OpenStreetMap and no API key is involved. The point is stored
as the plain string "latitude,longitude", so GeoDjango, GDAL and PostGIS
are not needed and everything works on SQLite.
Loading the static files¶
Leaflet itself is always loaded. Marker clustering and the heatmap are extra
libraries, enabled with the use_maps define:
DEFAULT_JS_IMPORTS = {
'use_maps': True,
}
Without it the map still works: clustered layers fall back to plain layer groups and the heatmap is skipped, each with a console warning.
Note
The widgets are initialised from gentelella/js/base.js, which is
generated and not shipped in the repository. Run
python manage.py createbasejs after installing or upgrading, or the
widgets silently never start.
Asking for a GPS point¶
MapPointInput renders a normal text input plus a map. The point can be set
by clicking the map, dragging the marker, typing the value, pressing the “use
my location” button or searching an address. Every path writes
"lat,lng" into the input and dispatches input and change, so
validation and autosave see the update.
Usage
from django import forms
from djgentelella.forms.forms import GTForm
from djgentelella.widgets.maps import MapPointInput
class PlaceForm(GTForm, forms.Form):
location = forms.CharField(widget=MapPointInput(
zoom=8, center=(9.9327, -84.0875), search=True))
Or, with validation and a model field included:
from django.db import models
from djgentelella.fields.maps import GTPointField
class Place(models.Model):
name = models.CharField(max_length=150)
country = models.CharField(max_length=100, blank=True)
city = models.CharField(max_length=100, blank=True)
location = GTPointField(
zoom=8,
center=(9.9327, -84.0875),
based_fields=['#id_country', '#id_city'],
)
GTPointField is a CharField underneath. Use
djgentelella.fields.maps.parse_point(place.location) to get
(lat, lng) floats back, or None when the value is empty or malformed.
Widget options
Option |
Default |
Description |
|---|---|---|
|
|
Initial zoom level. |
|
|
Where to look when the field has no value yet. |
|
|
Map height. Also settable in CSS through |
|
|
Show the address search box. |
|
|
Show the “use my location” button. |
|
|
CSS selectors whose joined values are geocoded to place the marker. |
|
|
Let |
|
OpenStreetMap |
Tile layer template. |
|
OpenStreetMap |
Attribution line for |
|
Nominatim |
Nominatim compatible endpoint. |
Note
“Use my location” relies on navigator.geolocation, which browsers only
answer over https or on localhost. Outside a secure context the button
is not rendered at all.
Warning
Blank tiles, or a 403 saying a referer is required? Django sets
SECURE_REFERRER_POLICY = 'same-origin' by default (since 3.1), which
tells the browser to send no Referer at all on cross-origin requests –
and every tile a map fetches is cross-origin.
OpenStreetMap’s tile usage policy requires a
Referer or a User-Agent identifying the application and answers 403
without one, so the default leaves you with a grid of empty squares.
Set it to the value current browsers use themselves, which sends only the origin – no path, no query – to another site:
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
The alternative is not to depend on OSM’s public servers at all: point
tile_url (and tile_attribution) at your own tile server or a
provider you have an API key for.
Warning
Address search and based_fields geocode through
Nominatim,
whose usage policy caps callers at one request per second and forbids heavy
automated use. That is why search is off by default. For anything
beyond a low-traffic site, run your own instance and point geocoder_url
at it.
Showing points from an API¶
Subclass BaseMapView in <app>/gtmaps.py
(auto-imported, like gtcharts.py) and register it:
from djgentelella.groute import register_lookups
from djgentelella.views.maps import BaseMapView
@register_lookups(prefix="places", basename="placesmap")
class PlacesMapView(BaseMapView):
def get_cluster(self):
return True
def get_layers(self):
country = self.request.query_params.get('country')
queryset = Place.objects.all()
if country:
queryset = queryset.filter(country=country)
return [{
'name': 'Places',
'color': '#2c7fb8',
'points': [
{'lat': lat, 'lng': lng, 'title': place.name,
'popup': '<b>%s</b>' % place.name}
for place in queryset
],
}]
Then drop the map into a template:
{% include 'gentelella/widgets/djmap.html' with map_url=places_url height='480px' %}
Filters. Every data-* attribute other than the reserved ones becomes a
query parameter, using the same syntax as the chart widget: a literal, a
#selector / .selector read from a live element, or {funcName} to
call a global function.
{% include 'gentelella/widgets/djmap.html' with map_url=places_url dataparams='data-country="#id_country"' %}
Call refreshGentelellaMap('.gentelella_map') to re-fetch, for instance from
a filter’s change handler. Adding data-refetch-on-move="true" re-fetches
as the map is panned, sending bbox and zoom.
The JSON contract
{
"center": [9.93, -84.09],
"zoom": 8,
"fit_bounds": true,
"cluster": true,
"layers": [
{"name": "Offices", "visible": true, "cluster": true, "color": "#1f77b4",
"points": [
{"lat": 9.93, "lng": -84.09, "title": "HQ",
"popup": "<b>HQ</b><br>San Jose", "color": "#d62728",
"icon": "fa fa-building", "url": "/place/1/", "weight": 1.0}
]}
],
"heatmap": {"name": "Density", "visible": false, "radius": 25, "blur": 15}
}
Each layer is a switchable overlay in Leaflet’s layer control, and the heatmap
sits alongside them. points may also be given at the top level as a
shorthand for a single unnamed layer. A point with color or icon is
drawn as a coloured pin; popup is inserted as HTML.
Note
lat/lng are serialized as floats, not decimals, so full GPS
precision survives the round trip.