Skip to content

Commit 2aaa088

Browse files
authored
Add MapboxPointFieldWidget (#126)
* Add MapboxPointFieldWidget (without autocompletion) * Use Mapbox's internal autocomplete system * Style tweaks
1 parent 197816a commit 2aaa088

File tree

5 files changed

+303
-8
lines changed

5 files changed

+303
-8
lines changed

mapwidgets/settings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
("streetViewControl", True),
1818
),
1919

20+
"MapboxPointFieldWidget": (
21+
("mapCenterLocationName", None),
22+
("mapCenterLocation", TIMEZONE_COORDINATES.get(getattr(django_settings, "TIME_ZONE", "UTC"))),
23+
("zoom", 6),
24+
("markerFitZoom", 15),
25+
("access_token", ""),
26+
),
27+
2028
"GoogleStaticMapWidget": (
2129
("zoom", 15),
2230
("size", "480x480"),
@@ -54,6 +62,7 @@
5462
"MINIFED": not django_settings.DEBUG,
5563
"GOOGLE_MAP_API_SIGNATURE": "",
5664
"GOOGLE_MAP_API_KEY": "",
65+
"MAPBOX_API_KEY": "",
5766
}
5867

5968

mapwidgets/static/mapwidgets/css/map_widgets.css

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,6 @@
9696
border-radius: 0 !important;
9797
}
9898

99-
.mw-wrap .icon-location{
100-
font-size: 20px;
101-
}
102-
103-
.mw-wrap .mw-btn-add-marker{
104-
padding: 2px 8px 2px;
105-
}
106-
10799
.mw-header {
108100
margin-bottom: 6px;
109101
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
(function($) {
2+
DjangoMapboxPointFieldWidget = DjangoMapWidgetBase.extend({
3+
4+
init: function(options){
5+
$.extend(this, options);
6+
7+
this.coordinatesOverlayToggleBtn.on("click", this.toggleCoordinatesOverlay.bind(this));
8+
this.coordinatesOverlayDoneBtn.on("click", this.handleCoordinatesOverlayDoneBtnClick.bind(this));
9+
this.coordinatesOverlayInputs.on("change", this.handleCoordinatesInputsChange.bind(this));
10+
this.addMarkerBtn.on("click", this.handleAddMarkerBtnClick.bind(this));
11+
this.myLocationBtn.on("click", this.handleMyLocationBtnClick.bind(this));
12+
this.deleteBtn.on("click", this.resetMap.bind(this));
13+
14+
// if the the location field in a collapse on Django admin form, the map need to initialize again when the collapse open by user.
15+
if ($(this.wrapElemSelector).closest('.module.collapse').length){
16+
$(document).on('show.fieldset', this.initializeMap.bind(this));
17+
}
18+
19+
// For the geocoding etc.
20+
mapboxgl.accessToken = this.mapOptions.access_token;
21+
22+
this.mapboxSDK = new mapboxSdk({ accessToken: this.mapOptions.access_token });
23+
24+
this.geocoder = new MapboxGeocoder({
25+
accessToken: mapboxgl.accessToken,
26+
zoom: 13,
27+
placeholder: 'Search a location',
28+
mapboxgl: mapboxgl,
29+
reverseGeocode: true,
30+
marker: false
31+
})
32+
33+
this.geocoder.on('result', (place) => this.handleAutoCompletePlaceChange(place.result))
34+
35+
this.initializeMap.bind(this)();
36+
},
37+
38+
initializeMap: function(){
39+
var mapCenter = this.mapCenterLocation;
40+
this.map = new mapboxgl.Map({
41+
container: this.mapElement.id, // container ID
42+
style: 'mapbox://styles/mapbox/streets-v11', // style URL
43+
center: [mapCenter[1], mapCenter[0]], // starting position [lng, lat]
44+
zoom: this.zoom // starting zoom
45+
});
46+
47+
this.geocoder.addTo(this.map)
48+
49+
$(this.mapElement).data('mapbox_map', this.map);
50+
$(this.mapElement).data('mapbox_map_widget', this);
51+
52+
if (!$.isEmptyObject(this.locationFieldValue)){
53+
this.updateLocationInput(this.locationFieldValue.lat, this.locationFieldValue.lng);
54+
this.fitBoundMarker();
55+
}
56+
},
57+
58+
addMarkerToMap: function(lat, lng){
59+
this.removeMarker();
60+
this.marker = new mapboxgl.Marker()
61+
.setLngLat([parseFloat(lng), parseFloat(lat)])
62+
.setDraggable(true)
63+
.addTo(this.map);
64+
this.marker.on("dragend", this.dragMarker.bind(this));
65+
},
66+
67+
fitBoundMarker: function () {
68+
if (this.marker) {
69+
this.map.flyTo({
70+
center: this.marker.getLngLat(),
71+
zoom: 14
72+
});
73+
}
74+
},
75+
76+
removeMarker: function(e){
77+
if (this.marker){
78+
this.marker.remove()
79+
}
80+
},
81+
82+
dragMarker: function(e){
83+
const position = this.marker.getLngLat()
84+
this.updateLocationInput(position.lat, position.lng)
85+
},
86+
87+
handleAddMarkerBtnClick: function(e){
88+
$(this.mapElement).toggleClass("click");
89+
this.addMarkerBtn.toggleClass("active");
90+
if ($(this.addMarkerBtn).hasClass("active")){
91+
this.map.on("click", this.handleMapClick.bind(this));
92+
} else {
93+
this.map.off("click", this.handleMapClick.bind(this));
94+
}
95+
},
96+
97+
handleMapClick: function(e){
98+
this.map.off("click", this.handleMapClick.bind(this));
99+
$(this.mapElement).removeClass("click");
100+
this.addMarkerBtn.removeClass("active");
101+
this.updateLocationInput(e.lngLat.lat, e.lngLat.lng)
102+
},
103+
104+
callPlaceTriggerHandler: function (lat, lng, place) {
105+
if (place === undefined) {
106+
this.mapboxSDK.geocoding.reverseGeocode({
107+
query: [parseFloat(lng), parseFloat(lat)]
108+
})
109+
.send()
110+
.then(response => {
111+
const address = response?.body?.features?.[0];
112+
this.geocoder.clear();
113+
this.geocoder.setPlaceholder(address?.place_name || "Somewhere");
114+
$(document).trigger(this.placeChangedTriggerNameSpace,
115+
[address, lat, lng, this.wrapElemSelector, this.locationInput]
116+
)
117+
})
118+
} else { // user entered an address
119+
$(document).trigger(this.placeChangedTriggerNameSpace,
120+
[place, lat, lng, this.wrapElemSelector, this.locationInput]
121+
);
122+
}
123+
},
124+
125+
handleAutoCompletePlaceChange: function (place) {
126+
if (!place.geometry) {
127+
// User entered the name of a Place that was not suggested and
128+
// pressed the Enter key, or the Place Details request failed.
129+
return;
130+
}
131+
var [lng, lat] = place.geometry.coordinates;
132+
this.updateLocationInput(lat, lng, place);
133+
this.fitBoundMarker()
134+
},
135+
});
136+
137+
})(mapWidgets.jQuery);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{% load i18n %}
2+
3+
<div class="mw-wrap" id="{{ name }}-mw-wrap">
4+
{% block container %}
5+
<div class="mw-header">
6+
{% block header %}
7+
<div class="mw-coordinates-wrap">
8+
<a class="mw-btn mw-btn-primary mw-btn-coordinates">{% trans "Edit coordinates" %} <i class="icon-down-dir" ></i></a>
9+
<div class="mw-coordinates-overlay hide">
10+
<label for="{{ name }}-mw-overlay-latitude">
11+
{% trans "Latitude:" %}
12+
<input type="text" id="{{ name }}-mw-overlay-latitude" class="form-control mw-overlay-input mw-overlay-latitude" placeholder="{% trans 'Ex: 41.015137' %}"/>
13+
</label>
14+
<label for="{{ name }}-mw-overlay-longitude">
15+
{% trans "Longitude:" %}
16+
<input type="text" id="{{ name }}-mw-overlay-longitude" class="form-control mw-overlay-input mw-overlay-longitude" placeholder="{% trans 'Ex: 28.979530' %}"/>
17+
</label>
18+
<a class="mw-btn mw-btn-success mw-btn-coordinates-done pull-right">{% trans "Done" %}</a>
19+
</div>
20+
</div>
21+
22+
<a class="mw-btn mw-btn-info mw-btn-my-location" >{% trans "Use My Location" %}</a>
23+
<a class="mw-btn mw-btn-warning mw-btn-add-marker" >{% trans "Choose point on map" %}</a>
24+
{% endblock header %}
25+
</div>
26+
27+
<div class="mw-map-wrapper">
28+
<div class="mw-loader-overlay hide" >
29+
<div class="mw-loader"></div>
30+
</div>
31+
<div id="{{ name }}-map-elem" class="mw-map"></div>
32+
<div style="display: none" class="hide">
33+
<textarea id="{{ id }}" name="{{ name }}">{{ serialized }}</textarea>
34+
{% block extra_input %}
35+
{% endblock extra_input %}
36+
37+
</div>
38+
</div>
39+
40+
<div class="mw-footer">
41+
<span class="mw-help-text help-text"></span>
42+
<a class="mw-btn mw-btn-delete pull-right {{ serialized|yesno:"mw-btn-danger, mw-btn-default disabled" }}" ><i class="icon-trash-empty"></i></a>
43+
</div>
44+
<div class="animated-loader"></div>
45+
{% endblock container %}
46+
</div>
47+
48+
{% block javascript %}
49+
<script type="application/javascript">
50+
(function($) {
51+
var mapOptions = JSON.parse("{{ options|escapejs }}");
52+
var field_value = JSON.parse("{{ field_value|escapejs }}");
53+
54+
var wrapElemSelector = "#{{ name }}-mw-wrap";
55+
var mapElemID = "{{ name }}-map-elem";
56+
var locationInputID = "#{{ id }}";
57+
58+
var mapWidgetOptions = {
59+
locationInput: $(locationInputID),
60+
wrapElemSelector: wrapElemSelector,
61+
locationFieldValue: field_value,
62+
mapElement: document.getElementById(mapElemID),
63+
mapCenterLocationName: mapOptions.mapCenterLocationName,
64+
mapCenterLocation: mapOptions.mapCenterLocation,
65+
coordinatesOverlayToggleBtn: $(".mw-btn-coordinates", wrapElemSelector),
66+
coordinatesOverlayDoneBtn: $(".mw-btn-coordinates-done", wrapElemSelector),
67+
coordinatesOverlayInputs: $(".mw-overlay-input", wrapElemSelector),
68+
coordinatesOverlay: $(".mw-coordinates-overlay", wrapElemSelector),
69+
myLocationBtn: $(".mw-btn-my-location", wrapElemSelector),
70+
mapOptions: mapOptions,
71+
deleteBtn: $(".mw-btn-delete", wrapElemSelector),
72+
addMarkerBtn: $(".mw-btn-add-marker", wrapElemSelector),
73+
loaderOverlayElem: $(".mw-loader-overlay", wrapElemSelector),
74+
zoom: mapOptions.zoom,
75+
markerFitZoom: mapOptions.markerFitZoom,
76+
streetViewControl: mapOptions.streetViewControl,
77+
markerCreateTriggerNameSpace: "mapbox_point_map_widget:marker_create",
78+
markerChangeTriggerNameSpace: "mapbox_point_map_widget:marker_change",
79+
markerDeleteTriggerNameSpace: "mapbox_point_map_widget:marker_delete",
80+
placeChangedTriggerNameSpace: "mapbox_point_map_widget:place_changed"
81+
};
82+
var widget = new DjangoMapboxPointFieldWidget(mapWidgetOptions);
83+
{% block extra_javascript %}
84+
85+
{% endblock %}
86+
})(mapWidgets.jQuery);
87+
</script>
88+
{% endblock javascript %}

mapwidgets/widgets.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,75 @@ def render(self, name, value, attrs=None, renderer=None):
120120
return self.as_super.render(name, value, attrs)
121121

122122

123+
class MapboxPointFieldWidget(BasePointFieldMapWidget):
124+
template_name = 'mapwidgets/mapbox-point-field-widget.html'
125+
settings = mw_settings.MapboxPointFieldWidget
126+
settings_namespace = 'MapboxPointFieldWidget'
127+
mapbox_map_srid = 4326
128+
129+
@property
130+
def media(self):
131+
css = {
132+
'all': [
133+
minify_if_not_debug('mapwidgets/css/map_widgets{}.css'),
134+
"https://api.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.css",
135+
"https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.css",
136+
]
137+
}
138+
139+
js = [
140+
"https://api.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.js",
141+
"https://unpkg.com/@mapbox/mapbox-sdk/umd/mapbox-sdk.min.js",
142+
"https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.7.2/mapbox-gl-geocoder.min.js",
143+
]
144+
145+
if not mw_settings.MINIFED: # pragma: no cover
146+
js = js + [
147+
'mapwidgets/js/jquery_init.js',
148+
'mapwidgets/js/jquery_class.js',
149+
'mapwidgets/js/django_mw_base.js',
150+
'mapwidgets/js/mw_mapbox_point_field.js',
151+
]
152+
else:
153+
js = js + [
154+
'mapwidgets/js/mw_mapbox_point_field.min.js'
155+
]
156+
157+
return forms.Media(js=js, css=css)
158+
159+
def render(self, name, value, attrs=None, renderer=None):
160+
if attrs is None:
161+
attrs = dict()
162+
163+
field_value = {}
164+
if value and isinstance(value, str):
165+
value = self.deserialize(value)
166+
longitude, latitude = value.coords
167+
field_value['lng'] = longitude
168+
field_value['lat'] = latitude
169+
170+
if isinstance(value, Point):
171+
if value.srid and value.srid != self.mapbox_map_srid:
172+
ogr = value.ogr
173+
ogr.transform(self.mapbox_map_srid)
174+
value = ogr
175+
176+
longitude, latitude = value.coords
177+
field_value['lng'] = longitude
178+
field_value['lat'] = latitude
179+
180+
181+
extra_attrs = {
182+
'options': self.map_options(),
183+
'field_value': json.dumps(field_value)
184+
}
185+
attrs.update(extra_attrs)
186+
self.as_super = super(MapboxPointFieldWidget, self)
187+
if renderer is not None:
188+
return self.as_super.render(name, value, attrs, renderer)
189+
else:
190+
return self.as_super.render(name, value, attrs)
191+
123192
class PointFieldInlineWidgetMixin(object):
124193

125194
def get_js_widget_data(self, name, element_id):

0 commit comments

Comments
 (0)