arrow for title

Mappe web: il progetto Mer et Demeures

25 Lug, 2019 Sviluppo
Mappe web: il progetto Mer et Demeures

Le mappe permettono, sin dai tempi più antichi, di cercare e trovare informazioni in “modalità spaziale”. Oggi è molto comune che vengano inserite in siti web, come abbiamo fatto noi nel caso di Mer et Demeures.

Cos’è una mappa web?

Una mappa web è la rappresentazione di un sistema geografico, fornisce dati e informazioni spaziali.

La mappa può avere diverse caratteristiche, a seconda delle necessità:

In generale, per realizzarla abbiamo bisogno di un database spaziale, per memorizzare i dati, e una libreria Javascript, per disegnarla sulla pagina web.

Per il cliente di cui ti parleremo tra poco, abbiamo utilizzato 3 componenti software fondamentali: GeoDjango, PostGIS e Leaflet JS. Vediamoli uno per volta.

GeoDjango

Non è un packages esterno ma fa parte di Django, come pacchetto contrib, dalla versione 1.0: trasforma, appunto, Django in un framework spaziale geografico.

Come funziona?

Innanzitutto GeoDjango ci fornisce dei tipi di field spaziali - ne sono anche parecchi - e ci permette di eseguire query di tipo spaziale direttamente nell’ORM di Django. Inoltre, i field sono già integrati nell’admin.

Al momento GeoDjango supporta 4 database geospaziali.

PostGIS

In questo progetto abbiamo usato PostGIS: lo abbiamo scelto perché utilizzavamo già PostgreSQL, ma anche perché è il backend per GeoDjango col supporto più esteso.

Si tratta di un’estensione di PostgreSQL che integra direttamente i dati spaziali al suo interno. 

Parallelamente a GeoDjango, PostGIS ci fornisce dei tipi di dati specifici per memorizzare le nostre informazioni spaziali. Inoltre ci sono degli indici ottimizzati per i dati spaziali, oltre che funzioni specifiche che possiamo utilizzare per cercare questi dati.

Leaflet JS

Abbiamo inserito Leaflet nel nostro stack, per la mappa lato frontend, per una serie di motivi.

Innanzitutto è una delle librerie Javascript per renderizzare mappe sul browser più usate.

Il primo aspetto importante, sia per il cliente che per noi sviluppatori, è che si tratta di Software Libero, con una buona comunità di sviluppatori che partecipano al suo sviluppo. Inoltre è mobile friendly, un aspetto non secondario nel contesto attuale, ed è davvero molto leggero.

E poi è di semplice utilizzo: la documentazione è accessibile e Il risultato che si ottiene ha delle ottime performance.

Un esempio base di mappa

Prendiamo l’applicazione Django di un Blog, presente nella documentazione, e facciamo un test con questi tre modelli. 

from django.db import models

class Blog(models.Model):
    name = models.CharField(max_length=100)

class Author(models.Model):
    name = models.CharField(max_length=200)

class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    authors = models.ManyToManyField(Author)

A questa applicazione andiamo ad aggiungere dei dati spaziali e li renderizziamo in una mappa web. Partendo da questo esempio, il primo passaggio è modificare i settings:

INSTALLED_APPS = [
    # …
    'django.contrib.gis',
]

DATABASES = { 
    'default' : {
        'ENGINE' : 'django.contrib.gis.db.backends.postgis',
        # …
    } 
}

Dobbiamo attivare anche l’estensione PostGIS con una migrazione:

from django.contrib.postgres import operations
from django.db import migrations

class Migration (migrations.Migration) :
    dependencies = [ ('blog',  '0001_initial') ]
    operations = [
        operations.CreateExtension ('postgis')
    ]

L’altra modifica che dobbiamo fare è di aggiungere un PointField, importandolo da GeoDjango. Abbiamo anche inserito una property, utile in Leaflet poi, per renderizzare longitudine e latitudine.

from django.contrib.gis.db.models import PointField
from django.db import models

class Entry (models.Model) :
    # …
    point = PointField()

    @property
    def lat_lng(self):
        return list(getattr(self.point, 'coords', [])[::-1])

La maniera più semplice per modificare i nostri entry, consiste nel cambiare Admin per usarne uno specifico. Possiamo indicare anche precisi attributi e zoom di default.

Il risultato finale sarà questo:

mappa web base

Possiamo zoomare, spostarci nella mappa e interagire: tutto con poche righe di codice.

Una volta editati gli entry e aggiunti i punti sull’admin, possiamo mostrarli su una mappa vera e propria. Usiamo la views e le urls:

from django.urls import path
from django.views.generic import ListView
from .models import Entry

class EntryList (ListView) :
    queryset = Entry.objects.filter(point__isnull=False)

urlpatterns = [
    path ('map/', EntryList.as_view()) ,
]

Definito l’EntryList, lo mappiamo in una url: e qui finisce il lavoro di backend. Il passaggio successivo riguarda la scrittura del template.

Innanzitutto nell’head includiamo lo stile di Leaflet JS e la libreria stessa in Javascript. Poi scriviamo un div per poter renderizzare la mappa nella parte html:

<script type="text/javascript">

  var m = L.map('m').setView([43.77, 11.26], 15); // Florence

  L.tyleLayer('//{s}.tile.osm.org/{z}/{x}/{y}.png').addTo(m);

  {% for e in object_list %}
    L.marker({{e.lat_lng}}).addTo(m);
  {% endfor %}

</script>

Per quanto riguarda la parte di Javascript: L sta per Leaflet, renderizziamo nel nostro Div la mappa e settiamo la visuale della mappa, latitudine, longitudine e lo zoom. Scegliamo anche il tile da inserire: in questo caso usiamo Open Street Map per la parte grafica, altrimenti non vedremmo nulla.

Inseriamo un ciclo, con la lista di marker, e per ogni marker chiediamo a Leaflet di stamparne uno con latitudine e longitudine.

ATTENZIONE: PostGIS chiede prima longitudine e poi latitudine, mentre Leaflet, al contrario, chiede prima latitudine e poi longitudine. Per questo abbiamo creato una proprietà.

E questo è il risultato finale:

mappa web finale

Questo codice è effettivamente funzionante: puoi provare anche tu a utilizzarlo, seguendo tutti i passaggi!

Il progetto Mer et Demeures

Rispetto a quanto abbiamo appena visto, la situazione che abbiamo affrontato in questo progetto è molto più complessa e richiede un approccio più dettagliato.

Mer et Demeures è una società francese, con sede in Provenza, che vende e affitta proprietà sul mare in tutto il mondo. Attiva dal 2014, ha un portale tradotto in 8 lingue, con oltre 100.000 annunci, in 40 Paesi e 6 continenti.

Il progetto, molto grande, era già in produzione da molto tempo e richiedeva una mappa molto complessa e interattiva.

Questo è uno screenshot della versione mobile 1.0, realizzata con Django 1.5, Python 2.7, PostgreSQL 9.3, con un solo campo testuale per memorizzare i dati spaziali. Leaflet era stato usato, ma solo per una visualizzazione statica della mappa.

versione iniziale mer et demeures

Questo è ciò che viene fuori dopo il nostro intervento, una mappa molto interattiva.

mappa mer et demeures finale

Abbiamo utilizzato:

Per capire la differenza dagli esempi di base di prima, ecco un estratto dei modelli.

Abbiamo utilizzato ad esempio il multipoligono per visualizzare i confini delle città, il modello annuncio con un PointField. E una gerarchia molto più complessa. 

from django.db import models
from django.contrib.gis.db.models import (
    MultiPolygonField, PointField
)

class City(models.Model):
    borders = MultiPolygonField()

class Ad(models.Model):
    city = models.ForeignKey(City, on_delete=models.CASCADE)
    location = PointField()

Non potevamo utilizzare un template per questo progetto, ma abbiamo deciso di implementare un API di tipo RESTful e di fornirla al frontend.

pip install djangorestframework     # RESTful API
pip install djangorestframework-gis # Geographic add-on
pip install django-filter           # Filtering support

INSTALLED _APPS = [
    # …
    'django.contrib.gis',
    'rest_framework',
    'rest_framework_gis',
    'django_filters',
]

Il primo passaggio per implementare un API è scrivere un serializzatore dei modelli: ne abbiamo ereditato uno di base, in cui abbiamo specificato modello, field e field aggiuntivi.

from rest_framework_gis.serializers import GeoFeatureModelSerializer
from .models import Ad

class AdSerializer(GeoFeatureModelSerializer):
    class Meta:
        model = Ad
        geo_field = 'location'
        fields = ('id',)

Passaggio successivo è stato importare una view:

from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework_gis.filters import InBBoxFilter
from .models import Ad
from .serializers import AdSerializer

class AdViewSet (ReadOnlyModelViewSet):
    bbox_filter_field = 'location'
    filter_backends = (InBBoxFilter,)
    queryset = Ad.objects.filter(location_isnull=False)
    serializer_class = AdSerializer

A questo punto usiamo DefaultRouter e chiudiamo il lavoro di backend.

from rest_framework.routers import DefaultRouter
from .views import AdViewSet

router = DefaultRouter()
router.register(r'markers', AdViewSet, basename='marker')
urlpatterns = router.urls

Il risultato è un GeoJSON, una modalità standard di fornire dati geo-spaziali:

{
    "type": "FeatureCollection",
    "features": [{
        "id": 1,
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": [11.255814, 43.769562]
        },
        "properties": {}
    }]
}

Qui non ci sono proprietà aggiuntive, ma nel progetto originale ne abbiamo inserite diverse per renderizzare i pop-up con dati aggiuntivi (come valuta, prezzo…).

A questo punto la palla passa nelle mani del frontend, che potrà interrogare questi dati.

Questo primo esempio è fatto con JavaScript:

<script type="text/javascript">

  var m = L.map('m').setView([43.77, 11.26], 15)
  L.tileLayer('//{s}.tile.osm.org/{z}/{x}/{y}.png').addTo(m);

  fetch('/markers')
    .then(function (results) {
      L.geoJSON(results).addTo(m)
    })

</script>

La differenza fondamentale rispetto al procedimento precedente è che il GeoJSON può contenere poligoni, markers, qualsiasi cosa che rispetti i suoi standard: quindi riusciamo a renderizzare tutto in un unico colpo. 

Leaflet, inoltre, fornisce degli eventi: quello che interessava a noi è Moveend, invocato quando il centro della mappa viene spostato e le animazioni terminano. Si tratta di un aspetto molto utile in caso, come per Mer et Demeures, di forte interazione con la mappa.

Per quanto riguarda la parte di React, abbiamo usato React - Leaflet, che non è una libreria a sé ma solo una stazione (è necessario quindi installare anche Leaflet): 

import React from 'react'
import {
    Map,
    TileLayer,
    GeoJSOn
} from 'react - leaflet'

export default class Map extends Component {
    state = {
        geoJson: {}
    }

    onMove = () => {
        fetch('/markers')
            .then(geoJson => this.setState({
                geoJson
            }))
        }
        // render ( ) { ... }
    }

Importiamo React e i 3 componenti che ci fornisce la libreria e che ci interessano, inizializziamo lo stato del nostro componente in un oggetto nuovo in chiave GeoJSON, in cui andranno tutti i contenuti nuovi. Poi c’è la funzione che viene invocata nel momento dell’interazione.

Vediamo il metodo Render, molto simile all’altro:

render() {
    return (
        <Map center= {c} zoom= {z} onMoveend= {this.onMove}>
        <TileLayer url="//{s}.tile.osm.org/ {z}/{x}/{y}.png"/>
        <GeoJSON data= {this.state.geoJson} />
        </Map>
    )
}

Nel nostro caso avevamo davvero molti annunci da inserire, e su Leaflet è sconsigliato renderizzare più di 100 marker perché si rallenta di molto. 

La soluzione che abbiamo trovato è stata clusterizzare questi marker lato backend e al click dell’utente venivano espansi: su React questa cosa ha rallentato di molto il render.

Conclusioni

I punti salienti del nostro lavoro sono, in conclusione, questi:

Un ringraziamento speciale a Mer et Demeures che, grazie al suo progetto, ci ha permesso di studiare e approfondire questo argomento così interessante!


* Content licensed under a Creative Commons Attribution-ShareAlike 4.0 International.

Paolo Melchiorre, Carmelo Catalfamo

Blog

Potrebbe interessarti anche:

contattaci

Hai una buona idea ma non sai che pesci prendere?

Parlaci del tuo progetto