Thursday, 17 January 2019

Is there an easy way to use Postgis GeoJSON in Openlayers 3


I was using google-vector-layers and leaflet-vector-layers from Jason Sanford (https://github.com/JasonSanford) to easily display, style and add customized popups for data from Postgis-databases. This works in combination with a modified version of the PHP-Database-GeoJSON from Bryan McBride.



Is there nothing comparable to use with Openlayers 3 out there? I must admit, that I don't have the programming skills to write such a library. Might be that someone knows about a comparable code. After doing a lot of Googling around, I found a lot of answers to particular problems and managed to implement things on how to load GeoJSON using Ajax and boundingBox strategy, styling the vector layers and to add popups, but I'm still missing an easy way on how to put all these things together.


I'm wondering if there is maybe an existing solution I didn't find till today, since I think PostGis > GeoJSON > Openlayers 3 (including styling and displaying features via popup) should be a quite standardized way?



Answer



Since there where no answers to my questions, I tried to get inspired by other already existing code, and went on develloping a basic GeoJSON-constructor in openlayers 3, that fullfills my needs.


I actually can



  • load different layers from PostGIS by passing a table-name, table-fields and a WHERE clause (with all the possible magic of PosgreSQL/PostGis)

  • style them (using single, unique or range values)

  • show/hide layers at min/maxResolution

  • unique identifier needs to be id to prevent that features are loaded multiple times


  • layers are loaded via the ol.loadingstrategy.bbox strategy so only visible data is loaded

  • define layer specific popup-templates

  • add layer specific labelling


So it's already a funny package, that can be adapted (and improved) very easily. Therefore I want to share the code here. The whole consists of three parts:



  • ol3Vector.js an extended class of ol.layer.Vector

  • a map.js-file in which the map and all the layers are defined

  • get_geojson.php file that is used serverside to build the sql-string and to return valid GeoJSON.



In order to work correct the ol3Vector.js needs to be loaded first. Here is the 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);

here an example of the 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: "
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: "

BH {sitecode}


{sitename}
{surfha} ha

",
showLabels: true, // show labels on map
label: "sitename" // field used for labeling
});

var n2k_do_l = new ol3Vector({
title: "Natura 2000 Birds Directive",
attribution: "
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: "

BD {sitecode}


{sitename}
{surfha} ha

",
showLabels: true,
label: "sitecode"
});

var communes = new ol3Vector({
map: map,
title: "Communes",

attribution: "
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: "

{commune}


{stat}
{surfha} ha

",
showLabels: true,
label: "commune"
});


var n2k_dh_r = new ol3Vector({
map: map,
title: "Natura 2000 Habitats Directive - site size",
attribution: "
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: "

BH {sitecode}


{sitename}
{surfha} ha

",
showLabels: true,
label: "sitecode"
});


var btk_p = new ol3Vector({
map: map,
title: "Habitats points", // point layers
attribution: "
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: "

{btyp1_code}


{btyp1_name}
{cs}

",
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);

Finally we need the get_geojson.php to fetch data from PostGis-database.


/**
* 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: geotable";
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);
?>

Have fun! May this help someone to go further or improve this code and my be publish it on GitHub.


No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...