Gibt es eine einfache Möglichkeit, Postgis GeoJSON in Openlayers 3 zu verwenden?

8

Ich habe Google-Vector-Layer und Leaflet-Vector-Layer von Jason Sanford ( https://github.com/JasonSanford ) verwendet, um einfach angepasste Popups für Daten aus Postgis-Datenbanken anzuzeigen, zu formatieren und hinzuzufügen. Dies funktioniert in Kombination mit einer modifizierten Version der PHP-Database-GeoJSON von Bryan McBride.

Gibt es nichts Vergleichbares für Openlayers 3? Ich muss zugeben, dass ich nicht die Programmierkenntnisse habe, um eine solche Bibliothek zu schreiben. Könnte sein, dass jemand einen vergleichbaren Code kennt. Nachdem ich viel gegoogelt hatte, fand ich viele Antworten auf bestimmte Probleme und schaffte es, Dinge zum Laden von GeoJSON mithilfe von Ajax und der BoundingBox-Strategie zu implementieren, die Vektorebenen zu gestalten und Popups hinzuzufügen, aber mir fehlt immer noch eine einfache Weg, wie man all diese Dinge zusammenfügt.

Ich frage mich, ob es vielleicht eine Lösung gibt, die ich bis heute nicht gefunden habe, da ich denke, dass PostGis> GeoJSON> Openlayers 3 (einschließlich Styling und Anzeige von Funktionen über Popup) eine ziemlich standardisierte Methode sein sollte.

geom
quelle
gis.stackexchange.com/questions/134688/… . Sie fügen die URL als Teil des src der Vektorebene hinzu (die Sie natürlich stylen können).
John Powell
Ich habe diesen Beitrag bereits studiert, aber wie ich bereits erwähnt habe, gibt es keine All-in-One-Lösung wie die Vektor-Layer-Skripte.
Geom
Vielleicht möchten Sie ein Beispiel hinzufügen, das zeigt, wie die Vektorebenenskripte funktionieren, da dies aus einem Link zu einer Github-Seite überhaupt nicht ersichtlich ist und was in den OL3-Beispielen in dieser Hinsicht fehlt.
John Powell

Antworten:

7

Da es dort keine Antworten auf meine Fragen gab, habe ich versucht, mich von anderen bereits vorhandenen Codes inspirieren zu lassen, und in Openlayers 3 einen grundlegenden GeoJSON-Konstruktor entwickelt, der meine Anforderungen erfüllt.

Ich kann es tatsächlich

  • Laden Sie verschiedene Ebenen aus PostGIS, indem Sie einen Tabellennamen, Tabellenfelder und eine WHERE-Klausel übergeben (mit der möglichen Magie von PosgreSQL / PostGis).
  • Stil sie (mit einzelnen, eindeutigen oder Bereichswerten)
  • Ebenen bei min / maxResolution ein- / ausblenden
  • Die eindeutige Kennung muss id sein, um zu verhindern, dass Features mehrmals geladen werden
  • Ebenen werden über die Strategie ol.loadingstrategy.bbox geladen, sodass nur sichtbare Daten geladen werden
  • Definieren Sie schichtenspezifische Popup-Vorlagen
  • schichtspezifische Beschriftung hinzufügen

Es ist also bereits ein lustiges Paket, das sehr einfach angepasst (und verbessert) werden kann. Deshalb möchte ich den Code hier teilen. Das Ganze besteht aus drei Teilen:

  • ol3Vector.js ist eine erweiterte Klasse von ol.layer.Vector
  • Eine map.js-Datei, in der die Map und alle Layer definiert sind
  • get_geojson.php-Datei, die serverseitig verwendet wird, um die SQL-Zeichenfolge zu erstellen und gültiges GeoJSON zurückzugeben.

Um korrekt zu arbeiten, muss zuerst die Datei ol3Vector.js geladen werden. Hier ist der Code:

 // class to load vector layers from Postgis using a modified php-script from
 // Bryan McBride
 // https://github.com/bmcbride/PHP-Database-GeoJSON 
 //
 ol3Vector = function(options) {

 //
 // -------- Defining default style settings ----------
 //
 var fill = new ol.style.Fill({
    color: 'rgba(255,255,255,0.4)'
 });
 var stroke = new ol.style.Stroke({
    color: '#3399CC',
    width: 1.25
 });
 var text = new ol.style.Text({
    text: "",
    font: "16px Calibri,sans-serif",
    fill: new ol.style.Fill({
        color: [255, 255, 255, 1]
    }),
    stroke: new ol.style.Stroke({
        color: [0, 0, 0, 1],
        width: 2.5
    })
});

var image = new ol.style.Circle({  // actually point styling only works with image, and not with icons
    fill: fill,
    stroke: stroke,
    radius: 5
});

var style = new ol.style.Style({
    image: image,
    fill: fill,
    stroke: stroke,
    text: text
});

var styles = [style];

//
// ------------- defining options to build the new ol3Vector-layer -----------
//
var options = {
    title: options.title,
    visible: false,
    geotable: options.geotable,  // table name in PostGis-database
    fields: options.fields,      // field-names
    where: options.where,        // where-string passed to PostGis
    source: new ol.source.Vector({
        projection: "EPSG:4326",
        attributions: [new ol.Attribution({
            html: options.attribution
        })],
        strategy: ol.loadingstrategy.bbox,   //load only data off the visible map
        loader: function(extent, resolution, projection) {
            var extent = ol.proj.transformExtent(extent, projection.getCode(), ol.proj.get('EPSG:4326').getCode());
            $.ajax({
                type: "GET",
                dataType: "json",
                url: "./mapdata/get_geojson.php?" +     // define path to the get_geojson.php script
                    "geotable=" + options.geotable +
                    "&fields=" + options.fields +
                    "&where=" + options.where +
                    "&bbox=" + extent.join(","),
                context: this
            }).done(function(data) {
                var format = new ol.format.GeoJSON();
                this.addFeatures(format.readFeatures(data, {
                    dataProjection: "EPSG:4326",
                    featureProjection: "EPSG:3857"
                }));

            });

        }
    }),
    minResolution: options.minResolution,
    maxResolution: options.maxResolution,
    content: options.content,
    symbology: options.symbology,
    showLabels: options.showLabels,
    label: options.label,
    style: (function label_style() { // style function needed to be wrapped in a function to get it work
        // ------ labeling -----------
        var layerLabel = "";
        if (!options.showLabels) {
            layerLabel = "";
        } else if (options.showLabels) {
            layerLabel = options.label;
        }
        return function(feature, resolution) {
            style.getText().setText(feature.get(layerLabel));

            // ------ styling -------------
            if (!options.symbology) {
                return style;
                //            return false;        
            } else if (options.symbology) {

                var atts = feature.getProperties();

                switch (options.symbology.type) {
                    case "single":
                        // Its a single symbology for all features so just set the values for fill and stroke
                        //
                        switch (feature.getGeometry().getType()) {
                            case "Point":
                            case "MultiPoint":

                                style.image = style.getImage().getFill().setColor(options.symbology.styleOptions.fill);
                                style.image = style.getImage().getStroke().setColor(options.symbology.styleOptions.color);
                                style.image = style.getImage().getStroke().setWidth(options.symbology.styleOptions.width);
                                style.image = style.getImage().setRadius(options.symbology.styleOptions.radius);
                                break;

                            case "LineString":
                            case "MultiLineString":
                            case "Polygon":
                            case "MultiPolygon":

                                style.fill = style.setFill(new ol.style.Fill({
                                    color: options.symbology.styleOptions.fill
                                }));
                                style.stroke = style.setStroke(new ol.style.Stroke({
                                    color: options.symbology.styleOptions.color,
                                    width: options.symbology.styleOptions.width
                                }));
                                break;
                        }
                        break;

                    case "unique":
                        // Its a unique symbology. Check if the features property value matches that in the symbology and style accordingly
                        // 
                        var att = options.symbology.property;
                        for (var i = 0, len = options.symbology.values.length; i < len; i++) { // field with values to define styles
                            if (atts[att] == options.symbology.values[i].value) { // unique value to identify style
                                for (var key in options.symbology.values[i].styleOptions) {

                                    switch (feature.getGeometry().getType()) {
                                        case "Point":
                                        case "MultiPoint":

                                            style.image = style.getImage().getFill().setColor(options.symbology.values[i].styleOptions.fill);
                                            style.image = style.getImage().getStroke().setColor(options.symbology.values[i].styleOptions.color);
                                            style.image = style.getImage().getStroke().setWidth(options.symbology.values[i].styleOptions.width);
                                            style.image = style.getImage().setRadius(options.symbology.values[i].styleOptions.radius);
                                            break;

                                        case "LineString":
                                        case "MultiLineString":
                                        case "Polygon":
                                        case "MultiPolygon":

                                            style.fill = style.setFill(new ol.style.Fill({
                                                color: options.symbology.values[i].styleOptions.fill
                                            }));
                                            style.stroke = style.setStroke(new ol.style.Stroke({
                                                color: options.symbology.values[i].styleOptions.color,
                                                width: options.symbology.values[i].styleOptions.width
                                            }));
                                            break;
                                    }
                                }
                            }
                        }
                        break;

                    case "range":
                        // Its a range symbology. Check if the features property value is in the range set in the symbology and style accordingly
                        //
                        var att = options.symbology.property;
                        for (var i = 0, len = options.symbology.ranges.length; i < len; i++) {
                            if (atts[att] >= options.symbology.ranges[i].range[0] && atts[att] <= options.symbology.ranges[i].range[1]) {
                                for (var key in options.symbology.ranges[i].styleOptions) {

                                    switch (feature.getGeometry().getType()) {
                                        case "Point":
                                        case "MultiPoint":

                                            style.image = style.getImage().getFill().setColor(options.symbology.ranges[i].styleOptions.fill);
                                            style.image = style.getImage().getStroke().setColor(options.symbology.ranges[i].styleOptions.color);
                                            style.image = style.getImage().getStroke().setWidth(options.symbology.ranges[i].styleOptions.width);
                                            style.image = style.getImage().setRadius(options.symbology.ranges[i].styleOptions.radius);
                                            break;

                                        case "LineString":
                                        case "MultiLineString":
                                        case "Polygon":
                                        case "MultiPolygon":

                                            style.fill = style.setFill(new ol.style.Fill({
                                                color: options.symbology.ranges[i].styleOptions.fill
                                            }));
                                            style.stroke = style.setStroke(new ol.style.Stroke({
                                                color: options.symbology.ranges[i].styleOptions.color,
                                                width: options.symbology.ranges[i].styleOptions.width
                                            }));
                                            break;
                                    }
                                }
                            }
                        }

                        break;
                }
            }
            return styles;
        }
    })()
}

ol.layer.Vector.call(this, options);

};

ol.inherits(ol3Vector, ol.layer.Vector);

hier ein Beispiel für die map.js

function init() {
document.removeEventListener('DOMContentLoaded', init);

//
// ------- Layers and Map ----------------
//
var baselayers = new ol.layer.Group({  
    title: 'Baselayers',
    layers: [
        new ol.layer.Tile({
            title: 'OSM',
            type: 'base',
            visible: true,
            source: new ol.source.OSM()
        })
    ]
});

var toplayers = new ol.layer.Group({
    title: 'Toplayer',
    layers: []
});


var view = new ol.View({
    center: ol.proj.fromLonLat([6.2, 49.6]),
    zoom: 12
});

var popup_div = document.getElementById('popup');
var popup = new ol.Overlay({
    element: popup_div,
    positioning: 'bottom-center',
    stopEvent: false
});

var map = new ol.Map({
    target: 'map',
    view: view,
    controls: ol.control.defaults().extend([
        new ol.control.Zoom(),
        new ol.control.FullScreen(),
        new ol.control.ZoomSlider(),
        new ol.control.LayerSwitcher({  //layergroups are used with the layerswitcher from https://github.com/walkermatt/ol3-layerswitcher
            tipLabel: 'Layer Switcher'
        }),
        new ol.control.OverviewMap(),
        new ol.control.ScaleLine()
    ]),
    overlays: [popup],
    layers: [baselayers, toplayers],
    interactions: ol.interaction.defaults().extend([
        new ol.interaction.Select({
            layers: [baselayers, toplayers]
        })
    ])
});

//
// ------------ define toplayers -------------------
//
var n2k_dh_l = new ol3Vector({
    title: "Natura 2000 Habitats Directive",   // name of the layer to show up in the layerswitcher
    attribution: "<br />Réseau Natura 2000 Habitats Directive",
    geotable: "n2k_dh",
    fields: "gid as id, sitecode, sitename, surfha",
    where: "sitename ilike '%moselle%'",    // You can use all the PostgreSQL or PostGis features here
    symbology: {
        type: "single",
        styleOptions: {
            fill: "rgba(100,250,0,0.1)",   // define colors as rgba()
            color: "green",                // or simple color-names
            width: 2
        }
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong> BH {sitecode}</strong><hr>{sitename}<br />{surfha} ha </p>",
    showLabels: true,    // show labels on map
    label: "sitename"    // field used for labeling
});

var n2k_do_l = new ol3Vector({
    title: "Natura 2000 Birds Directive",
    attribution: "<br />Réseau Natura 2000 Birds Directive",
    geotable: "n2k_do",
    fields: "gid as id, sitecode, sitename, surfha",
    where: "",
    symbology: {
        type: "single",
        styleOptions: {
            fill: "rgba(100,250,0,0.1)",
            color: "magenta",
            width: 2
        }
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong> BD {sitecode}</strong><hr>{sitename}<br />{surfha} ha </p>",
    showLabels: true,
    label: "sitecode"
});

var communes = new ol3Vector({
    map: map,
    title: "Communes",
    attribution: "<br />Communes",
    geotable: "communes",
    fields: "gid as id, surfha, commune, stats",
    where: "",
    symbology: {
        type: "unique",
        property: "stats",  // field used to style depending on their value
        values: [{
                value: "AB", 
                styleOptions: {
                    fill: "rgba(0,0,0,0.0)",
                    color: "grey",
                    width: 1.25
                }
            },
            {
                value: "CD",
                styleOptions: {
                    fill: "rgba(100,250,0,0.1)",
                    color: "green",
                    width: 2
                }
            },
            {
                value: "",
                styleOptions: {
                    fill: "rgba(250,0,0,0.1)",
                    color: "red",
                    width: 2
                }
            }
        ]
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong>{commune}</strong><hr>{stat}<br />{surfha} ha </p>",
    showLabels: true,   
    label: "commune"    
});

var n2k_dh_r = new ol3Vector({
    map: map,
    title: "Natura 2000 Habitats Directive - site size",
    attribution: "<br />Réseau Natura 2000 Habitats Directive",
    geotable: "n2k_dh",
    fields: "gid as id, sitecode, sitename, surfha",
    where: "",
    symbology: {
        type: "range",
        property: "surfha", // field that holds values that should be displayed as ranges
        ranges: [{
            range: [1, 100], // defining range min and max values of property field
            styleOptions: {
                fill: "rgba(220,20,60,0.3)",
                color: "crimson",
                width: 1
            }
        }, {
            range: [101, 1000],
            styleOptions: {
                fill: "rgba(255,165,0,0.3)",
                color: "orange",
                width: 1
            }
        }, {
            range: [1001, 10000],
            styleOptions: {
                fill: "rgba(255,255,0,0.3)",
                color: "Yellow",
                width: 1
            }
        }]
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong> BH {sitecode}</strong><hr>{sitename}<br />{surfha} ha </p>",
    showLabels: true,
    label: "sitecode"
});

var btk_p = new ol3Vector({
    map: map,
    title: "Habitats points",  // point layers
    attribution: "<br />Cartho",
    geotable: "btk_p",
    fields: "gid as id, btyp1_code, btyp1_name, cs",
    where: "",
    symbology: {
        type: "unique",
        property: "cs",
        values: [{
                value: "A", 
                styleOptions: {
                    fill: "rgba(0,128,0,0.3)",
                    color: "rgba(0,128,0,0.3)",
                    width: 1,
                    radius: 20 // must be set in order to render point features    
                }
            },
            {
                value: "B",
                styleOptions: {
                    fill: "rgba(255,165,0,0.3)",
                    color: "rgba(255,165,0,0.3)",
                    width: 1,
                    radius: 15
                }
            },
            {
                value: "C",
                styleOptions: {
                    fill: "rgba(255,0,0,0.3)",
                    color: "rgba(255,0,0,0.3)",
                    width: 1,
                    radius: 10
                }
            }
        ]
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong>{btyp1_code}</strong><hr>{btyp1_name}<br />{cs}</p>",
    showLabels: false,
    label: "bewertung1"
});

// ------------------ add ol3Vectors to toplayers ----------
// 
toplayers.setLayers(new ol.Collection([n2k_do_l, n2k_dh_l, communes, n2k_dh_r, btk_p]));
//
// ------------------ show popups based on content-template for different layers --------------------
//  
map.on('click', function(evt) {
    var feature = map.forEachFeatureAtPixel(evt.pixel,
        function(feature, layer) {
            return feature;
        });
    var popupContent = map.forEachLayerAtPixel(evt.pixel,
        function(layer) {
            return layer.get('content');
        });
    if (feature) {
        popup.setPosition(evt.coordinate);
        var atts = feature.getProperties();
        for (var prop in atts) {
            var re = new RegExp("{" + prop + "}", "g");
            popupContent = popupContent.replace(re, atts[prop]);
        }
        $(popup_div).attr('data-placement', 'auto');
        $(popup_div).attr('data-content', popupContent);
        $(popup_div).attr('data-html', true);
        $(popup_div).popover();
        $(popup_div).popover('show');
        $('.popover-title').click(function() {
            $(popup_div).popover('destroy');
        });
    } else {
        $(popup_div).popover('destroy');
    }
});
} // End function init()
document.addEventListener('DOMContentLoaded', init);

Schließlich benötigen wir die Datei get_geojson.php, um Daten aus der PostGis-Datenbank abzurufen.

<?php
/**
 * GET GeoJSON from PostGIS
 * Query a PostGIS table or view and return the results in GeoJSON format, suitable for use in OpenLayers, Leaflet, etc.
 * Author:  Bryan R. McBride, GISP, adapted by G.Moes
 * Contact: bryanmcbride.com
 * GitHub:  https://github.com/bmcbride/PHP-Database-GeoJSON
 * 
 * @param       string      $geotable       The PostGIS layer name *REQUIRED*
 * @param       string      $geomfield      The PostGIS geometry field *REQUIRED*
 * @param       string      $srid           The SRID of the returned GeoJSON *OPTIONAL (If omitted, EPSG: 2169 will be used)*
 * @param       string      $fields         Fields to be returned *OPTIONAL (If omitted, all fields will be returned)* 
 *                              NOTE- Uppercase field names should be wrapped in double quotes
 * @param       string      $parameters     SQL WHERE clause parameters *OPTIONAL*
 * @param       string      $orderby        SQL ORDER BY constraint *OPTIONAL*
 * @param       string      $sort           SQL ORDER BY sort order (ASC or DESC) *OPTIONAL*
 * @param       string      $limit          Limit number of results returned *OPTIONAL*
 * @param       integer     $precision      digits of returned geojson 6 = 0.111 m submeter as DEFAULT *OPTIONAL*
 * @param       real        $simplify       simplify geometry to >5.0m as DEFAULT *OPTIONAL*  
 * @param       string      $offset         Offset used in conjunction with limit *OPTIONAL*
 * @return      string                  resulting geojson string
 */


# Connect to PostgreSQL database You need to pass here the credentials to connect to Your database
require("../database/connect.php");


function escapeJsonString($value) { # list from www.json.org: (\b backspace, \f formfeed)
  $escapers = array("\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c");
  $replacements = array("\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b");
  $result = str_replace($escapers, $replacements, $value);
  return $result;
}


# Retrive URL variables
if (empty($_GET['geotable'])) {
    echo "missing required parameter: <i>geotable</i>";
    exit;
} else
    $geotable = $_GET['geotable'];
if (empty($_GET['geomfield'])) {
    $geomfield='the_geom';
} else
    $geomfield = $_GET['geomfield'];
if (empty($_GET['srid'])) {
    $srid = 2169;    // changethis if You need another standard SRID
} else
    $srid = $_GET['srid'];
if (empty($_GET['fields'])) {
    $fields = '*';
} else
    $fields = $_GET['fields'];
$parameters = $_GET['where'];
$bbox = $_GET['bbox'];
if (empty($_GET['precision'])) {
    $precision = 6;    // change this to Your needs
} else
    $precision = $_GET['precision'];
if (empty($_GET['simplify'])) {
    $simplify = 5.0;   // change this to Your needs
} else
    $simplify = $_GET['simplify'];    
$orderby    = $_GET['orderby'];
if (empty($_GET['sort'])) {
    $sort = 'ASC';
} else
    $sort = $_GET['sort'];
$limit      = $_GET['limit'];
$offset     = $_GET['offset'];



# Build SQL SELECT statement and return the geometry as a GeoJSON element in EPSG: 4326
$sql = "SELECT " . pg_escape_string($fields) . ", st_asgeojson(st_transform(ST_SimplifyPreserveTopology(" . pg_escape_string($geomfield) . ",".$simplify."),4326),".$precision.") AS geojson FROM " . pg_escape_string($geotable);
if (strlen(trim($parameters)) > 0) {

      $sql .= " WHERE " . str_replace("''", "'", pg_escape_string($parameters));
}
if (strlen(trim($parameters)) > 0 AND strlen(trim($bbox)) > 0){
     $sql .= " AND the_geom &&  st_transform(st_makeenvelope(".pg_escape_string ($bbox).",4326),".$srid.")";
}
if (strlen(trim($parameters)) <= 0 AND strlen(trim($bbox)) > 0) {
      $sql .= " WHERE the_geom &&  st_transform(st_makeenvelope(".pg_escape_string ($bbox).",4326),".$srid.")";
}
if (strlen(trim($orderby)) > 0) {
    $sql .= " ORDER BY " . pg_escape_string($orderby) . " " . $sort;
}
if (strlen(trim($limit)) > 0) {
    $sql .= " LIMIT " . pg_escape_string($limit);
}
if (strlen(trim($offset)) > 0) {
    $sql .= " OFFSET " . pg_escape_string($offset);
}
//echo $sql;
# Try query or error
$rs = pg_query($db_handle, $sql);
if (!$rs) {
    echo "An SQL error occured.\n";
    echo $sql;
    exit;
}
# Build GeoJSON
$output    = '';
$rowOutput = '';
while ($row = pg_fetch_assoc($rs)) {
    $rowOutput = (strlen($rowOutput) > 0 ? ',' : '') . '{"type": "Feature", "geometry": ' . $row['geojson'] . ', "properties": {';
    $props = '';
    $id    = '';
    foreach ($row as $key => $val) {
        if ($key != "geojson") {
            $props .= (strlen($props) > 0 ? ',' : '') . '"' . $key . '":"' . escapeJsonString($val) . '"';
        }
        if ($key == "id") {
            $id .= ',"id":"' . escapeJsonString($val) . '"';
        }
    }

    $rowOutput .= $props . '}';
    $rowOutput .= $id;
    $rowOutput .= '}';
    $output .= $rowOutput;
}
$output = '{"type": "FeatureCollection", "features": [ ' . $output . ' ]}';

header('Content-type:application/json;charset=utf-8');
echo json_encode( $output);
?>

Habe Spaß! Möge dies jemandem helfen, weiter zu gehen oder diesen Code zu verbessern und ihn auf GitHub zu veröffentlichen.

geom
quelle
Gibt es eine POSTGIS-Funktion, um die Leistung erheblich zu verbessern? Ich spreche von 50.000 Features auf einmal
Revo
Vielleicht: new ol.source.ImageVector ()?
Revo
1
Diese Skripte (JS und PHP) sollten nicht wie hier gezeigt verwendet werden, da sie die Tür zur Injektion
JGH
Tatsächlich werden diese JS- und PHP-Skripte nur intern aufgerufen, um Layer in eine Map zu laden. Die PHP-Skripte werden nur ausgeführt, wenn man angemeldet ist. Aber Sie haben Recht. Jeder, der angemeldet ist, kann die GET-Zeichenfolge leicht manipulieren, um schädliches SQL auszuführen. Ich werde den Link studieren und sehen, wie ich eine Injektion verhindern kann. Vielen Dank, dass Sie sich den Code angesehen haben.
Geom
1
Sie möchten das Rad neu erfinden, warum nicht einfach WFS verwenden?
nmtoken