[PYTHON] [Note d'étude] Créez un serveur de tuiles vectorielles GeoJSON avec Fast API

Motivation, vue d'ensemble

Dans GeoDjango, par exemple, here Dans le formulaire, il semble qu'il existe une fonction qui peut acquérir des informations de la table DB (en utilisant PostGIS) et la faire fonctionner comme un serveur de tuiles vectorielles.

Je me suis demandé si je pouvais faire quelque chose de similaire avec le micro-framework Web de Python (ici j'utilise FastAPI), j'ai donc fait beaucoup de recherches et pris une note pour moi-même.

Idéalement, ce devrait être PostGIS, ou s'il s'agit de NoSQL, ce devrait être la [Geospatial Query] de MongoDB (https: // docs). Nous avons construit un système en utilisant autour de .mongodb.com / manual / geospatial-queries /), et le format de distribution n'est pas seulement GeoJSON mais aussi un format de tuile vectorielle binaire plus pratique ([Référence](https: //docs.mapbox.). Je voudrais prendre en charge com / vector-tiles / specification /)), mais il est difficile de tous les supporter soudainement, alors ici

L'accent principal est mis sur. D'autre part

--Connexion / liaison avec DB (PostgreSQL, MongoDB, etc.) --Distribution au format binaire tel que mvt (pbf)

Cela n'est pas fait à ce stade et sera un problème pour l'avenir. (En d'autres termes, ** la situation est encore loin d'être pratique ** ... si possible, nous prévoyons d'enquêter et d'ajouter des articles à l'avenir)

Entraine toi

démo

Peek 2020-08-30 01-28.gif

Environnement d'exécution

Je me souviens que les dépendances et les constructions des bibliothèques géospatiales étaient inopinément gênantes, alors je suis en train de créer un environnement à l'aide de conda.

L'environnement virtuel conda est, par exemple

conda_packages.yml


name: fastapi_geojsontileserver
channels:
  - conda-forge
  - defaults
dependencies:
  # core
  - python==3.7.*
  # FastAPI
  - fastapi
  - uvicorn
  - aiofiles
  # for handling GeoSpatial data
  - pandas>=1.1
  - geopandas>=0.8
  - gdal==3.0.*
  - shapely

Préparez un fichier YAML comme

#Créer un environnement virtuel à partir du fichier YAML
conda env create -f conda_packages.yml

#Activez l'environnement virtuel
conda activate <Nom de l'environnement virtuel>

Faire.

Le nom de l'environnement virtuel (name: part) et la bibliothèque à installer (dependencies: part) du fichier YAML doivent être modifiés si nécessaire.

Structure de répertoire / fichiers

Par exemple:

.
├── app
│   ├── __init__.py
│   ├── client
│   │   └── index.html
│   └── main.py
└── data
    └── test.geojson

--ʻApp / main.py` implémente le traitement du serveur de tuiles

Des exemples de ʻapp / main.py et ʻapp / client / index.html sont détaillés ci-dessous.

app/main.py

main.py


"""
app main

GeoJSON VectorTileLayer Test
"""

import pathlib
import json
import math
from typing import Optional

from fastapi import (
    FastAPI,
    HTTPException,
)
from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse, HTMLResponse
import geopandas as gpd
import shapely.geometry

# const
PATH_STATIC = str(pathlib.Path(__file__).resolve().parent / "client")

EXT_DATA_PATH = "./data/"   # TMP

# test data
gdf_testdata = gpd.read_file(EXT_DATA_PATH + "test.geojson")


def create_app():
    """create app"""
    _app = FastAPI()

    # static
    _app.mount(
        "/client",
        StaticFiles(directory=PATH_STATIC, html=True),
        name="client",
    )

    return _app


app = create_app()


@app.get('/', response_class=HTMLResponse)
async def site_root():
    """root"""
    return RedirectResponse("/client")


@app.get("/tiles/test/{z}/{x}/{y}.geojson")
async def test_tile_geojson(
    z: int,
    x: int,
    y: int,
    limit_zmin: Optional[int] = 8,
) -> dict:
    """
    return GeoJSON Tile
    """

    if limit_zmin is not None:
        if z < limit_zmin:
            raise HTTPException(status_code=404, detail="Over limit of zoom")

    # test data
    gdf = gdf_testdata.copy()

    bbox_polygon = tile_bbox_polygon(z, x, y)

    # filtering
    intersections = gdf.geometry.intersection(bbox_polygon)
    gs_filtered = intersections[~intersections.is_empty]    # geoseries
    gdf_filtered = gpd.GeoDataFrame(
        gdf.loc[gs_filtered.index, :].drop(columns=['geometry']),
        geometry=gs_filtered,
    )

    # NO DATA
    if len(gs_filtered) == 0:
        raise HTTPException(status_code=404, detail="No Data")

    # return geojson
    return json.loads(
        gdf_filtered.to_json()
    )


def tile_coord(
    zoom: int,
    xtile: int,
    ytile: int,
) -> (float, float):
    """
    This returns the NW-corner of the square. Use the function
    with xtile+1 and/or ytile+1 to get the other corners.
    With xtile+0.5 & ytile+0.5 it will return the center of the tile.
    http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_numbers_to_lon..2Flat._2
    """
    n = 2.0 ** zoom
    lon_deg = xtile / n * 360.0 - 180.0
    lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
    lat_deg = math.degrees(lat_rad)
    return (lon_deg, lat_deg)


def tile_bbox_polygon(
    zoom: int,
    xtile: int,
    ytile: int,
) -> shapely.geometry.Polygon:
    """
    create bbox for Tile by using shapely.geometry
    """
    z = zoom
    x = xtile
    y = ytile

    # get bbox
    nw = tile_coord(z, x, y)
    se = tile_coord(z, x+1, y+1)
    bbox = shapely.geometry.Polygon(
        [
            nw, (se[0], nw[1]),
            se, (nw[0], se[1]), nw
        ]
    )
    return bbox

app/client/index.html

index.html


<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
  <title>Leaflet GeoJSON TileLayer(Polygon) Test</title>

  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
  <script src="https://unpkg.com/[email protected]/leaflet-hash.js"></script>

  <style>
    #map {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
    }

    .leaflet-control-container::after {
      content: url(https://maps.gsi.go.jp/image/map/crosshairs.png);
      z-index: 1000;
      display: block;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }

  </style>
</head>

<body>
  <div id="map"></div>
  <script>
    // Initalize map
    const map = L.map("map", L.extend({
      minZoom: 5,
      zoom: 14,
      maxZoom: 22,
      center: [35.5, 139.5],
    }, L.Hash.parseHash(location.hash)));
    map.zoomControl.setPosition("bottomright");
    L.hash(map);


    // GeoJSON VectorTileLayer
    const tile_geojson_sample = Object.assign(new L.GridLayer({
        attribution: "hoge",
        minZoom: 4,
        maxZoom: 22,
    }), {
        createTile: function(coords) {
            const template = "http://localhost:8000/tiles/test/{z}/{x}/{y}.geojson?limit_zmin=7";
            const div = document.createElement('div');
            div.group = L.layerGroup();
            fetch(L.Util.template(template, coords)).then(a => a.ok ? a.json() : null).then(geojson => {
                if (!div.group) return;
                if (!this._map) return;
                if (!geojson) return;
                div.group.addLayer(L.geoJSON(geojson, {
                    style: () => {
                        return {}
                    }
                }).bindPopup("test"));
                div.group.addTo(this._map);
            });
                return div;
            }
        }).on("tileunload", function(e) {
        if (e.tile.group) {
            if (this._map) this._map.removeLayer(e.tile.group);
            delete e.tile.group;
        }
    });

    // basemap layers
    const osm = L.tileLayer('http://tile.openstreetmap.jp/{z}/{x}/{y}.png', {
        attribution: "<a href='http://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors",
        // minZoom: 10,
        maxNativeZoom: 18,
        maxZoom: 22,
    });

    const gsi_std = L.tileLayer(
'https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
        {
            attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>Tuile Institut de géographie (carte standard)</a>",
            maxNativeZoom: 18,
            maxZoom: 22,
            opacity:1
        });

    const gsi_pale = L.tileLayer(
        'http://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
        {
            attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>Tuile Institut géographique (carte des couleurs claires)</a>",
            maxNativeZoom: 18,
            maxZoom: 22,
        });

    const gsi_ort = L.tileLayer(
'https://cyberjapandata.gsi.go.jp/xyz/ort/{z}/{x}/{y}.jpg',
        {
            attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>Carrelage Institut de géographie (Ortho)</a>",
            maxNativeZoom: 17,
            maxZoom: 22,
            opacity:0.9
        });

    const gsi_blank = L.tileLayer(
'https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png',
        {
            attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>Tuile Institut de géographie (carte blanche)</a>",
            maxNativeZoom: 14,
            maxZoom: 22,
            opacity:1,
        });


    L.control.scale({
        imperial: false,
        metric: true,
    }).addTo(map);

    const baseLayers ={
        "Tuile Institut de géographie (carte standard)": gsi_std,
        "Tuile Institut géographique (carte des couleurs claires)": gsi_pale,
        "Carrelage Institut de géographie (Ortho)": gsi_ort,
        "Tuile Institut de géographie (carte blanche)": gsi_blank,
        'osm': osm.addTo(map),
    };

    const overlays = {"GeoJSON TileLayer(sample)": tile_geojson_sample};

    L.control.layers(baseLayers, overlays, {position:'topright',collapsed:true}).addTo(map);
    const hash = L.hash(map);

  </script>
</body>

</html>

Dans la partie const tile_geojson_sample, spécifiez l'URL du serveur de tuiles que vous avez créé et exécutez + la chaîne de requête et lisez-la. L'essentiel est de vérifier si le serveur de tuiles fonctionne correctement, et comme ce n'est pas la partie essentielle, d'autres détails sont omis.

Pour créer le HTML ci-dessus,

Je l'ai mentionné.

Lancer et exécuter

Pour démarrer le serveur, par exemple, tapez la commande suivante:

uvicorn app.main:app

Vous pouvez vérifier l'API du serveur de tuiles créé en tant que Swagger (Open API) à partir, par exemple, de http: // localhost: 8000. (Génération automatique de documents + Vous pouvez facilement vérifier l'opération sur place)

image.png

Si vous lancez localement, vous pouvez vérifier le HTML de visualisation créé ci-dessus en ouvrant http: // localhost: 8000 / client etc.:

image.png

image.png

image.png

L'exemple d'exécution ci-dessus utilise les données de limite de ville / quartier / ville / village (Polygon, MultiPolygon), mais il peut être exécuté presque de la même manière en utilisant LineString ou Point comme données. À propos, la partie qui manque étrangement est le problème du contenu des données de test. (Lors du traitement des intersections avec les géopandas, la sélection des données, etc. est effectuée à l'avance, par exemple en supprimant celles «non valides»)

Si vous regardez le journal de la console FastAPI (uvicorn) au moment de l'exécution,

INFO:     127.0.0.1:54910 - "GET /tiles/test/9/451/202.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54902 - "GET /tiles/test/9/456/202.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54908 - "GET /tiles/test/9/450/199.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54910 - "GET /tiles/test/9/457/199.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54896 - "GET /tiles/test/9/457/200.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54904 - "GET /tiles/test/9/450/201.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54906 - "GET /tiles/test/9/457/201.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54908 - "GET /tiles/test/9/457/202.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54904 - "GET /tiles/test/9/451/198.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54902 - "GET /tiles/test/9/450/202.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54896 - "GET /tiles/test/9/452/198.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54910 - "GET /tiles/test/9/450/198.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54906 - "GET /tiles/test/9/453/198.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54910 - "GET /tiles/test/9/454/198.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54902 - "GET /tiles/test/9/449/199.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54904 - "GET /tiles/test/9/449/200.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54908 - "GET /tiles/test/9/449/198.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54910 - "GET /tiles/test/9/448/200.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54906 - "GET /tiles/test/9/449/201.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54896 - "GET /tiles/test/9/448/199.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54906 - "GET /tiles/test/7/112/49.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54908 - "GET /tiles/test/9/448/198.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54902 - "GET /tiles/test/9/455/198.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54904 - "GET /tiles/test/9/448/201.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54906 - "GET /tiles/test/7/111/49.geojson?limit_zmin=7 HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:54902 - "GET /tiles/test/7/114/49.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54896 - "GET /tiles/test/7/113/49.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54910 - "GET /tiles/test/7/113/48.geojson?limit_zmin=7 HTTP/1.1" 200 OK
INFO:     127.0.0.1:54902 - "GET /tiles/test/7/112/51.geojson?limit_zmin=7 HTTP/1.1" 200 OK

C'est comme ressentir.

Résumé

La plupart des données et traitements utilisés sont provisoires, mais j'ai pu construire un serveur de tuiles qui fonctionne comme ça.

Comme mentionné au début, il y a en fait de nombreux problèmes, mais pour le moment, c'était une bonne étude des tuiles de carte, des tuiles vectorielles et du micro-framework Web de Python en général.

référence

Recherche précédente

Expérience de fourniture de tuiles vectorielles du National Land Research Institute

Globalement assez utile

Informations de format pour les tuiles vectorielles

GeoJSON

protobuf

Ceux avec l'extension «.mvt» ou «.pbf». Non implémenté cette fois, mais comme problème ultérieur

Autre

Une fonction différente de "Dynamic Vector Tile Server" mais utile

Tuile de vecteur statique

geobuf

Étant donné que les serveurs de tuiles dynamiques peuvent avoir des problèmes de charge et de temps de réponse, dans certains cas, il est tout à fait possible que cette méthode soit plus réaliste que la tuile.

Recommended Posts

[Note d'étude] Créez un serveur de tuiles vectorielles GeoJSON avec Fast API
Créez un environnement d'API rapide avec docker-compose
[Fast API + Firebase] Construction d'un serveur API pour l'authentification au porteur
Créez un serveur API Web ultra-rapide avec Falcon
Persistez le serveur d'API Flask avec Forever