makinacorpus / django-leaflet

Use Leaflet in your Django projects
GNU Lesser General Public License v3.0
717 stars 283 forks source link

How to capture a LineStringField with dim=3 (XYZ) through LeafletWidget #344

Closed florianm closed 3 years ago

florianm commented 3 years ago

Hi, long time happy django-leaflet user here.

I've got a model with a django.contrib.gis.db.LineStringField(dim=3) which stores data mostly from a structured source (OpenDataKit captures XYZM geotraces = linestrings with altitude and accuracy).

When I use the django-leaflet LeafletWidget to capture a LineString through the Django admin, the widget captures only XY coordinate pairs, and saving the model promptly fails on the missing third dimension.

E.g. this LineString captured by drawing on the LeafletWidget fails to save:

{"type":"LineString","coordinates":[[117.079024,-20.460389],[117.077157,-20.463284],[117.074883,-20.46646],[117.073188,-20.461475],[117.072179,-20.458439],[117.071943,-20.457072]]}

E.g. this manually amended LineString saves OK - I've added the ,0 altitude into each coordinate tuple:

{"type":"LineString","coordinates":[[117.079024,-20.460389,0],[117.077157,-20.463284,0],[117.074883,-20.46646,0],[117.073188,-20.461475,0],[117.072179,-20.458439,0],[117.071943,-20.457072,0]]}

Imported data from the structured source (ODK XYZM linestrings) saves as

{"type":"LineString","coordinates":[[115.8844,-31.99675,-16.38127],[115.8844,-31.99675,-17.41482],[115.8844,-31.99675,-20.21721]]}

Short of changing my LineStringField to dim=2 and losing the (admittedly questionably inaccurate) altitude, is there a way to make the LeafletWidget work with LineStringFields of higher dimensions? It would be OK to set altitude to 0 if drawn on a map. I couldn't find anything helpful on SO, in the django-leaflet docs or elsewhere online. Any pointers would be appreciated!

Options:

Gagaro commented 3 years ago

Hi, thanks for the issue, it actually made me find a typo preventing this which dated back to 2013!

I was able to do what you wanted by customizing the FieldStore:

class LineWidget(LeafletWidget):
    include_media = True
    map_template = 'leaflet/admin/widget.html'
    modifiable = True
    map_width = 'min(calc(100vw - 30px), 720px)'
    map_height = '400px'
    field_store_class = 'L.Field3DStore'

    class Media:
        js = ['leaflet/leaflet.forms.js', 'leaflet.3d.js']

class LineTestForm(forms.ModelForm):
    class Meta:
        model = LineTest
        fields = ['line']
        widgets = {
            'line': LineWidget
        }

class LineTestAdmin(LeafletGeoAdmin):
    form = LineTestForm

admin.site.register(LineTest, LineTestAdmin)

leaflet.3d.js:

L.Field3DStore = L.FieldStore.extend({
    _serialize: function (layer) {
        const geojson = JSON.parse(L.FieldStore.prototype._serialize.call(this, layer));
        for (const coordinate of geojson.coordinates) {
            coordinate.push(0);
        }
        return JSON.stringify(geojson);
    }
});

I'll release the fix ASAP so you can try it yourself (you can use master in the meantime).

Gagaro commented 3 years ago

Released in 0.28.2

florianm commented 3 years ago

Thanks for the lighting quick turnaround, I'll give it a whirl!