SoRa - Sozial-Raumwissenschaftliche Forschungsdateninfrastruktur

Da es sich bei Sora um ein reines Backend handelt, wird auch nur dieses vorgestellt. Das Backend hat vor allem die Aufgabe die im SoRa Projekt gestellten Anforderungen zu erfüllen indem freie Nutzeranfragen auf topografischen Geobasisdaten beantwortet werden. Diese werden vom Clienten gestellt.

Inhalt

Architektur

Alle Anfragen für das SoRa-Forschungsprojekt werden an die Monitor-API geschickt, wofür der Blueprints sora implementiert wurde. Innerhalb der routes.py befindet sich das RequestMapping, welches für die entsprechenden query strings alle Anfragen beantwortet.

Auf dem ESRI-Server wurden die Service-Layer implementiert. Der Flask Microservice auf https://monitor.ioer.de/monitor_api/sora nimmt die Requests des Gesis- Location Mapping an. Über die Klasse Request-Manager, werden die Anfragen gestellt. Das die Kommunikation mit einem WPS teilweise sehr kompliziert sein kann, wurde diese Service-Schicht implementiert.

REST-Schnittstelle

REST steht für REpresentational State Transfer, API für Application Programming Interface. Gemeint ist damit ein Programmierschnittstelle, die sich an den Paradigmen und Verhalten des World Wide Web (WWW) orientiert und einen Ansatz für die Kommunikation zwischen Client und Server in Netzwerken beschreibt. Quelle

Diese Schnittstelle wurde sowohl auf dem Esri-Server als auch auf dem Flask-Microservice eingesetzt. Da bei allen durch das Projekt genutzten Diensten z.T. sehr große Koordinaten-Anfragen gestellt werden, unterstützt die API nur POST-Requests. Da auf orginären Rasterkarten gerechnet wird, kann die Anfrage je nach Menge der Koordinaten länger Dauern, wird Polling angewendet. Hierfür wird dem Clienten auf seine POST Anfrage eine JobID übermittelt. Jetzt kann über eine entsprechend parametrisierte GET Anfrage periodisch angefragt werden, ob das Ergebnis bereitgestellt wurde.


Nachfolgend ist die Kommunikation mit der REST-API dokumentiert, um Requests und Responses zu verdeutlichen wurden Beispiele hinterlegt. Der Genau Code für die WPS ist im Punkt Esri-Server dokumentiert. Die Code-Beschreibung für Flask ist hier zu finden.

  1. Abfrage von Indikatorwerten zu Koordinaten
  2. Routing zum nächstgelegenen POI
  3. Routing zwischen zwei Koordinaten

1. Abfrage von Indikatorwerten zu Koordinaten

Beschreibung

Mit diesem Service können für vorgegebene Koordinaten die jeweiligen Indikatorwerte abgefragt werden. Wird ein Buffer-Wert gesetzt, ermittelt der Service den Durchschnitt des Indikators innhalb des Buffer-Bereichs. Hierbei wird ein quadratische Buffer um jede Koordinate gesetzt, dessen Räumlicher Durchschnitt über den Paramter buffer gesetzt werden kann

Endpoint: https://monitor.ioer.de/monitor_api/sora/services

  1. Post der JSON-Datei:
    • Query String: job=coordinates
    • Datei: folgend eine beispielhafte JSON Datei, welche alle notwendigen Parameter übermittelt

        {
            "coordinates":
                [{"id":8291,"x":"4340896,28050814","y":"2764378,83432727"},
                {"id":3058,"x":"4340657,55155301","y":"2765292,51267246"},
                {"id":3438,"x":"4342453,82536125","y":"2764230,5410444"},
                {"id":249,"x":"4339813,12908578","y":"2765946,78532257"},
                {"id":3800,"x":"4339879,51749317","y":"2765648,13305142"}],
            "indicators":
                [{"id":"S12RG","year":"2012"},
                {"id":"S12RG","year":"2015"},
                {"id":"F01RG","year":"2015","buffer":"100"}],
            "epsg":"3035"
        }
      
      • coordinates: x/y Koordinaten mit der gesetzten id, hierbei ist es egal ob die Koordinaten als Zahlenwert(.) oder String übergeben werden. Es würde genauso funktionieren:

          ...
          {"id":8291,"x":"4340896.28050814","y":"2764378.83432727"}
          ...
        
        
      • indicators: id des Indikators und der gewünschte Zeitschnitt, optional ist der buffer, welcher in Meter angegeben wird.
      • epsg: zugrundeliegendes Koordinatensystem
      • Als Antwort auf den request erfolgt eine JSON, welche die JobID beinhaltet und den Status.
        {
         "jobId": "j14585674d2434aada462cffe22862324",
         "jobStatus": "esriJobSubmitted"
        }
      

      Beispiel mit curl:

        curl --header "Content-Type: application/json" \
             --request POST \
             --data '{"coordinates":[{"x":"12.067024122630599","y":"54.1015602797111","id":66},{"x":"12.141539756251","y":"54.1154796784391","id":99}],"indicators":[{"id":"S12RG","year":"2017"}],"epsg":"4326"}' \
             https://monitor.ioer.de/monitor_api/sora/services?job=coordinates
      

      Als Ergebnis wird dann folgende JSON mit einer Job-ID geliefert:

        {
        "jobId": "j19e57f40c4a44755a32078d9ad7097f2",
        "jobStatus": "esriJobSubmitted"
        }
      

Wurde dem Clienten die JobID als Response übermittelt, kann via HTTP-GET das Ergebnis periodisch angefragt werden. Hat das Backend ein Ergebnis zur Verfügung, wird dieses bereitgestellt.

  1. GET des Ergebnisses:
    • Query Strings:
      • job=coordinates
      • job_id=id des Jobs aus dem Post-Request, in diesem Beispiel j14585674d2434aada462cffe22862324
    • Wird der Job noch berechnet, sendet der Service eine entsprechende Mitteilung als JSON zurück und muss weiter angefragt werden. Liegt das Ergebnis vor, wird das Ergebnis als JSON gesendet.

    Beispiel mit curl:

     curl --request GET \
          -d 'job=coordinates'
          -d 'job_id=j19e57f40c4a44755a32078d9ad7097f2'
          https://monitor.ioer.de/monitor_api/sora/services
    

    rechnet der Service noch wird z.B. folgende JSON als Response geliefert:

     {
         "jobId": "jc7e4847561014440a06cd6e4cf2e7fb2",
         "jobStatus": "esriJobExecuting",
         "messages": [
         {
         "type": "esriJobMessageTypeInformative",
         "description": "Submitted."
         },
         {
         "type": "esriJobMessageTypeInformative",
         "description": "Executing..."
         }
         ]
     }
    

    ist der Service fertig wird z.B. folgender Response geliefert:

         {
     "paramName": "outputJSON",
     "dataType": "GPString",
     "value": [
     {
     "y":  "54.1015602797111",
         "x":  "12.067024122630599",
         "values":  [
         {
         "indicator":  "S12RG",
         "indicator_value":  "100.0",
         "time":  "2017"
         }
     ],
         "id":  "66"
     },
     {
     "y":  "54.1154796784391",
         "x":  "12.141539756251",
         "values":  [
         {
         "indicator":  "S12RG",
         "indicator_value":  "100.0",
         "time":  "2017"
         }
     ],
         "id":  "99"
     }
     ]
     }    
    

Die folgenden Dienste berechnen mit Hilfe des Open Route Service, die Wegstrecken zwischen den übergebenen Koordinaten und der Anforderung. Hierbei ist es möglich durch die Variierung der Parameter, die Berechnung zu beeinflussen.

2. Routing zum nächstgelegenen POI

Beschreibung

Dieser Service berechnet auf der Grundlage des übergebenen Punktes, die Distanz zum nächst gelegenen POI.

Endpoint: https://monitor.ioer.de/monitor_api/sora/services

  1. Post der JSON-Datei:
    • Query String: job=routing_poi
    • Datei: folgend eine beispielhafte JSON Datei, welche alle notwendigen Parameter übermittelt
     {"coordinates":
         [{"id":5297,"x":"4339535,6687453","y":"2765964,35707297"},
         {"id":4453,"x":"4339873,89116431","y":"2764805,05240828"},
         {"id":4989,"x":"4339655,35336738","y":"2765570,54764701"},
         {"id":7789,"x":"4339816,61177691","y":"2764105,56506073"}
         ......
         ],
     "options":[{"profile":"walking","epsg":{"input":"3035","output":"25833"}}
     ]}
    
    • Optionen innerhalb der JSON:
      • options:
        • profile: gibt an, wie der gefundene POI erreicht werden soll. Unterstützt werden im Moment: walking, driving
        • epsg: gibt an, in welchem Koordinatensystem die Punkte sich befinden und können optional auch in eine anderes Koordinatensystem transformiert werden. Hierfür muss der optionale Parameter output angegeben werden.
        • poi: gibt an, welcher nächst gelegene POI berechnet werden soll. Aktuell werden Grünflächen als green_areas und der Öffentliche Nahverkehr als public_transport.

    Beispiel mit curl:

     curl --header "Content-Type: application/json" \
             --request POST \
             --data '{"coordinates":[{"id":5297,"x":"4339535,6687453","y":"2765964,35707297"}],"options":[{"profile":"walking","epsg":{"input":"3035"},"poi":"public_transport"}]}' \
             https://monitor.ioer.de/monitor_api/sora/services?job=routing_poi
    

    Als Ergebnis wird dann folgende JSON mit einer Job-ID geliefert:

     {
     "jobId": "j19e57f40c4a44755a32078d9ad7097f2",
     "jobStatus": "esriJobSubmitted"
     }
    
  2. GET des Ergebnisses:
    • Wie im Service coodinates, nur jetzt als job routing_poi angegeben

    Beispiel mit curl:

     curl --request GET \
         -d 'job=routing_poi'
         -d 'job_id=j19e57f40c4a44755a32078d9ad7097f2'
         https://monitor.ioer.de/monitor_api/sora/services
    

    rechnet der Service noch wird z.B. folgende JSON als Response geliefert:

     {
         "jobId": "jc7e4847561014440a06cd6e4cf2e7fb2",
         "jobStatus": "esriJobExecuting",
         "messages": [
         {
         "type": "esriJobMessageTypeInformative",
         "description": "Submitted."
         },
         {
         "type": "esriJobMessageTypeInformative",
         "description": "Executing..."
         }
         ]
     }
    

    ist der Service fertig wird z.B. folgender Response geliefert:

     {
         "paramName": "outputJSON",
         "dataType": "GPString",
         "value": [
             {
                 "x": "4339535,6687453",
                 "y": "2765964,35707297",
                 "id": "5297",
                 "values": [
                     {
                         "endpoint": {
                             "x": "4339346.90443",
                             "y": "2766960.30919"
                         },
                         "distance_open_route": {
                             "value": "1559.4",
                             "unit": "m"
                         },
                         "duration_open_route": {
                             "value": "1122.8",
                             "unit": "s"
                         }
                     }
                 ]
             }
         ]
     }
    

Die Ergebnis-JSON beinhaltet die Koordinaten der nächstgelegenen Grünfläche und die Distanz zu dieser in Meter. Auf der Grundlage des definierten Profiles wird auch die benötigte Zeit angegeben.

3. Routing zwischen zwei Koordinaten

Beschreibung

Ähnlich des Services Suche nach den räumlich nächst gelegenen POI berechnet dieser Service die Route und die Distanz zwischen zwei Punkten. Hier werden jedoch der Anfangs- und der Endpunkt übergeben, wodurch die Suche nach dem nächst gelegenen POI entfällt.

Endpoint: https://monitor.ioer.de/monitor_api/sora/services

  1. Post der JSON-Datei:
    • Query String: job=routing_xy
    • Datei: folgend eine beispielhafte JSON Datei, welche alle notwendigen Parameter übermittelt
         {"coordinates":
             [{"id":5297,
                 "startpoint":
                      {"x":"4583809.29","y":"3108954.51"},"endpoint":{"x":"4584822.66","y":"3109658.40"}
             },
             {"id":5298,
                 "startpoint":
                     {"x":"4580902.38","y":"3110862.75"},"endpoint":{"x":"4583809.29","y":"3108954.51"}
             }],
         "options":[{"profile":"walking","epsg":{"input":"3035","output":"25833"}}]}
    
    • Die Optionen innerhalb der JSON sind gleich des Servies routing_poi
  2. GET des Ergebnisses:
    • Wie im Service coodinates, nur jetzt als job routing_xy angegeben

RDF

Das Resource Description Framework (RDF, engl. sinngemäß „System zur Beschreibung von Ressourcen“) bezeichnet eine technische Herangehensweise im Internet zur Formulierung logischer Aussagen über beliebige Dinge (Ressourcen). Ursprünglich wurde RDF vom World Wide Web Consortium (W3C) als Standard zur Beschreibung von Metadaten konzipiert. Mittlerweile gilt RDF als ein grundlegender Baustein des Semantischen Webs. RDF ähnelt den klassischen Methoden zur Modellierung von Konzepten wie UML-Klassendiagramme und Entity-Relationship-Modell. Im RDF-Modell besteht jede Aussage aus den drei Einheiten Subjekt, Prädikat und Objekt, wobei eine Ressource als Subjekt mit einer anderen Ressource oder einem Wert (Literal) als Objekt näher beschrieben wird. Quelle

Mit der Beschreibung der Indikatoren und deren Kategorien in RDF, wurde der Beschluss innerhlab der SoRa Projektmitglieder umgesetzt, eine Beschreibung der Metadaten in RDF durchzuführen. Zur Arbeit mit diesem Format wurde die Python-Bibliothek rdflib eingesetzt.

Indikator

RDF-Abfrage Code

Diese Klasse fragt bei der Instaziierung vom Monitor-Backend alle verfügbaren Indikatoren ab, welche im Raster-Format vorliegen. Iterativ werden diese Indikatoren und deren Metadaten dem Graphen hinzugefügt.

Category

RDF-Abfrage Code

Ähnlich wie die Indikatoren fragt diese Klasse bei der Instaziierung vom Monitor-Backend alle verfügbaren Kategorien ab, welche für das Raster-Format vorliegen. Iterativ werden diese Kategorien und deren Metadaten dem Graphen hinzugefügt.

Flask

Github

Dieser Microservice wird für die Kommunikation zwischen Clienten und den WPS eingesetzt, um die Anfragen so leicht wie möglich zu gestalten. Eine Dokumentation zu Flask ist unter dem folgenden Link zu finden. Das nochfolgende UML bildet die verwnedeten Klassen ab, welche nachfolgend genauer Dokumentiert werden.

routes.py

Hier erfolgt des Request-Mapping, indem genau defineirt wird, was bei welchen query passiert. Im nachfolgenden Code-Block ist der Quellcode abgebildet.

#RDF-Schnittstelle um sich die Indikatoren als .ttl auszugeben
@sora.route("/indicator", methods=['GET', 'POST'])
def get_indicators():
    indicator = Indicator(json_url=url)
    try:
        res = indicator.g
    except Exception as e:
        return abort(500)
    if len(res) == 0:
        return abort(404)
    else:
        return Response(res.serialize(format="turtle"), mimetype="text/n3")

#RDF-Schnittstelle um sich die Kategorien als .ttl auszugeben
@sora.route("/category", methods=['GET', 'POST'])
def get_categories():
    category = Category(json_url=url)
    try:
        res = category.g
    except Exception as e:
        return abort(500)
    if len(res) == 0:
        return abort(404)
    else:
        return Response(res.serialize(format="turtle"), mimetype="text/n3")

#Ausgabe der RDF-Ontologie des IÖR
@sora.route("/ontology", methods=['GET', 'POST'])
def get_ontology():
    dir = os.getcwd()
    graph = Graph().parse("{}/app/sora/data/ontology.ttl".format(dir), format="turtle")
    response = Response(graph.serialize(format="turtle"), mimetype='text/n3')
    response.headers['Access-Control-Allow-Origin'] = '*'
    return response

#Schnittstelle um die Esri-Geoprocessing Dienste zu nutzen
@sora.route('/services', methods=['GET', 'POST'])
def get():
    job = request.args.get('job') or None
    values = request.get_data() or None
    job_id = request.args.get('job_id') or None
    # test if JSON is valid
    try:
        # validate json
        # set request and get response from esri server
        request_handler = ESRIServerManager(job, values=values, job_id=job_id)
        app.logger.debug("result: \n%s", str(request_handler.get_request()))
        return request_handler.get_request()
    except Exception as e:
        if job == None:
            return jsonify(error='no job query, API-Doku: https://ioer-dresden.github.io/monitor-api-doku/docs/sora')
        else:
            return InvalidUsage(e,status_code=410)

# Error handling
@sora.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

Da die Code Kommentare schon sehr gut wiedergeben was funktionale Betsandteile sindb g, soll nicht einzeln beschrieben werden was die instanziierten Klassen tun, dies ist nachfolgende dokumentiert.

EsriServerManager

Code

Diese Klasse hat die Aufgabe anhand der übergebenen Job-ID den entsprechenden Service auf dem Esri-Server aufzurufen und das Ergebnis an den Clienten zu streamen. Treten dabei Fehler (Exceptions) auf oder ist die Anfrage durch den Clienten falsch formuliert, wird eine entsprechende Meldung als Response gesendet. Somit ist es dem Clienten möglich darauf zu reagieren.

Esri-Server

Github

Um arcpy auf dem Server einzubinden, ist man auf einen ESRI-Server angewiesen. Alle Geo-Prozessing Services wurden in OOP-Python geschrieben und mit ArcMap getestet. Die Veröffentlichung eines Geo-Processing Services erfolgte nach der Offiziellen Anleitung.

Abfrage von Indikatorwerten zu Koordinaten

Rest-API

Mit diesem Service können für vorgegebene Koordinaten die jeweiligen Indikatorwerte abgefragt werden. Wird ein Bufferwert gesetzt, ermittelt der Service den Durchschnitt des Indikators innhalb des Buffer-Bereichs. Hierbei wird ein quadratische Buffer um jede Koordinate gesetzt, dessen Räumlicher Durchschnitt über den Paramter buffer gesetzt werden kann (siehe Paramter JSON). Aus diesem Buffer wird durch eine Umgebungsanalyse alle enthaltenen Pixelwerte ermittelt und deren Durchschnittliches Ergebnis an den Response für jede Koordinate angehangen. Nachfolgend ist für den Service das UML abgebildet:

Anhand der Main-Methode wird ersichtlich, wie die vom TaskRepository aufgerufenen Methoden als Workflow dienen um das angefragte Ergebnis zu generieren.

def main():
    # ENV Settings
    arcpy.env.overwriteOutput = 1
    # how to round the values
    round = 2
    # the keys
    indicators = "indicators"
    indicator_id = "id"
    year_id = "year"
    buffer_id = "buffer"
    # user input
    input = json.loads(arcpy.GetParameterAsText(0))
    task = Result(input,round)
    for i in input[indicators]:
        # settings
        indicator = i[indicator_id]
        year = i[year_id]
        if i.has_key("buffer"):
            buffer = int(i[buffer_id])
        else:
            buffer = None
        # create the result
        task.getImagePath(indicator, year)
        task.extractInput()
        task.createPixelValues(buffer=buffer)


    arcpy.SetParameterAsText(1,json.dumps(task.extractJSON()))

Im ersten Schritt wird die in der Beschreibung der Rest-Schnittstelle doukumentierte JSON geparst und alle notwendigen Informationen aus dieser entnommen. Dann wird das entspreche GeoTIFF herausgesucht und der Pixelwert für jede Koordinate ermittelt. Ist ein Buffer gesetzt, wird für den gegebenen Indikator der entsprechende Wert ermittelt.

Routing zwischen zwei Koordinaten

Rest-API

Mit diesem Srvice kann die Lauf/Fahrdistanz zwischen zwei Koordinaten berechnet werden. Für die Berechnung der Distanz wird der Routing-Dienst Open Route Service eingesetzt, welcher von der Uni Heidelberg entwickelt wurde. Dieser Routing Dienst wird auf dem monitor.ioer.de Server lokal gehostet.

Im ersten Schritt werdebn aus dem JSON-Request die notwendigen Paramter geparst. Dann müssen abweichende Koordinaten (der Open Route Service kann nur mit dem Koordinatensystem EPSG:4326 arbeiten) transformiert werden. Anschließend kann die Distanz bestimmt und ein Response gesendet werden.

Routing zum nächstgelegenen POI

Rest-API

Ähnlich wie bei dem Service zur Bestimmung der Distanz zwischen zwei Kooridnaten, nutzt auch dieser WPS den Open Route Service um die Distanz zu berechnen. Jedoch werden in dem dokumentierten Dienst durch eine topographische-Umfeldanalyse die nähesten POI bestimmt und deren Distanz. Hierfür wurde eine Datenbank erstellt, welche zum jetzigen Zeitpunkt alle öffentlich zugänglichen Grünflächen und Haltestellten für den öffentlichen Nahverkehr in ganz Deutschland beinhalten. Auf dieser Grundlage kann nach der transformation der Koordnaten in das EPSG:4236 (falls nötig) für alle Koordinaten der näheste POI bestimmt werden. Dies ist mit der arcpy Funktion nearest Table erfolgt.

Probleme

Bei allen Diensten verusacht die Koordinatentransformation eine starke Verlangsamung des Rechenprozesses, was an arcpy liegt. Hierfür muss eine bessere Funktionalität gefunden werden. Leider ist man bei den ESRI-Servern auf arcpy angeweisen. Anbei der verantwortliche Code:

def transformPoint(self,x, y, epsg_In, epsg_OUT):
    point = arcpy.PointGeometry(arcpy.Point(x, y), arcpy.SpatialReference(epsg_In)).projectAs(
        arcpy.SpatialReference(epsg_OUT))
    return point.centroid