10  Desarrollo de aplicaciones geoespaciales de pila completa (full stack) con GeoDjango

10.1 Introducción

GeoDjango es una extensión de Django, un popular marco de trabajo web de Python, que permite desarrollar aplicaciones geoespaciales, tanto para la capa de backend (acceso a los datos) como para la capa de frontend (interfaz de usuario).

Un marco de trabajo (framework) es una estructura reutilizable que proporciona una base para el desarrollo de un proyecto de sofware. Usualmente, un marco de trabajo incluye bibliotecas y un conjunto de mejores prácticas para utilizarlas. El uso de un marco de trabajo permite un desarrollo más rápido, al proporcionar los componentes esenciales de un proyecto y dotarlos de funcionalidades básicas, que de otro modo habría que desarrollar completamente.

10.2 Django

Django fue desarrollado originalmente para gestionar páginas web orientadas a noticias de la World Company de Lawrence, Kansas. En 2005, fue liberado al público con una licencia BSD. Fue nombrado al guitarrista de jazz gitano Django Reinhardt.

10.2.1 Estructura de proyectos

10.2.1.1 Directorio principal

Un proyecto Django reside en un directorio del sistema de archivos, el cual contiene varios archivos y subdirectorios. Por ejemplo, el directorio de un proyecto llamado miproyecto, tendría una estructura inicial como la siguiente:

miproyecto/
    manage.py
    miproyecto/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py

Seguidamente, se describen los principales componentes de esta estructura:

  • El directorio raíz externo miproyecto/ es un contenedor para el proyecto. Puede tener cualquier nombre.
  • manage.py: es un utilitario de línea de comandos que permite realizar varias tareas administrativas, como ejecutar el proyecto y revisar su consistencia.
  • El directorio interno miproyecto/ es el paquete de Python correspondiente al proyecto.
    • miproyecto/__init__.py: un archivo vacío que le dice a Python que este directorio debe considerarse un paquete.
    • miproyecto/settings.py: configuración del proyecto (aplicaciones, conexiones a bases de datos).
    • miproyecto/urls.py: conjunto de URL que utiliza el proyecto (ej. para acceder a las vistas).
    • miproyecto/asgi.py: punto de entrada para servidores web compatibles con ASGI.
    • miproyecto/wsgi.py: punto de entrada para servidores web compatibles con WSGI.

10.2.1.2 Aplicaciones

Un proyecto contiene varias aplicaciones. Algunas están incorporadas por defecto en cualquier proyecto. Un programador también puede desarrollar sus propias aplicaciones.

Al crear una aplicación llamada miaplicacion, se genera un directorio como el siguiente:

miaplicacion/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py

Las aplicaciones de un proyecto se listan en un vector del archivo settings.py llamado INSTALLED_APPS, como el que se muestra seguidamente:

INSTALLED_APPS = [
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles'
]

Si se incluyen o se crean nuevas aplicaciones para este proyecto, deben agregarse al vector INSTALLED_APPS.

10.2.2 El patrón Modelo–Vista–Controlador

La arquitectura de Django está basada en el patrón de diseño de software denominado Modelo-Vista-Controlador (MVC), el cual es muy popular en el desarrollo de aplicaciones y sus interfaces de usuario. El patrón consiste de tres componentes, los cuales se muestran en la Figura 10.1.

10.2.2.1 Modelo

El modelo es la representación interna de los datos. En el caso de Django, esta representación se implementa mediante clases de Python, las cuales se definen en el archivo models.py de cada aplicación.

Django utiliza ORM (Object Relational Mapping), o mapeo relacional de objetos, para mapear las clases definidas en models.py a las tablas de una base de datos. Esto permite utilizar el paradigma de programación orientada a objetos para manipular la base de datos. Se puede considerar un ORM como una herramienta para convertir los objetos Python de una aplicación a un formato adecuado para ser almacenados en sistemas administradores de bases de datos como Oracle, MySQL, PostgreSQL o SQLite.

10.2.2.2 Vista

Una vista presenta un modelo (i.e. datos) en un formato adecuado. Las vistas se definen en el archivo views.py de cada aplicación.

10.2.2.3 Controlador

Responde a eventos e invoca peticiones al modelo cuando se hace alguna solicitud de información (ej. editar un documento o un registro en una base de datos). El controlador funciona como un “intermediario” entre el modelo y la vista.

10.2.3 Frontend y Backend

Django es marco de trabajo de “pila completa” (full stack), lo que significa que apoya el desarrollo de las dos grandes capas de la arquitectura de un sistema de información: el frontend y el backend, cuya relación se muestra en la Figura 10.2:

El frontend es la capa con la que los usuarios finales interactúan directamente. Incluye todos los aspectos relacionados con la interfaz del usuario, tales como la disposición de los elementos en la pantalla, los estilos y las interacciones que se realizan a través de menús, botones y formularios para ingreso de datos, entre otras.

Por su parte, el backend se encarga del procesamiento de los datos y del funcionamiento interno. No es visible para los usuarios finales. Incluye servidores de aplicaciones, bases de datos y la lógica de aplicación necesaria para procesar las instrucciones que los usuarios envían a través del frontend y para retornar los resultados por la misma vía.

Esta arquitectura permite una clara separación de responsabilidades, donde el frontend se enfoca en la experiencia del usuario y el backend en la lógica de procesamiento y acceso a datos. Esto no solo mejora la organización del código y facilita el mantenimiento, sino que también permite modificar partes de la aplicación de manera independiente, mejorando la eficiencia y la capacidad de manejo de los datos.

10.2.4 El Django REST Framework

El Django REST Framework (DRF) es una biblioteca para la creación de interfaces de programación de aplicaciones (API) para la Web en Django. Es ampliamente utilizado en el desarrollo de servicios backend que necesitan proporcionar datos al frontend mediante solicitudes HTTP (Protocolo de transferencia de hipertexto).

10.3 GeoDjango

GeoDjango es un módulo de Django para el desarrollo de aplicaciones geoespaciales. Enriquece una aplicación Django capacidades como:

  • Acceso a bases de datos espaciales.
  • Consultas espaciales (relaciones topológicas, cálculo de área, cálculo de distancias, etc.).
  • Creación y modificación de geometrías.
  • Transformaciones entre sistemas de coordenadas.

Las aplicaciones desarrolladas con GeoDjango usualmente utilizan una base de datos espacial como PostgreSQL y su extensión PostGIS. También requieren de bibliotecas espaciales como GDAL.

10.4 Ejemplo de desarrollo de un proyecto en GeoDjango

En esta sección, a manera de ejemplo, se desarrolla un proyecto con GeoDjango. El proyecto permite la captura de datos de puntos de observación y la generación de un mapa de calor que muestra las zonas con mayor concentración de puntos.

El proyecto consiste de dos aplicaciones:

  • Un backend en el que se definen los modelos y las vistas. Los datos se capturan mediante la interfaz de administración de Django y se almacenan en una base de datos PosgreSQL y su extensión espacial PostGIS.
  • Un frontend que despliega el mapa de calor.

Las secciones subsiguientes muestran los pasos detallados del proceso de desarrollo del proyecto.

10.4.1 Instalación de herramientas

En esta sección, se detalla la instalación del marco de trabajo Django y la base de datos PostgreSQL y su extensión espacial PostGIS.

10.4.1.1 Django

En este proyecto se utiliza la versión 4.2 de Django. La instalación se realiza en un ambiente del administrador de paquetes Conda, el cual se crea mediante la distribución de Python llamada Miniconda. En el mismo ambiente, se instala la biblioteca geoespacial GDAL versión 3.0.4. Se eligieron estas versiones de Django y GDAL por razones de compatibilidad. También se instala el paquete de Python psycopg2 (para conexión a PostgreSQL) y el Django REST Framework.

1. Instale Miniconda.

2. Cree un ambiente conda:

# Actualización de Conda
conda update conda

# Borrado del ambiente (si es que existe)
# conda remove -n desarrollo_geodjango --all

# Creación del ambiente
conda create -n desarrollo_geodjango

# Activación del ambiente
conda activate desarrollo_geodjango

# Instalación de módulos
conda install -c conda-forge gdal=3.0.4
conda install -c conda-forge django
conda install -c conda-forge psycopg2
conda install -c conda-forge djangorestframework
conda install -c conda-forge djangorestframework-gis

# Desactivación del ambiente (para cuando termine de usarse)
# conda deactivate

3. Revise las versiones del software instalado:

# Versión de Python
python --version
> Python 3.8.15

# Versión de Django
python -m django --version
> 4.2.13

# Versión de GDAL
gdalinfo --version
> GDAL 3.0.4, released 2020/01/28

10.4.1.2 PostgreSQL y PostGIS

4. Instale PostgreSQL y PostGIS.

10.4.2 Creación de una base de datos espaciales

5. Cree una base de datos PostgreSQL llamada mapa_leaflet_postgis que tenga la extensión postgis.

10.4.3 Creación de un proyecto Django y sus aplicaciones

Se crea un proyecto Django con dos aplicaciones: una para el backend y otra para el frontend.

10.4.3.1 Creación del proyecto

6. Cree un proyecto Django:

# Creación
django-admin startproject mapa_leaflet_postgis

# Cambio de directorio
cd mapa_leaflet_postgis

# Revisión
python manage.py check
> System check identified no issues (0 silenced).

# Ejecución del servidor
python manage.py runserver

El servidor debe estar disponible en la dirección http://127.0.0.1:8000/.

10.4.3.2 Creación de las aplicaciones

7. Cree una aplicación para el backend y otra para el frontend.

django-admin startapp mapa_backend
django-admin startapp mapa_frontend

10.5 Configuración de Django y GeoDjango

10.5.1 Adición de aplicaciones

8. Edite el archivo mapa_leaflet_postgis/settings.py y en el vector INSTALLED_APPS agregue las aplicaciones:

  • django.contrib.gis (GeoDjango).
  • rest_framework (DRF).
  • rest_framework_gis (funciones espaciales del DRF).
  • mapa_backend.apps.MapaBackendConfig (backend).
  • mapa_frontend.apps.MapaFrontendConfig (frontend).
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.gis',
    'rest_framework',
    'rest_framework_gis',
    'mapa_backend.apps.MapaBackendConfig',
    'mapa_frontend.apps.MapaFrontendConfig'
]

10.5.2 Configuración de la conexión a la base de datos

9. Edite el archivo mapa_leaflet_postgis/settings.py y en el objeto DATABASES especifique la conexión a la base de datos espaciales que creó:

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'mapa_leaflet_postgis',
        'USER': 'postgres',
        'PASSWORD': 'postgres',
        'HOST': 'localhost',
        'PORT': '5432'
    }
}

10. Revise la configuración:

# Revisión
python manage.py check
> System check identified no issues (0 silenced).

10.5.3 Eliminación de los mensajes de migraciones no aplicadas

11. Elimine los mensajes de “migraciones” no aplicadas (se están desplegando al ejecutar el servidor):

# Comando migrate
python manage.py migrate

# Ejecución del servidor (para revisar)
python manage.py runserver

10.5.4 Creación de un superusuario

Un superusuario tiene todos los privilegios en la interfaz de administración de Django. En las próximas secciones, se utilizará el superusuario para ingresar datos mediante esa interfaz.

11. Cree un superusuario para la intefaz de administración de Django:

# Creación del superusuario
python manage.py createsuperuser

12. Verifique el acceso en http://127.0.0.1:8000/admin/.

10.6 Desarrollo del backend

10.6.1 Modelos

13. Edite el archivo mapa_backend/models.py y agregue un modelo llamado Observacion:

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

class Observacion(models.Model):
    nombre = models.CharField("Nombre de la observación", max_length=50, help_text="Nombre de la observación")
    descripcion = models.CharField("Descripción de la observación", max_length=254, blank=True, help_text="Descripción de la observación")
    ubicacion = models.PointField()

    class Meta:
        verbose_name_plural = "Observaciones"

    def __str__(self):
        return self.nombre

14. Realice la “migración” de los modelos:

# Comando makemigrations
python manage.py makemigrations mapa_backend

# Comando migrate
python manage.py migrate

15. Revise los archivos de migración en la carpeta mapa_backend/migrations y la tabla mapa_backend_observacion en la base de datos mapa_leaflet_postgis. Ambos deben haberse creado como producto de los comandos anteriores.

10.6.2 Configuración de la interfaz de administración

16. Edite el archivo mapa_backend/admin.py:

from django.contrib.gis import admin
from .models import Observacion

# Register your models here.
class CustomGeoAdmin(admin.GISModelAdmin):
    gis_widget_kwargs = {
        'attrs': {
            'default_zoom': 7,
            'default_lon': -84.0,
            'default_lat': 10.0
        }
    }

@admin.register(Observacion)
class ObservacionAdmin(CustomGeoAdmin):
    pass

17. Pruebe la interfaz de administración en http://127.0.0.1:8000/admin/ y agregue algunos registros de observaciones.

**18. Confirme que las observaciones se hayan agregado a la tabla mapa_backend_observacion en la base de datos mapa_leaflet_postgis.

10.6.3 Vistas

De acuerdo con el patrón Modelo - Vista - Controlador (MVC), una vista es una interfaz que presenta información al usuario. En el caso de Django, la presenta por medio de documentos web (i.e. a través del protocolo HTTP). Cada vista recibe una solicitud (request) y retorna una respuesta (response).

10.6.3.1 Ejemplo

Primero, a manera de ejemplo, se crea una vista muy sencilla.

19. Edite el archivo mapa_backend/views.py:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.
def hello_world(request):
    return HttpResponse("Hola mundo")

Cada vista debe estar asociada a un URL.

20. Cree el archivo mapa_backend/urls.py y agregue el siguiente contenido:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.hello_world, name="hello_world")
]

El URL que acaba de crearse en la aplicación mapa_backend debe enlazarse con el archivo de URL del proyecto.

21. Edite el archivo mapa_leaflet_postgis/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("mapa_backend.urls"))
]

De esta manera, los URL de la aplicación quedan enlazados con los URL del proyecto.

22. Verifique el acceso la vista hello_world en http://127.0.0.1:8000/.

Ejercicio: cambie la hilera vacía del URL por backend/ y trate de accederlo.

10.6.3.2 Recuperación de todos los objetos de un modelo

En un nuestro caso, lo que deseamos es consultar una base de datos espacial y obtener respuestas en formato GeoJSON (en el caso de datos vectoriales). Para lograr esto, es necesario realizar una serialización de los modelos. En este caso, significa transformar el contenido de la base de datos al formato GeoJSON.

23. Agregue una vista para el modelo Observacion en mapa_backend/views.py:

from django.shortcuts import render
from django.http import HttpResponse
from django.core.serializers import serialize
from .models import Observacion

# Create your views here.
def observaciones(request):
    consulta = Observacion.objects.all()
    geojson = serialize("geojson", consulta, geometry_field="ubicacion", srid=3857)
    return HttpResponse(geojson, content_type="application/json")

La vista anterior:

  • Recupera todos los objetos del modelo Observacion a través de una consulta realizada mediante el API de Django.
  • Serializa el resultado de la consulta y lo convierte a GeoJSON.
  • Retorna como respuesta la consuta en formato GeoJSON.

24. Para acceder a la vista, debe asociarse con un URL. Modifique de la siguiente forma el archivo mapa_backend/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path("observaciones/", views.observaciones, name="observaciones")
]

25. También debe modificar las URL del proyecto en el archivo mapa_leaflet_postgis/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/v1/", include("mapa_backend.urls"))
]

Puede acceder a la vista en: http://127.0.0.1:8000/api/v1/observaciones/

10.6.3.3 Recuperación de un objeto

Seguidamente, se crea una vista que recupera una observación según el valor del campo pk (este campo se crea automáticamente).

26. Modifique el archivo mapa_backend/views.py y agregue una función llamada observacion_por_llave():

from django.shortcuts import render
from django.http import HttpResponse
from django.core.serializers import serialize
from .models import Observacion

# Create your views here.
def observaciones(request):
    consulta = Observacion.objects.all()
    geojson = serialize("geojson", consulta, geometry_field="ubicacion", srid=3857)
    return HttpResponse(geojson, content_type="application/json")
    
def observacion_por_llave(request, llave):
    observacion = Observacion.objects.get(pk=llave)
    geojson = serialize("geojson", [observacion], geometry_field="ubicacion", srid=3857)
    return HttpResponse(geojson, content_type="application/json")

La función observacion_por_llave() recibe (además de request) el argumento llave, el cual se utiliza para recuperar un objeto de tipo Observacion, el cual se serializa y se retorna.

27. Para acceder a la vista, modifique de la siguiente forma el archivo mapa_backend/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path("observaciones/", views.observaciones, name="observaciones"),
    path("observacion_por_llave/<int:llave>", views.observacion_por_llave, name="observacion_por_llave")
]

Acceda a la nueva vista con, por ejemplo, http://127.0.0.1:8000/api/v1/observacion_por_llave/1.

Ejercicios:

  1. Cree un modelo llamado Ciudad con los atributos nombre, poblacion y point_geom. Ingrese algunos registros a través de la interfaz de administración de Django.
  2. Cree una vista para recuperar los datos de todas las ciudades y otra para recuperar los datos de una ciudad por nombre.

10.6.4 Uso del DRF

En esta sección, se mostrará el uso del DRF en el backend.

28. Cree el archivo mapa_backend/serializers.py y agregue un serializador para el modelo Observacion, con el siguiente contenido:

from .models import Observacion
from rest_framework_gis.serializers import GeoFeatureModelSerializer


class ObservacionSerializador(GeoFeatureModelSerializer):
    class Meta:
        model = Observacion
        geo_field = 'ubicacion'

        fields = (
            'pk',
            'nombre',
            'descripcion'
        )

29. Utilice el serializador que acaba de crear en dos vistas. Edite el archivo mapa_backend/views.py y agregue el siguiente contenido (puede eliminar o comenat el código de las vistas que creó en los pasos anteriores):

from .models import Observacion
from .models import Ciudad
from .serializers import ObservacionSerializador
from rest_framework import generics

# Create your views here.
class ObservacionLista(generics.ListAPIView):
    queryset = Observacion.objects.all()
    serializer_class = ObservacionSerializador
    name = 'observacion-lista'

class ObservacionDetalle(generics.RetrieveAPIView):
    queryset = Observacion.objects.all()
    serializer_class = ObservacionSerializador
    name = 'observacion-detalle' 

Puede ver la especificación técnica de las vistas genéricas del DRF en Generic views - Django REST Framework.

30. Para agregar los URL correspondientes a las vistas, edite el archivo mapa_backend/urls.py y agregue el siguiente contenido (antes comente el código de los URL que creó en los pasos anteriores):

from django.urls import path
from . import views

urlpatterns = [
    path("observaciones/", views.ObservacionLista.as_view(), name=views.ObservacionLista.name),
    path("observaciones/<int:pk>/", views.ObservacionDetalle.as_view(), name=views.ObservacionDetalle.name)
]

Puede acceder a las vistas en:

Ejercicios

  1. Cambie a la vista genérica de la vista ObservacionLista a CreateAPIView (https://www.django-rest-framework.org/api-guide/generic-views/#createapiview) y en cree una nueva observación en la interfaz web que se muestra al invocar la vista.
  2. Cambie a la vista genérica de la vista ObservacionDetalle a RetrieveUpdateDestroyAPIView (https://www.django-rest-framework.org/api-guide/generic-views/#retrieveupdatedestroyapiview) y modifique y borre observaciones en la interfaz web que se muestra al invocar la vista.
  3. Cree un serializador para el modelo Ciudad y vistas para recuperar todos los objetos y un objeto por la llave primaria (pk).

10.7 Desarrollo del frontend

El frontend recibe, mediante vistas, los datos contenidos en los modelos del backend y los despliega para el usuario. En este caso, se muestran en un mapa desarrollado en la bibioteca Leaflet.

10.7.1 Archivos estáticos

Las vistas acceden archivos JS y CSS. En este contexto, estos archivos se denominan “estáticos”, por tener contenido fijo o estático, a diferencia de las plantillas, que tienen contenido dinámico.

31. Cree los siguientes archivos estáticos en la aplicación frontend (note que debe crear algunos directorios también):

  • mapa_frontend/static/css/estilos.css
*{
    box-sizing: border-box;
}

body {
    padding: 0;
    margin: 0;
    overflow: auto;
}

#mapaid {
    height: 100vh;
    width: 100vw;
}
  • mapa_frontend/static/js/principal.js
// Evento que se dispara al cargar la página web
document.addEventListener("DOMContentLoaded", iniciar)

// Función que se invoca con el disparo de "DOMContentLoaded"
function iniciar() {

    // Objeto del mapa Leaflet
    var mapa = L.map('mapaid').setView([9.5, -84], 8);

    // Capa base de Carto
    positromap = L.tileLayer(
        "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png",
        {
          attribution:
            '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
          subdomains: "abcd",
          maxZoom: 20,
        }
    ).addTo(mapa);

    // Capa base de OSM
    osm = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
        maxZoom: 19,
        attribution:
        '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
    });
  
    // Capa base de ESRI
    esriworld = L.tileLayer(
        "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
        {
        attribution:
            "Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",
        }
    );

    // Objeto de capas base
    var mapasbase = {
      "Carto Positron": positromap,
      OpenStreetMap: osm,
      "ESRI WorldImagery": esriworld,
    };

    // Control de capas
    control_capas = L.control
    .layers(mapasbase, null, { collapsed: false })
    .addTo(mapa);

    // Función asíncrona que realiza una solicitud HTTP (tipo GET) 
    // a una URL especificada, procesa la respuesta JSON y luego
    // ejecuta una función pasada como argumento con los datos JSON obtenidos.
    const fetchGetRequest = async(url, func) => {
        try {
            const response = await fetch(url)
            const json = await response.json()
            return func(json)
        } catch (error) {
            console.log(error.message)
        }    
    }

    // Función que agrega los datos GeoJSON al mapa
    const agregarObservacionesAlMapa = (json) => {
        // console.log(json)

        // Se obtienen los datos en GeoJSON
        observaciones = L.geoJSON(json, {}).addTo(mapa);

        // Se agrgan los datos GeoJSON al mapa
        control_capas.addOverlay(observaciones, "Observaciones");      
    }

    // Llamado a fetchGetRequest()
    fetchGetRequest('/api/v1/observaciones', agregarObservacionesAlMapa)
}

10.7.2 Plantillas

Las plantillas (templates) son archivos HTML combinados con el Django Template Languaje (DTL). Estos archivos definen la estructura de las páginas web. Utilizan el DTL para incluir contenido dinámico, permiten incorporar datos provenientes de las vistas y pueden incluir etiquetas y filtros de Django para lógica básica y manipulación de datos.

32. Cree los archivos:

  • mapa_frontend/templates/mapa_frontend/base.html
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block titulo %}Observaciones{% endblock %}</title>
    <!-- CSS -->
    {% block enlaces_css %}{% endblock %}
</head>
<body>
    <!-- JS -->
    {% block enlaces_js %}{% endblock %}

    <div id="content">
        {% block contenido %}{% endblock %}
    </div>    
</body>

</html>
  • mapa_frontend/templates/mapa_frontend/observaciones_base.html
{% extends './base.html' %}
{% load static %}

{% block enlaces_css %}
<!-- Enlace a hoja CSS de Leaflet -->
<link
  rel="stylesheet"
  href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
  integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
  crossorigin=""
/>
<!-- Enlace a estilos del mapa -->
<link rel="stylesheet" href="{% static 'css/estilos.css' %}">
{% endblock %}

{% block enlaces_js %}
<!-- Enlace a biblioteca JavaScript de Leaflet -->
<script
  src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
  integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
  crossorigin=""
></script>
<!-- Código JS/Leaflet para crear y configurar el mapa -->
<script src="{% static 'js/principal.js' %}"></script>
{% endblock %}

{% block contenido %}
<div id="mapaid"></div>
{% endblock %}

10.7.3 Vistas

Las vistas del frontend retornan HTML, para lo que emplean el método render().

33. Edite el archivo mapa_frontend/views.py:

from django.shortcuts import render

# Create your views here.
def observacionesListaMapa(request):
    return render(request, 'mapa_frontend/observaciones_base.html')

34. Cree el archivo mapa_frontend/urls.py y agregue el siguiente contenido:

from django.urls import path
from . import views

app_name = 'mapa_frontend'
urlpatterns = [
    path('', views.observacionesListaMapa, name='observaciones-lista-mapa')
]

35. Edite el archivo mapa_leaflet_postgis/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/v1/", include("mapa_backend.urls")),
    path('', include('mapa_frontend.urls'))
]

36. El mapa Leaflet con los puntos de observación ingresados por medio de la interfaz de administración de Django debe estar visible en la dirección http://127.0.0.1:8000/.

Ejercicios

  1. Agregue al mapa Leaflet una capa de puntos agrupados, con base en la capa de observaciones.
  2. Agregue al mapa Leaflet una capa de calor, con base en la capa de observaciones.

10.8 Carga de datos externos

En esta sección se muestra como cargar datos geoespaciales a través de GeoDjango para su uso en las aplicaciones.

37. Cree el directorio mapa_backend/datos y coloque ahí el archivo registros-presencia.gpkg.

Este archivo contiene un conjunto de registros de presencia de especies.

38. Ejecute el siguiente comando:

python manage.py ogrinspect mapa_backend/datos/registros-presencia.gpkg RegistroPresencia --mapping --name-field species --null true

Como salida, generará un modelo Django y un mapeo de campos que se utilizará en un script de Python.

39. Agregue el modelo a mapa_backend/models.py (note los cambios en los argumentos max_length)

class RegistroPresencia(models.Model):
    species = models.CharField(max_length=50, null=True)
    sex = models.CharField(max_length=50, null=True)
    age = models.IntegerField(null=True)
    decimallongitude = models.FloatField(null=True)
    decimallatitude = models.FloatField(null=True)
    geom = models.PointField()

    def __str__(self): return self.species

40. Revise la documentación en https://docs.djangoproject.com/en/4.1/ref/contrib/gis/tutorial/#layermapping y cree el archivo mapa_backend/load.py:

from pathlib import Path
from django.contrib.gis.utils import LayerMapping
from .models import RegistroPresencia

registropresencia_mapping = {
    'species': 'species',
    'sex': 'sex',
    'age': 'age',
    'decimallongitude': 'decimalLongitude',
    'decimallatitude': 'decimalLatitude',
    'geom': 'POINT',
}

registros_gpkg = Path(__file__).resolve().parent / 'datos' / 'registros-presencia.gpkg'

def run(verbose=True):
    lm = LayerMapping(RegistroPresencia, registros_gpkg, registropresencia_mapping, transform=False)
    lm.save(strict=True, verbose=verbose)

41. Realice las migraciones:

python manage.py makemigrations    
python manage.py migrate

42. Abra la interfaz de línea de comandos de Django:

python manage.py shell

43. Cargue los datos desde el archivo:

from mapa_backend import load
load.run()

44. Consulte los datos cargados:

from mapa_backend.models import RegistroPresencia
RegistroPresencia.objects.count()
RegistroPresencia.objects.all()
RegistroPresencia.objects.get(id = 1)
RegistroPresencia.objects.get(id = 1).sex
RegistroPresencia.objects.filter(sex="Hembra")
RegistroPresencia.objects.filter(sex="Hembra").count()
exit()

45. Edite el archivo mapa_backend/admin.py para incluir el modelo en la interfaz de administración de Django:

from django.contrib.gis import admin
from .models import Observacion
from .models import RegistroPresencia


# Register your models here.
class CustomGeoAdmin(admin.GISModelAdmin):
    gis_widget_kwargs = {
        'attrs': {
            'default_zoom': 7,
            'default_lon': -84.0,
            'default_lat': 10.0
        }
    }

@admin.register(Observacion)
class ObservacionAdmin(CustomGeoAdmin):
    pass

@admin.register(RegistroPresencia)
class RegistroPresenciaAdmin(CustomGeoAdmin):
    pass

Ejercicios

  1. Cree un serializador y una vista en modo lista para todas las instancias del modelo RegistroPresencia.
  2. Cree los URL necesarios.
  3. Modifique la aplicación Leaflet para desplegar los registros de presencia.

10.9 Recursos de interés

Python Web Apps 101 Python Web Apps: Django