Skip to content

Commit

Permalink
Set world bounding box on GeoServer layer if DB table is empty
Browse files Browse the repository at this point in the history
Fix #40

Related to #159
  • Loading branch information
jirik committed Dec 1, 2020
1 parent a6dde8e commit 6d01840
Show file tree
Hide file tree
Showing 15 changed files with 186 additions and 26 deletions.
2 changes: 2 additions & 0 deletions doc/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Body parameters:
- ShapeFile files (at least three files: .shp, .shx, .dbf)
- file names, i.e. array of strings
- if file names are provided, files must be uploaded subsequently using [POST Layer Chunk](#post-layer-chunk)
- if published file has empty bounding box (i.e. no features), its bounding box on WMS/WFS endpoint is set to the whole World
- *name*, string
- computer-friendly identifier of the layer
- must be unique among all layers of one workspace
Expand Down Expand Up @@ -191,6 +192,7 @@ Body parameters:
- ShapeFile files (at least three files: .shp, .shx, .dbf)
- file names, i.e. array of strings
- if file names are provided, files must be uploaded subsequently using [POST Layer Chunk](#post-layer-chunk)
- if published file has empty bounding box (i.e. no features), its bounding box on WMS/WFS endpoint is set to the whole World
- *title*
- *description*
- *crs*, string `EPSG:3857` or `EPSG:4326`
Expand Down
Binary file added sample/layman.layer/empty.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions sample/layman.layer/empty.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
1 change: 1 addition & 0 deletions sample/layman.layer/empty.qpj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]
Binary file added sample/layman.layer/empty.shp
Binary file not shown.
Binary file added sample/layman.layer/empty.shx
Binary file not shown.
Binary file added sample/layman.layer/single_point.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions sample/layman.layer/single_point.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
1 change: 1 addition & 0 deletions sample/layman.layer/single_point.qpj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]
Binary file added sample/layman.layer/single_point.shp
Binary file not shown.
Binary file added sample/layman.layer/single_point.shx
Binary file not shown.
32 changes: 32 additions & 0 deletions src/layman/layer/db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import defaultdict
import json
import math
import os
import psycopg2
Expand Down Expand Up @@ -524,3 +525,34 @@ def ensure_attributes(attribute_tuples):
if missing_attributes:
create_string_attributes(missing_attributes, conn_cur)
return missing_attributes


def get_bbox(username, layername, conn_cur=None):
conn, cur = conn_cur or get_connection_cursor()

try:
cur.execute(f"""
select ST_AsGeoJSON(ST_Extent(wkb_geometry))
from {username}.{layername}
""")
except BaseException:
raise LaymanError(7)
rows = cur.fetchall()
result = rows[0][0]
if result is not None:
result = json.loads(result)
if isinstance(result, dict):
if result['type'] == 'Point':
result = result['coordinates'] + result['coordinates']
elif result['type'] == 'Polygon':
result = [
min([c[0] for c in result['coordinates']]),
min([c[1] for c in result['coordinates']]),
max([c[0] for c in result['coordinates']]),
max([c[1] for c in result['coordinates']]),
]
else:
raise Exception(f"Unknown BBox type {result['type']}")
elif result is not None:
raise Exception(f"Unknown BBox result {result}")
return result
38 changes: 38 additions & 0 deletions src/layman/layer/db/db_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,30 @@ def populated_places_table(testuser1):
delete_layer(username, layername)


@pytest.fixture()
def empty_table(testuser1):
file_path = 'sample/layman.layer/empty.shp'
username = testuser1
layername = 'empty'
with layman.app_context():
db.import_layer_vector_file(username, layername, file_path, None)
yield username, layername
with layman.app_context():
delete_layer(username, layername)


@pytest.fixture()
def single_point_table(testuser1):
file_path = 'sample/layman.layer/single_point.shp'
username = testuser1
layername = 'single_point'
with layman.app_context():
db.import_layer_vector_file(username, layername, file_path, None)
yield username, layername
with layman.app_context():
delete_layer(username, layername)


def test_abort_import_layer_vector_file(client):
username = 'testuser1'
layername = 'ne_10m_admin_0_countries'
Expand Down Expand Up @@ -269,3 +293,17 @@ def test_get_most_frequent_lower_distance(client, country110m_table, country50m_
sd_5k = db.guess_scale_denominator(username, layername_5k)
assert 1000 <= sd_5k <= 25000
assert sd_5k == 5000


def test_empty_table_bbox(empty_table):
username, layername = empty_table
with layman.app_context():
bbox = db.get_bbox(username, layername)
assert bbox is None


def test_single_point_table_bbox(single_point_table):
username, layername = single_point_table
with layman.app_context():
bbox = db.get_bbox(username, layername)
assert bbox[0] == bbox[2] and bbox[1] == bbox[3], bbox
75 changes: 75 additions & 0 deletions src/layman/layer/empty_bbox_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os
import pytest
import requests
from owslib.wms import WebMapService
from layman import settings
from layman.layer.geoserver import wms as gs_wms
from test import process, process_client
from test.data import wfs as wfs_data_util


ensure_layman = process.ensure_layman


def get_shp_file_paths(shp_file_path):
extensions = ['dbf', 'prj', 'qpj', 'shx']
root, shp_ext = os.path.splitext(shp_file_path)
result = [f"{root}.{ext}" for ext in extensions]
result.append(shp_file_path)
return result


def assert_non_empty_bbox(bbox):
assert bbox[0] < bbox[2] and bbox[1] < bbox[3]


def assert_wms_layer(workspace, layername, exp_title):
wms = WebMapService(gs_wms.get_wms_url(workspace), gs_wms.VERSION)
assert layername in wms.contents
wms_layer = wms[layername]
assert wms_layer.title == exp_title
assert_non_empty_bbox(wms_layer.boundingBox)
assert_non_empty_bbox(wms_layer.boundingBoxWGS84)
return wms_layer


def wfs_t_insert_point(workspace, layername):
wfs_t_data = wfs_data_util.get_wfs20_insert_points(workspace, layername)
wfs_t_url = f"http://{settings.LAYMAN_SERVER_NAME}/geoserver/{workspace}/wfs?request=Transaction"
wfs_t_headers = {
'Accept': 'text/xml',
'Content-type': 'text/xml',
}
r = requests.post(wfs_t_url,
data=wfs_t_data,
headers=wfs_t_headers)
assert r.status_code == 200


@pytest.mark.parametrize('layername, file_paths', [
('empty', get_shp_file_paths('sample/layman.layer/empty.shp')),
('single_point', get_shp_file_paths('sample/layman.layer/single_point.shp')),
])
@pytest.mark.usefixtures('ensure_layman')
def test_empty_shapefile(layername, file_paths):
workspace = 'test_empty_bbox_workspace'
title = layername

process_client.publish_layer(workspace, layername, file_paths=file_paths)

wms_layer = assert_wms_layer(workspace, layername, title)
native_bbox = wms_layer.boundingBox
wgs_bbox = wms_layer.boundingBoxWGS84

title = 'new title'
process_client.patch_layer(workspace, layername, title=title)
wms_layer = assert_wms_layer(workspace, layername, title)
assert wms_layer.boundingBox == native_bbox
assert wms_layer.boundingBoxWGS84 == wgs_bbox

wfs_t_insert_point(workspace, layername)
wms_layer = assert_wms_layer(workspace, layername, title)
assert wms_layer.boundingBox == native_bbox
assert wms_layer.boundingBoxWGS84 == wgs_bbox

process_client.delete_layer(workspace, layername)
61 changes: 35 additions & 26 deletions src/layman/layer/geoserver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from layman.http import LaymanError
from layman import settings, util as layman_util
from layman.common import geoserver as common
from layman.layer import LAYER_TYPE
from layman.layer import LAYER_TYPE, db as db_source

FLASK_WORKSPACES_KEY = f"{__name__}:WORKSPACES"
FLASK_RULES_KEY = f"{__name__}:RULES"
Expand Down Expand Up @@ -65,31 +65,40 @@ def publish_layer_from_db(username, layername, description, title, access_rights
title
]
keywords = list(set(keywords))
r = requests.post(
urljoin(settings.LAYMAN_GS_REST_WORKSPACES,
username + '/datastores/postgresql/featuretypes/'),
data=json.dumps(
{
"featureType": {
"name": layername,
"title": title,
"abstract": description,
"keywords": {
"string": keywords
},
"srs": "EPSG:3857",
"projectionPolicy": "FORCE_DECLARED",
"enabled": True,
"store": {
"@class": "dataStore",
"name": username + ":postgresql",
},
}
}
),
headers=headers_json,
auth=settings.LAYMAN_GS_AUTH
)
feature_type_def = {
"name": layername,
"title": title,
"abstract": description,
"keywords": {
"string": keywords
},
"srs": "EPSG:3857",
"projectionPolicy": "FORCE_DECLARED",
"enabled": True,
"store": {
"@class": "dataStore",
"name": username + ":postgresql",
},
}
db_bbox = db_source.get_bbox(username, layername)
if db_bbox is None:
# world
native_bbox = {
"minx": -20026376.39,
"miny": -20048966.10,
"maxx": 20026376.39,
"maxy": 20048966.10,
"crs": "EPSG:3857",
}
feature_type_def['nativeBoundingBox'] = native_bbox
r = requests.post(urljoin(settings.LAYMAN_GS_REST_WORKSPACES,
username + '/datastores/postgresql/featuretypes/'),
data=json.dumps({
"featureType": feature_type_def
}),
headers=headers_json,
auth=settings.LAYMAN_GS_AUTH,
)
r.raise_for_status()
# current_app.logger.info(f'publish_layer_from_db before clear_cache {username}')
from . import wms
Expand Down

0 comments on commit 6d01840

Please sign in to comment.