¿Hay una manera fácil de usar Postgis GeoJSON en Openlayers 3


8

Estaba usando google-vector-layers y leaflet-vector-layers de Jason Sanford ( https://github.com/JasonSanford ) para mostrar, diseñar y agregar ventanas emergentes personalizadas para los datos de las bases de datos Postgis. Esto funciona en combinación con una versión modificada de PHP-Database-GeoJSON de Bryan McBride.

¿No hay nada comparable para usar con Openlayers 3? Debo admitir que no tengo las habilidades de programación para escribir tal biblioteca. Puede ser que alguien sepa sobre un código comparable. Después de buscar mucho en Google, encontré muchas respuestas a problemas particulares y logré implementar cosas sobre cómo cargar GeoJSON usando la estrategia Ajax y la división de límite, diseñar las capas vectoriales y agregar ventanas emergentes, pero aún me falta un Manera de cómo poner todas estas cosas juntas.

Me pregunto si tal vez haya una solución existente que no encontré hasta hoy, ya que creo que PostGis> GeoJSON> Openlayers 3 (incluido el estilo y la visualización de funciones a través de ventanas emergentes) debería ser una forma bastante estandarizada.


gis.stackexchange.com/questions/134688/… . Agrega la url como parte del src de la capa vectorial (que, por supuesto, puede diseñar).
John Powell el

Ya he estudiado esta publicación, pero como mencioné antes, no hay una solución todo en uno como las secuencias de comandos de capas vectoriales.
Geom

Es posible que desee agregar un ejemplo que muestre cómo funcionan las secuencias de comandos de capas vectoriales, ya que no es del todo obvio en un enlace a una página de github y lo que falta en los ejemplos de OL3 a ese respecto.
John Powell el

Respuestas:


7

Como no había respuestas a mis preguntas, traté de inspirarme en otro código ya existente, y seguí desarrollando un constructor GeoJSON básico en Openlayers 3, que llena mis necesidades.

En realidad puedo

  • cargar diferentes capas de PostGIS pasando un nombre de tabla, campos de tabla y una cláusula WHERE (con toda la magia posible de PosgreSQL / PostGis)
  • diseñarlos (usando valores únicos, únicos o de rango)
  • mostrar / ocultar capas en min / maxResolution
  • el identificador único debe ser id para evitar que las funciones se carguen varias veces
  • las capas se cargan mediante la estrategia ol.loadingstrategy.bbox, por lo que solo se cargan datos visibles
  • definir plantillas emergentes específicas de capa
  • agregar etiquetado específico de capa

Por lo tanto, ya es un paquete divertido, que se puede adaptar (y mejorar) muy fácilmente. Por lo tanto, quiero compartir el código aquí. El conjunto consta de tres partes:

  • ol3Vector.js una clase extendida de ol.layer.Vector
  • un archivo map.js en el que se definen el mapa y todas las capas
  • El archivo get_geojson.php que se usa en el servidor para construir la cadena sql y devolver GeoJSON válido.

Para que funcione correctamente, ol3Vector.js debe cargarse primero. Aquí está el código:

 // 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);

aquí un ejemplo de 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);

Finalmente, necesitamos get_geojson.php para obtener datos de la base de datos PostGis.

<?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);
?>

¡Que te diviertas! Que esto ayude a alguien a ir más allá o mejorar este código y publicarlo en GitHub.


¿Existe alguna función POSTGIS para mejorar en gran medida el rendimiento? Estoy hablando de 50k ish características a la vez
Revo

Tal vez: nuevo ol.source.ImageVector ()?
Revo

1
Estos scripts (JS y PHP) no deben usarse como se muestra aquí, ya que abren la puerta a la inyección
JGH

En realidad, estos scripts JS y PHP solo se llaman internamente para cargar capas en un mapa. Los scripts php solo se ejecutan si hay uno conectado. Pero tiene razón. Cualquiera que haya iniciado sesión podría manipular fácilmente la cadena GET para ejecutar SQL malicioso. Estudiaré el enlace y veré cómo puedo prevenir la inyección. Gracias por mirar el código.
geom

1
Se siente un poco como reinventar la rueda, ¿por qué no solo usar WFS?
nmtoken
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.