Skip to content

Commit

Permalink
feat: geom simplification through 'max-allowable-offset' parameter
Browse files Browse the repository at this point in the history
# Conflicts:
#	internal/data/feature_mock.go
#	internal/service/db_test/runner_db_test.go
  • Loading branch information
nick-rv authored and benoitdm-oslandia committed Feb 21, 2023
1 parent 1b38dba commit 7915680
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 46 deletions.
29 changes: 15 additions & 14 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,21 @@ const (
// ================== ParamReserved ==================

const (
ParamCrs = "crs"
ParamLimit = "limit"
ParamOffset = "offset"
ParamBbox = "bbox"
ParamBboxCrs = "bbox-crs"
ParamFilter = "filter"
ParamFilterCrs = "filter-crs"
ParamGroupBy = "groupby"
ParamOrderBy = "orderby"
ParamPrecision = "precision"
ParamProperties = "properties"
ParamSortBy = "sortby"
ParamTransform = "transform"
ParamType = "type"
ParamCrs = "crs"
ParamLimit = "limit"
ParamOffset = "offset"
ParamBbox = "bbox"
ParamBboxCrs = "bbox-crs"
ParamFilter = "filter"
ParamFilterCrs = "filter-crs"
ParamGroupBy = "groupby"
ParamOrderBy = "orderby"
ParamPrecision = "precision"
ParamProperties = "properties"
ParamSortBy = "sortby"
ParamTransform = "transform"
ParamType = "type"
ParamMaxAllowableOffset = "max-allowable-offset"
)

// known query parameter name
Expand Down
11 changes: 6 additions & 5 deletions internal/data/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ type QueryParam struct {
FilterSql string
Filter []*PropertyFilter
// Columns is the list of columns to return
Columns []string
GroupBy []string
SortBy []api.Sorting
Precision int
TransformFuns []api.TransformFunction
Columns []string
GroupBy []string
SortBy []api.Sorting
Precision int
TransformFuns []api.TransformFunction
MaxAllowableOffset float64
}
10 changes: 9 additions & 1 deletion internal/data/db_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,19 @@ const sqlFmtGeomCol = `ST_AsGeoJSON( %v %v ) AS _geojson`
func sqlGeomCol(geomCol string, sourceSRID int, param *QueryParam) string {
geomColSafe := strconv.Quote(geomCol)
geomExpr := applyTransform(param.TransformFuns, geomColSafe)
geomOutExpr := transformToOutCrs(geomExpr, sourceSRID, param.Crs)
simplifiedGeom := simplifyWithTolerance(geomExpr, param.MaxAllowableOffset)
geomOutExpr := transformToOutCrs(simplifiedGeom, sourceSRID, param.Crs)
sql := fmt.Sprintf(sqlFmtGeomCol, geomOutExpr, sqlPrecisionArg(param.Precision))
return sql
}

func simplifyWithTolerance(geomOutExpr string, tolerance float64) string {
if tolerance == 0.0 {
return geomOutExpr
}
return fmt.Sprintf("ST_Simplify(%v, %v)", geomOutExpr, tolerance)
}

func transformToOutCrs(geomExpr string, sourceSRID, outSRID int) string {
if sourceSRID == outSRID {
return geomExpr
Expand Down
44 changes: 44 additions & 0 deletions internal/data/feature_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ func makeFeatureMockPoint(tableName string, id int, x float64, y float64) *featu
return &feat
}

func makeFeatureMockPolygon(tableName string, val int, coords orb.Ring) *featureMock {

geom := geojson.NewGeometry(orb.Polygon{coords})

sum := fnv.New32a()
encodedContent, _ := json.Marshal(geom)
sum.Write(encodedContent)
weakEtag := fmt.Sprint(sum.Sum32())

httpDateString := api.GetCurrentHttpDate() // Last modified value

idstr := strconv.Itoa(val)

feat := featureMock{
GeojsonFeatureData: *api.MakeGeojsonFeature(
tableName,
idstr,
*geom,
map[string]interface{}{"prop_a": "propA", "prop_b": val, "prop_c": "propC", "prop_d": val % 10},
weakEtag,
httpDateString,
),
}
return &feat
}

func (fm *featureMock) toJSON(propNames []string) string {
props := fm.extractProperties(propNames)
return api.MakeGeojsonFeatureJSON("", fm.ID, *fm.Geom, props, fm.WeakEtag.Etag, fm.WeakEtag.LastModified)
Expand Down Expand Up @@ -135,6 +161,11 @@ func MakeFeatureMockPointAsJSON(tableName string, id int, x float64, y float64,
return feat.toJSON(columns)
}

func MakeFeatureMockPolygonAsJSON(tableName string, id int, coords orb.Ring, columns []string) string {
feat := makeFeatureMockPolygon(tableName, id, coords)
return feat.toJSON(columns)
}

func MakeFeaturesMockPoint(tableName string, extent api.Extent, nx int, ny int) []*featureMock {
basex := extent.Minx
basey := extent.Miny
Expand All @@ -156,3 +187,16 @@ func MakeFeaturesMockPoint(tableName string, extent api.Extent, nx int, ny int)
}
return features
}

// Returns a slice of polygon featureMocks with as many entries into the coords slice as argument
// val is an arbitraty value which is used to populate the properties values from each Feature
func MakeFeaturesMockPolygon(tableName string, val int, coords []orb.Ring) []*featureMock {

features := make([]*featureMock, len(coords))
index := 0
for _, coord := range coords {
features[index] = makeFeatureMockPolygon(tableName, val, coord)
index++
}
return features
}
100 changes: 100 additions & 0 deletions internal/service/db_test/handler_db_lod_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package db_test

/*
Copyright 2023 Crunchy Data Solutions, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Date : February 2023
Authors : Nicolas Revelant (nicolas dot revelant at ign dot fr)
*/

import (
"encoding/json"
"fmt"
"net/http"

"testing"

"github.com/CrunchyData/pg_featureserv/internal/api"
util "github.com/CrunchyData/pg_featureserv/internal/utiltest"
"github.com/paulmach/orb"
"github.com/paulmach/orb/geojson"
)

// Simple unit test case ensuring that simplification is working on a single feature
func (t *DbTests) TestGeometrySimplificationSingleFeature() {
t.Test.Run("TestGeometrySimplificationSingleFeature", func(t *testing.T) {
rr := hTest.DoRequest(t, "/collections/mock_poly/items/6?max-allowable-offset=0.01")
var v api.GeojsonFeatureData
errUnMarsh := json.Unmarshal(hTest.ReadBody(rr), &v)
util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh))
util.Equals(t, 5, len(v.Geom.Geometry().(orb.Polygon)[0]), "")

})
}

// Simple unit test case ensuring that simplification is working on several features
func (t *DbTests) TestGeometrySimplificationSeveralFeatures() {
t.Test.Run("TestGeometrySimplificationSeveralFeatures", func(t *testing.T) {
rr := hTest.DoRequest(t, "/collections/mock_poly/items?max-allowable-offset=0.01")
// Feature collection
var v api.FeatureCollection
errUnMarsh := json.Unmarshal(hTest.ReadBody(rr), &v)
util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh))
util.Equals(t, 6, len(v.Features), "wrong number of features")
feature := v.Features[0]
util.Equals(t, 4, len(feature.Geom.Geometry().(orb.Polygon)[0]), "wrong number of simplified coordinates")

})
}

// Test case with negative value as simplification factor
func (t *DbTests) TestGeometrySimplificationNegativeValue() {
t.Test.Run("TestGeometrySimplificationNegativeValue", func(t *testing.T) {
path := "/collections/mock_poly/items/6?max-allowable-offset=-0.01"
// If lower thant minVal, then minValue (0) is considered
hTest.DoRequestMethodStatus(t, "GET", path, nil, nil, http.StatusOK)
})
}

// Test case with wrong float separator for the simplification factor
func (t *DbTests) TestGeometrySimplificationWrongFloatSeparatorValue() {
t.Test.Run("TestGeometrySimplificationWrongFloatSeparatorValue", func(t *testing.T) {
path := "/collections/mock_poly/items?max-allowable-offset=0,01"
hTest.DoRequestMethodStatus(t, "GET", path, nil, nil, http.StatusBadRequest)
})
}

// Test case with various values as simplification factor
func (t *DbTests) TestGeometrySimplificationVariousSimplificationValues() {
t.Test.Run("TestGeometrySimplificationVariousSimplificationValues", func(t *testing.T) {
path := "/collections/mock_poly/items/4?max-allowable-offset=0.01"
rr := hTest.DoRequestMethodStatus(t, "GET", path, nil, nil, http.StatusOK)
// Feature collection
var feat api.GeojsonFeatureData
errUnMarsh := json.Unmarshal(hTest.ReadBody(rr), &feat)
util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh))
util.Equals(t, 4, len(feat.Geom.Geometry().(orb.Polygon)[0]), "wrong number of simplified coordinates")

path = "/collections/mock_poly/items/4?max-allowable-offset=0.001"
rr = hTest.DoRequestMethodStatus(t, "GET", path, nil, nil, http.StatusOK)
errUnMarsh = json.Unmarshal(hTest.ReadBody(rr), &feat)
util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh))
util.Equals(t, 10, len(feat.Geom.Geometry().(orb.Polygon)[0]), "wrong number of simplified coordinates")

path = "/collections/mock_poly/items/4?max-allowable-offset=1"
rr = hTest.DoRequestMethodStatus(t, "GET", path, nil, nil, http.StatusOK)
errUnMarsh = json.Unmarshal(hTest.ReadBody(rr), &feat)
util.Assert(t, errUnMarsh == nil, fmt.Sprintf("%v", errUnMarsh))
util.Equals(t, (*geojson.Geometry)(nil), feat.Geom, "simplified geometry still present")

})
}
10 changes: 10 additions & 0 deletions internal/service/db_test/runner_db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ func TestRunnerHandlerDb(t *testing.T) {
test.TestReplaceFeatureIfNoneMatchStarValueWithExistingRepresentationInCacheDb()
afterEachRun()
})
t.Run("LOD", func(t *testing.T) {
beforeEachRun()
test := DbTests{Test: t}
test.TestGeometrySimplificationSingleFeature()
test.TestGeometrySimplificationSeveralFeatures()
test.TestGeometrySimplificationNegativeValue()
test.TestGeometrySimplificationWrongFloatSeparatorValue()
test.TestGeometrySimplificationVariousSimplificationValues()
afterEachRun()
})

// after tests cleaning
afterRun()
Expand Down
1 change: 0 additions & 1 deletion internal/service/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,6 @@ func handleCollectionItems(w http.ResponseWriter, r *http.Request) *appError {
}
param, errQuery := createQueryParams(&reqParam, tbl.Columns, tbl.Srid)
param.Filter = parseFilter(reqParam.Values, tbl.DbTypes)

if errQuery == nil {
ctx := r.Context()
switch format {
Expand Down
72 changes: 50 additions & 22 deletions internal/service/param.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,20 @@ type NameValMap map[string]string

// RequestParam holds the parameters for a request
type RequestParam struct {
Crs int
Limit int
Offset int
Bbox *api.Extent
BboxCrs int
Properties []string
Filter string
FilterCrs int
GroupBy []string
SortBy []api.Sorting
Precision int
TransformFuns []api.TransformFunction
Values NameValMap
Crs int
Limit int
Offset int
Bbox *api.Extent
BboxCrs int
Properties []string
Filter string
FilterCrs int
GroupBy []string
SortBy []api.Sorting
Precision int
TransformFuns []api.TransformFunction
MaxAllowableOffset float64
Values NameValMap
}

func parseRequestParams(r *http.Request) (RequestParam, error) {
Expand Down Expand Up @@ -145,6 +146,13 @@ func parseRequestParams(r *http.Request) (RequestParam, error) {
return param, err
}

// --- max-allowable-offset parameter
// parseFloat(paramValues, key string, minVal float64, maxVal float64, defaultVal float64)
param.MaxAllowableOffset, err = parseFloat(paramValues, api.ParamMaxAllowableOffset, 0, 100000, 0)
if err != nil {
return param, err
}

return param, nil
}

Expand Down Expand Up @@ -181,6 +189,25 @@ func parseInt(values NameValMap, key string, minVal int, maxVal int, defaultVal
return val, nil
}

func parseFloat(values NameValMap, key string, minVal float64, maxVal float64, defaultVal float64) (float64, error) {
valStr := values[key]
// key not present or missing value
if len(valStr) < 1 {
return defaultVal, nil
}
val, err := strconv.ParseFloat(valStr, 64)
if err != nil {
return 0, fmt.Errorf(api.ErrMsgInvalidParameterValue, key, valStr)
}
if val < minVal {
val = minVal
}
if maxVal >= 0 && val > maxVal {
val = maxVal
}
return val, nil
}

func parseLimit(values NameValMap) (int, error) {
val := values[api.ParamLimit]
if len(val) < 1 {
Expand Down Expand Up @@ -443,15 +470,16 @@ func parseFilter(paramMap map[string]string, colNameMap map[string]api.Column) [
// createQueryParams applies any cross-parameter logic
func createQueryParams(param *RequestParam, colNames []string, sourceSRID int) (*data.QueryParam, error) {
query := data.QueryParam{
Crs: param.Crs,
Limit: param.Limit,
Offset: param.Offset,
Bbox: param.Bbox,
BboxCrs: param.BboxCrs,
GroupBy: param.GroupBy,
SortBy: param.SortBy,
Precision: param.Precision,
TransformFuns: param.TransformFuns,
Crs: param.Crs,
Limit: param.Limit,
Offset: param.Offset,
Bbox: param.Bbox,
BboxCrs: param.BboxCrs,
GroupBy: param.GroupBy,
SortBy: param.SortBy,
Precision: param.Precision,
TransformFuns: param.TransformFuns,
MaxAllowableOffset: param.MaxAllowableOffset,
}
cols := param.Properties
// --- if groupby is present it replaces properties (it may be empty)
Expand Down
Loading

0 comments on commit 7915680

Please sign in to comment.