Skip to content

Instantly share code, notes, and snippets.

@jsanz
Last active Dec 15, 2021
Embed
What would you like to do?
FOSS4G 2021: pg_tileserv and pg_featureserv workshop

Building a cartographic web application with pg_tileserv and pg_featureserv

  • Workshop materials.
  • Reused the postgis Docker Compose template from previous workshop
  • Reused also the osgeo/gdal container to use ogr2ogr on the downloaded shapefiles.
  • Added pg_featureserv and pg_tileserv Docker images passing the .toml configurations as part of the command setting.
  • Added an nginx service to the compose file for the HTML files.
  • Played with different HTML pages that load tiles from pg_tileserv and displays them using OpenLayers.
  • Created a view that returns random points.
version: '3.7'
services:
db:
image: postgis/postgis:13-master
volumes:
- pgdata:/var/lib/postgresql/data:z
networks:
- foss4g
environment:
- POSTGRES_PASSWORD=foss4g
- POSTGRES_HOST_AUTH_METHOD=trust
- PGDATA=/var/lib/postgresql/data/pgdata
ports:
- 5432:5432
gdal:
image: osgeo/gdal:latest
volumes:
- ../version/data:/tmp/data
networks:
- foss4g
command: bash
tileserv:
image: pramsey/pg_tileserv
command: "--config /app/config.toml"
ports:
- 7800:7800
volumes:
- ./tileserv_config.toml:/app/config.toml:ro
networks:
- foss4g
depends_on:
- db
featureserv:
image: pramsey/pg_featureserv
command: "--config /app/config.toml"
ports:
- 9000:9000
volumes:
- ./featureserv_config.toml:/app/config.toml:ro
networks:
- foss4g
depends_on:
- db
web:
image: nginx
ports:
- 8000:80
volumes:
- ./index_1.html:/usr/share/nginx/html/index_1.html
- ./index_2.html:/usr/share/nginx/html/index_2.html
- ./index_3.html:/usr/share/nginx/html/index_3.html
- ./index_4.html:/usr/share/nginx/html/index_4.html
- ./index_4_2.html:/usr/share/nginx/html/index_4_2.html
networks:
- foss4g
depends_on:
- tileserv
- featureserv
networks:
foss4g:
name: foss4g
volumes:
pgdata:
[Server]
# Accept connections on this subnet (default accepts on all)
HttpHost = "0.0.0.0"
# IP ports to listen on
HttpPort = 9000
# String to return for Access-Control-Allow-Origin header
CORSOrigins = "*"
# set Debug to true to run in debug mode (can also be set on cmd-line)
Debug = true
[Database]
# Database connection
# postgresql://username:password@host/dbname
# DATABASE_URL environment variable takes precendence if set.
DbConnection = "postgresql://postgres:foss4g@db/cadastre"
[Paging]
# The default number of features in a response
LimitDefault = 20
# Maxium number of features in a response
LimitMax = 10000
[Metadata]
# Title for this service
Title = "FOSS4G pg-featureserv"
# Description of this service
Description = "FOSS4G Feature Server"
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenLayers Vector Tiles</title>
<!-- CSS/JS for OpenLayers map -->
<link rel="stylesheet" href="https://openlayers.org/en/v6.1.1/css/ol.css" type="text/css">
<script src="https://openlayers.org/en/v6.1.1/build/ol.js"></script>
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100%;
font-family: sans-serif;
}
#meta {
background-color: rgba(255,255,255,0.75);
color: black;
z-index: 2;
position: absolute;
top: 10px;
left: 20px;
padding: 10px 20px;
margin: 0;
}
</style>
</head>
<body>
<div id="meta">
<h2>Our cartographic application !</h2>
<ul>
<li><a href="http://localhost:7800/"> pg_tileserv Server</a></li>
</ul>
</div>
<div id="map"></div>
<script>
var vectorServer = "http://localhost:7800/";
var vectorSourceLayer = "public.parcelles";
var vectorProps = "?properties=ogc_fid,id,commune,prefixe,section,numero,contenance,created,updated"
var vectorUrl = vectorServer + vectorSourceLayer + "/{z}/{x}/{y}.pbf" + vectorProps;
var vectorStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
width: 2,
color: "#ff00ff99"
}),
fill: new ol.style.Fill({
color: "#ff00ff33"
})
});
var vectorLayer = new ol.layer.VectorTile({
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: vectorUrl
}),
style: vectorStyle
});
var baseLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: "http://osm.oslandia.io/styles/osm-bright/{z}/{x}/{y}.png"
})
});
var map = new ol.Map({
target: 'map',
view: new ol.View({
center: [260369,6249082],
zoom: 16
}),
layers: [baseLayer, vectorLayer]
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenLayers Vector Tiles</title>
<!-- CSS/JS for OpenLayers map -->
<link rel="stylesheet" href="https://openlayers.org/en/v6.1.1/css/ol.css" type="text/css">
<script src="https://openlayers.org/en/v6.1.1/build/ol.js"></script>
<script src="https://unpkg.com/ol-layerswitcher@3.7.0"></script>
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100%;
font-family: sans-serif;
}
#meta {
background-color: rgba(255,255,255,0.75);
color: black;
z-index: 2;
position: absolute;
top: 10px;
left: 20px;
padding: 10px 20px;
margin: 0;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/ol-layerswitcher@3.7.0/src/ol-layerswitcher.css" />
</head>
<body>
<div id="meta">
<h2>Our cartographic application !</h2>
<ul>
<li><a href="https://localhost:7800">pg_tileserv server</a></li>
</ul>
</div>
<div id="map"></div>
<script>
var vectorServer = "http://localhost:7800/";
var vectorSourceLayer = "public.parcelles";
var vectorProps = "?properties=ogc_fid,id,commune,prefixe,section,numero,contenance,created,updated"
var vectorUrl = vectorServer + vectorSourceLayer + "/{z}/{x}/{y}.pbf" + vectorProps;
var vectorStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
width: 2,
color: "#ff00ff99"
}),
fill: new ol.style.Fill({
color: "#ff00ff33"
})
});
var vectorLayer = new ol.layer.VectorTile({
title:'Parcelles'
,source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: vectorUrl
}),
style: vectorStyle
});
var baseLayer = new ol.layer.Tile({
title: 'osm',
source: new ol.source.XYZ({
url: "http://osm.oslandia.io/styles/osm-bright/{z}/{x}/{y}.png"
})
});
var map = new ol.Map({
target: 'map',
view: new ol.View({
center: [260369,6249082],
zoom: 16
}),
layers: [baseLayer, vectorLayer]
});
var layerSwitcher = new ol.control.LayerSwitcher({
tipLabel: 'Legend', // Optional label for button
groupSelectStyle: 'children' // Can be 'children' [default], 'group' or 'none'
});
map.addControl(layerSwitcher);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenLayers Vector Tiles</title>
<!-- CSS/JS for OpenLayers map -->
<link
rel="stylesheet"
href="https://openlayers.org/en/v6.1.1/css/ol.css"
type="text/css"
/>
<script src="https://openlayers.org/en/v6.1.1/build/ol.js"></script>
<script src="https://unpkg.com/ol-layerswitcher@3.7.0"></script>
<script src="https://unpkg.com/ol-popup@4.0.0"></script>
<link
rel="stylesheet"
href="https://unpkg.com/ol-layerswitcher@3.7.0/src/ol-layerswitcher.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/ol-popup@4.0.0/src/ol-popup.css"
/>
<style>
body {
padding: 0;
margin: 0;
}
html,
body,
#map {
height: 100%;
width: 100%;
font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS",
sans-serif;
}
#meta {
background-color: rgba(255, 255, 255, 0.85);
color: #803536;
z-index: 2;
position: absolute;
bottom: 10px;
left: 20px;
padding: 10px 20px;
margin: 0;
}
.ol-popup-content {
min-width: 250px;
}
.my-popup {
color: #803536;
}
.my-popup ul {
padding-left: 0;
}
</style>
</head>
<body>
<div id="meta">
<h2>Our cartographic application !</h2>
<ul>
<li><a href="https://localhost:7800"> pg_tileserv server</a></li>
</ul>
</div>
<div id="map"></div>
<script>
function getPlainStyle(color) {
return new ol.style.Style({
stroke: new ol.style.Stroke({
width: 2,
color: color,
}),
fill: new ol.style.Fill({
color: color + "33",
}),
});
}
function getLayer(name, url, color, olProps) {
return new ol.layer.VectorTile({
title: name,
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: url,
}),
style: getPlainStyle(color),
...olProps
});
}
function generateLists(properties) {
var list = [];
for (prop in properties) {
list.push(`<li><strong>${prop}</strong>: ${properties[prop]}</li>`);
}
return list.join("");
}
const vectorServer = "http://localhost:7800/";
const vectorXYZ = "/{z}/{x}/{y}.pbf";
var baseLayer = new ol.layer.Tile({
title: "osm",
source: new ol.source.XYZ({
url: "http://osm.oslandia.io/styles/osm-bright/{z}/{x}/{y}.png",
}),
});
const dataLayers = [
{
name: "Communes",
table: "public.communes",
props: ["ogc_fid", "nom"],
color: "#0a7e80",
ol: {
visible: false
}
},
{
name: "Blocks",
table: "public.blocks",
props: [
"ogc_fid",
"id",
],
color: "#0a7e80",
ol: {
visible: true
}
},
{
name: "Parcelles",
table: "public.parcelles",
props: [
"ogc_fid",
"commune",
"prefixe",
"section",
"numero",
"contenance",
],
color: "#803536",
ol: {
visible: false
}
},
{
name: "Batiments",
table: "public.batiments",
props: ["ogc_fid", "commune", "nom", "type"],
color: "#310655",
ol: {
visible: false
}
},
];
var layers = [
baseLayer,
...dataLayers.map((layer) => {
return getLayer(
layer.name,
vectorServer +
layer.table +
vectorXYZ +
"?properties=" +
layer.props.join(","),
layer.color,
layer.ol
);
}),
];
var map = new ol.Map({
target: "map",
view: new ol.View({
center: [260010, 6251497],
extent: [238942,6236363,287345,6260517],
zoom: 17,
}),
layers,
});
var layerSwitcher = new ol.control.LayerSwitcher({
tipLabel: "Legend", // Optional label for button
groupSelectStyle: "children", // Can be 'children' [default], 'group' or 'none'
});
map.addControl(layerSwitcher);
var popup = new Popup();
map.addOverlay(popup);
map.on("singleclick", function (evt) {
var info_text = "";
var features = map.getFeaturesAtPixel(evt.pixel);
if (features.length != 0) {
var properties = features[0].getProperties();
info_text = "<ul>" + generateLists(properties) + "</ul>";
}
popup.show(
evt.coordinate,
'<div class="my-popup"><h3>Attributes</h3>' + info_text + "</div>"
);
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenLayers Vector Tiles</title>
<!-- CSS/JS for OpenLayers map -->
<link rel="stylesheet" href="https://openlayers.org/en/v6.1.1/css/ol.css" type="text/css">
<script src="https://openlayers.org/en/v6.1.1/build/ol.js"></script>
<script src="https://unpkg.com/ol-layerswitcher@3.7.0"></script>
<script src="https://unpkg.com/ol-popup@4.0.0"></script>
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100%;
font-family: sans-serif;
}
#meta {
background-color: rgba(255,255,255,0.75);
color: black;
z-index: 2;
position: absolute;
top: 10px;
left: 20px;
padding: 10px 20px;
margin: 0;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/ol-layerswitcher@3.7.0/src/ol-layerswitcher.css" />
<link rel="stylesheet" href="https://unpkg.com/ol-popup@4.0.0/src/ol-popup.css" />
</head>
<body>
<div id="meta">
<h2>Our cartographic application !</h2>
<ul>
<li><a href="https://localhost:7800">pg_tileserv server</a></li>
<li>
<form>
<label>Radius :</label>
<input type="number" id="radius" name="radius" min="10" max="2000" value="100" step="50">
</form>
</li>
</ul>
</div>
<div id="map"></div>
<script>
var vectorServer = "http://localhost:7800/";
var vectorSourceLayer = "public.parcelles";
var vectorProps = "?properties=ogc_fid,id,commune,prefixe,section,numero,contenance,created,updated"
var vectorUrl = vectorServer + vectorSourceLayer + "/{z}/{x}/{y}.pbf" + vectorProps;
var vectorStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
width: 2,
color: "#ff00ff99"
}),
fill: new ol.style.Fill({
color: "#ff00ff33"
})
});
var vectorStyleBlue = new ol.style.Style({
stroke: new ol.style.Stroke({
width: 1,
color: "#0000ff99"
}),
fill: new ol.style.Fill({
color: "#0000ff33"
})
});
var vectorLayer = new ol.layer.VectorTile({
title:'Parcelles'
,source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: vectorUrl
}),
style: vectorStyle
});
var parcelles_radius = new ol.layer.VectorTile({
title:'Parcelles radius'
,source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: vectorServer + "public.parcelles_radius" + "/{z}/{x}/{y}.pbf" + "?properties=id"
}),
style: vectorStyleBlue
});
var baseLayer = new ol.layer.Tile({
title: 'osm',
source: new ol.source.XYZ({
url: "http://osm.oslandia.io/styles/osm-bright/{z}/{x}/{y}.png"
})
});
var map = new ol.Map({
target: 'map',
view: new ol.View({
center: [260369,6249082],
zoom: 16
}),
layers: [baseLayer, vectorLayer, parcelles_radius]
});
var layerSwitcher = new ol.control.LayerSwitcher({
tipLabel: 'Legend', // Optional label for button
groupSelectStyle: 'children' // Can be 'children' [default], 'group' or 'none'
});
map.addControl(layerSwitcher);
var popup = new Popup();
map.addOverlay(popup);
map.on('singleclick', function(evt) {
var click = ol.proj.transform(evt.coordinate, 'EPSG:3857', 'EPSG:4326');
var radius = document.getElementById('radius').value;
var source = parcelles_radius.getSource();
source.setUrl(vectorServer
+ "public.parcelles_radius"
+ "/{z}/{x}/{y}.pbf"
+ "?properties=id"
+ "&click_lon=" + click[0]
+ "&click_lat=" + click[1]
+ "&radius=" + radius)
source.refresh({force: true});
});
</script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenLayers Vector Tiles</title>
<!-- CSS/JS for OpenLayers map -->
<link rel="stylesheet" href="https://openlayers.org/en/v6.1.1/css/ol.css" type="text/css">
<script src="https://openlayers.org/en/v6.1.1/build/ol.js"></script>
<script src="https://unpkg.com/ol-layerswitcher@3.7.0"></script>
<script src="https://unpkg.com/ol-popup@4.0.0"></script>
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100%;
font-family: sans-serif;
}
#meta {
background-color: rgba(255,255,255,0.75);
color: black;
z-index: 2;
position: absolute;
top: 10px;
left: 20px;
padding: 10px 20px;
margin: 0;
}
</style>
<link rel="stylesheet" href="https://unpkg.com/ol-layerswitcher@3.7.0/src/ol-layerswitcher.css" />
<link rel="stylesheet" href="https://unpkg.com/ol-popup@4.0.0/src/ol-popup.css" />
</head>
<body>
<div id="meta">
<h2>Our cartographic application !</h2>
<ul>
<li><a href="https://localhost:9000">pg_featureserv server</a></li>
<li>
<form>
<label>Radius :</label>
<input type="number" id="radius" name="radius" min="10" max="2000" value="100" step="50">
</form>
</li>
</ul>
</div>
<div id="map"></div>
<script>
var vectorServer = "http://localhost:7800/";
var featureServer = "http://localhost:9000/";
var vectorSourceLayer = "public.parcelles";
var vectorProps = "?properties=ogc_fid,id,commune,prefixe,section,numero,contenance,created,updated"
var vectorUrl = vectorServer + vectorSourceLayer + "/{z}/{x}/{y}.pbf" + vectorProps;
var vectorStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
width: 2,
color: "#ff00ff99"
}),
fill: new ol.style.Fill({
color: "#ff00ff33"
})
});
var vectorStyleBlue = new ol.style.Style({
stroke: new ol.style.Stroke({
width: 1,
color: "#0000ff99"
}),
fill: new ol.style.Fill({
color: "#0000ff33"
})
});
var vectorLayer = new ol.layer.VectorTile({
title:'Parcelles'
,source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: vectorUrl
}),
style: vectorStyle
});
var parcelles_radius = new ol.layer.Vector({
title:'Parcelles radius'
,source: new ol.source.Vector({
url: featureServer + "functions/parcelles_radius/items.json?limit=10000",
format: new ol.format.GeoJSON()
}),
style: vectorStyleBlue
});
var baseLayer = new ol.layer.Tile({
title: 'osm',
source: new ol.source.XYZ({
url: "http://osm.oslandia.io/styles/osm-bright/{z}/{x}/{y}.png"
})
});
var map = new ol.Map({
target: 'map',
view: new ol.View({
center: [260369,6249082],
zoom: 16
}),
layers: [baseLayer, vectorLayer, parcelles_radius]
});
var layerSwitcher = new ol.control.LayerSwitcher({
tipLabel: 'Legend', // Optional label for button
groupSelectStyle: 'children' // Can be 'children' [default], 'group' or 'none'
});
map.addControl(layerSwitcher);
var popup = new Popup();
map.addOverlay(popup);
map.on('singleclick', function(evt) {
var click = ol.proj.transform(evt.coordinate, 'EPSG:3857', 'EPSG:4326');
var radius = document.getElementById('radius').value;
var source = parcelles_radius.getSource();
source.setUrl(featureServer
+ "functions/parcelles_radius/items.json?limit=10000"
+ "&click_lon=" + parseFloat(click[0]).toFixed(4)
+ "&click_lat=" + parseFloat(click[1]).toFixed(4)
+ "&radius=" + radius)
source.refresh({force: true});
});
</script>
</body>
</html>
# Database connection
DbConnection = "user=postgres password=foss4g host=db dbname=cadastre"
# Close pooled connections after this interval
DbPoolMaxConnLifeTime = "1h"
# Hold no more than this number of connections in the database pool
DbPoolMaxConns = 4
# Look to read html templates from this directory
AssetsPath = "./assets"
# Accept connections on this subnet (default accepts on all subnets)
HttpHost = "0.0.0.0"
# Accept connections on this port
HttpPort = 7800
# Advertise URLs relative to this server name
# default is to look this up from incoming request headers
# UrlBase = "http://yourserver.com/"
# Resolution to quantize vector tiles to
DefaultResolution = 4096
# Rendering buffer to add to vector tiles
DefaultBuffer = 256
# Limit number of features requested (-1 = no limit)
MaxFeaturesPerTile = -1
# Advertise this minimum zoom level
DefaultMinZoom = 10
# Advertise this maximum zoom level
DefaultMaxZoom = 22
# Allow any page to consume these tiles
CORSOrigins = "*"
# logging information?
Debug = true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment