[PYTHON] [Studiennotiz] Erstellen Sie einen GeoJSON-Vektor-Kachelserver mit Fast API

Motivation, Überblick

In GeoDjango zum Beispiel hier In dem Formular scheint es eine Funktion zu geben, die Informationen aus der DB-Tabelle (mit PostGIS) abrufen und als Vektor-Kachelserver betreiben kann.

Ich habe mich gefragt, ob ich mit Pythons Micro-Web-Framework (hier verwende ich FastAPI) etwas Ähnliches tun könnte, also habe ich viel recherchiert und mir eine Notiz gemacht.

Idealerweise sollte es [PostGIS] sein (https://postgis.net/docs/manual-2.4/postgis-ja.html), oder wenn es NoSQL ist, sollte es MongoDBs [Geospatial Query](https: // docs) sein. Wir haben ein System mit .mongodb.com / manual / geospatial-queries /) erstellt. Das Verteilungsformat ist nicht nur GeoJSON, sondern auch ein praktischeres Format für binäre Vektorkacheln ([Referenz](https: //docs.mapbox.). Ich würde gerne com / vector-tiles / specification /)) unterstützen, aber es ist schwierig, alle plötzlich zu unterstützen, also hier

Das Hauptaugenmerk liegt auf. Auf der anderen Seite

--Verbindung / Verknüpfung mit DB (PostgreSQL, MongoDB usw.)

Dies ist derzeit noch nicht geschehen und wird ein Problem für die Zukunft sein. (Mit anderen Worten, ** die Situation ist noch weit vom praktischen Nutzen entfernt ** ... Wenn möglich, planen wir, Artikel in Zukunft zu untersuchen und hinzuzufügen)

Trainieren

Demo

Peek 2020-08-30 01-28.gif

Ausführungsumgebung

Ich erinnere mich, dass die Abhängigkeiten und Builds von Geodatenbibliotheken unerwartet problematisch waren. Deshalb baue ich hier die Umgebung mit conda.

Die conda virtuelle Umgebung ist zum Beispiel

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

Bereiten Sie eine YAML-Datei wie vor

#Erstellen Sie eine virtuelle Umgebung aus einer YAML-Datei
conda env create -f conda_packages.yml

#Aktivieren Sie die virtuelle Umgebung
conda activate <Name der virtuellen Umgebung>

Machen.

Der Name der virtuellen Umgebung (name: part) und die zu installierende Bibliothek (dependencies: part) der YAML-Datei sollten nach Bedarf bearbeitet werden.

Verzeichnisstruktur / Dateien

Zum Beispiel:

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

Beispiele für "app / main.py" und "app / client / index.html" sind unten aufgeführt.

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'>Kachel des Geographie-Instituts (Standardkarte)</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'>Geografische Institutskachel (helle Farbkarte)</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'>Geographie Institut Fliese (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'>Geography Institute Kachel (weiße Karte)</a>",
            maxNativeZoom: 14,
            maxZoom: 22,
            opacity:1,
        });


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

    const baseLayers ={
        "Kachel des Geographie-Instituts (Standardkarte)": gsi_std,
        "Geografische Institutskachel (helle Farbkarte)": gsi_pale,
        "Geographie Institut Fliese (Ortho)": gsi_ort,
        "Geography Institute Kachel (weiße Karte)": 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>

Geben Sie im Teil const tile_geojson_sample die URL des von Ihnen erstellten Kachelservers an, führen Sie + die Abfragezeichenfolge aus und lesen Sie sie. Die Hauptsache ist, zu überprüfen, ob der Kachelserver ordnungsgemäß funktioniert. Da dies nicht der wesentliche Teil ist, werden weitere Details weggelassen.

Um den obigen HTML-Code zu erstellen,

Ich bezog mich auf.

Starten und ausführen

Geben Sie zum Starten des Servers beispielsweise den folgenden Befehl ein:

uvicorn app.main:app

Die API des erstellten Kachelservers kann als Swagger (Open API) beispielsweise von http: // localhost: 8000 bestätigt werden. (Automatische Dokumentenerstellung + Sie können den Vorgang einfach vor Ort überprüfen.)

image.png

Wenn Sie lokal starten, können Sie den oben erstellten Visualisierungs-HTML-Code überprüfen, indem Sie http: // localhost: 8000 / client etc öffnen.

image.png

image.png

image.png

Das obige Ausführungsbeispiel verwendet Grenzdaten für Stadt / Gemeinde / Stadt / Dorf (Polygon, MultiPolygon), kann jedoch auf fast dieselbe Weise ausgeführt werden, indem "LineString" oder "Point" als Daten verwendet werden. Der Teil, der seltsamerweise fehlt, ist übrigens das Problem des Inhalts der Testdaten. (Bei der Kreuzungsverarbeitung mit Geopandas werden Daten im Voraus ausgewählt, z. B. "ungültige" Daten entfernen.)

Wenn Sie sich das FastAPI-Konsolenprotokoll (uvicorn) zur Laufzeit ansehen,

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

Es fühlt sich an wie.

Zusammenfassung

Die meisten verwendeten Daten und Verarbeitungen sind vorläufig, aber ich konnte einen Kachelserver erstellen, der so funktioniert.

Wie eingangs erwähnt, gibt es tatsächlich viele Probleme, aber vorerst war es eine gute Studie über Kartenkacheln, Vektorkacheln und Pythons Micro-Web-Framework im Allgemeinen.

Referenz

Frühere Forschungen

Experiment zur Bereitstellung von Vektorfliesen des Nationalen Landforschungsinstituts

Insgesamt sehr hilfreich

Formatinformationen für Vektorkacheln

GeoJSON

protobuf

Die mit der Erweiterung ".mvt" oder ".pbf". Diesmal nicht implementiert, aber als Folgeproblem

Andere

Eine andere Funktion als "Dynamic Vector Tile Server", aber hilfreich

Statische Vektorkachel

geobuf

Da bei dynamischen Kachelservern Probleme mit der Last und der Antwortzeit auftreten können, ist diese Methode in einigen Fällen durchaus realistischer als gekachelt.

Recommended Posts

[Studiennotiz] Erstellen Sie einen GeoJSON-Vektor-Kachelserver mit Fast API
Erstellen Sie mit Docker-Compose eine schnelle API-Umgebung
[Fast API + Firebase] Aufbau eines API-Servers für die Bearer-Authentifizierung
Erstellen Sie mit Falcon einen Light-Speed-Web-API-Server
Behalten Sie den Flask API-Server für immer bei