Mercurial > gemma
changeset 5446:e36cadc51d35
Merged marking-single-beam into default.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Wed, 14 Jul 2021 17:48:48 +0200 |
parents | 5be842692d87 (current diff) 0d6fb393c79a (diff) |
children | 7a96321be219 |
files | |
diffstat | 26 files changed, 969 insertions(+), 200 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/Pdftool.vue Wed Jul 14 16:02:43 2021 +0200 +++ b/client/src/components/Pdftool.vue Wed Jul 14 17:48:48 2021 +0200 @@ -283,7 +283,14 @@ bbox: currentExtent, geometryName: "areas" }; - if (this.selectedSurvey) { + const survey = this.selectedSurvey; + if (survey) { + if (survey["survey_type"] === "marking") { + params["featureTypes"] = [ + "sounding_results_marking_points_geoserver" + ]; + params["geometryName"] = "points"; + } params["filter"] = equalToFilter( "bottleneck_id", this.selectedSurvey.bottleneck_id @@ -808,7 +815,11 @@ this.soundingInfo.feature.properties.zpg_exception; let survey = this.selectedSurvey; - + const SURVEYTYPES = { + marking: "Marking Vessel", + multi: "Multibeam", + single: "Singlebeam" + }; // determine text dimensions // this is a little bit cumbersome but we need to separate width // calculations and writing @@ -818,7 +829,8 @@ let str1_1 = this.$gettext("Bottleneck") + ": "; let str1_2 = this.selectedBottleneck; let str2_1 = this.$gettext("Survey date") + ": "; - let str2_2 = survey.date_info; + let str2_2 = + survey.date_info + " (" + SURVEYTYPES[survey["survey_type"]] + ")"; let str3_1 = this.$gettext("Ref gauge") + ": "; let str3_2 = survey.gauge_objname; let str4_1 = this.$gettext("Depth relativ to") + ": ";
--- a/client/src/components/fairway/BottleneckDialogue.vue Wed Jul 14 16:02:43 2021 +0200 +++ b/client/src/components/fairway/BottleneckDialogue.vue Wed Jul 14 17:48:48 2021 +0200 @@ -103,6 +103,7 @@ <select v-model="additionalSurvey" class="form-control form-control-sm small" + :disabled="!areDifferecesAllowed" > <option :value="null">None</option> <option @@ -264,7 +265,7 @@ <button class="btn btn-info btn-sm w-100" @click="toggleCutTool" - :disabled="!selectedSurvey" + :disabled="!isCutAllowed" > <font-awesome-icon :icon="cutToolEnabled ? 'times' : 'plus'" /> {{ cutToolEnabled ? "Cancel" : "New" }} @@ -330,6 +331,7 @@ * * Author(s): * Markus Kottländer <markus.kottlaender@intevation.de> + * Thomas Junk <thomas.junk@intevation.de> */ import { mapState, mapGetters } from "vuex"; import Feature from "ol/Feature"; @@ -358,6 +360,16 @@ "surveys", "surveysLoading" ]), + selectedSurveyIsMarking() { + if (!this.selectedSurvey) return false; + return this.selectedSurvey["survey_type"] === "marking"; + }, + areDifferecesAllowed() { + return !this.selectedSurveyIsMarking; + }, + isCutAllowed() { + return !this.selectedSurveyIsMarking && !!this.selectedSurvey; + }, isAllowedToDelete() { const userCountryCode = this.userCountries[this.user]; const bottleneck = this.bottlenecksList.find(
--- a/client/src/components/importconfiguration/types/Soundingresults.vue Wed Jul 14 16:02:43 2021 +0200 +++ b/client/src/components/importconfiguration/types/Soundingresults.vue Wed Jul 14 17:48:48 2021 +0200 @@ -254,6 +254,7 @@ this.token = null; this.eMailNotification = false; this.messages = []; + this.beamType = ""; }, fileSelected(e) { const files = e.target.files || e.dataTransfer.files; @@ -296,10 +297,10 @@ } }) .then(response => { - if (response.data.meta) { + if (response.data && response.data.meta) { const { bottleneck, date, epsg } = response.data.meta; const depthReference = response.data.meta["depth-reference"]; - const singlebeam = response.data.meta["single-beam"]; + const surveyType = response.data.meta["survey-type"]; this.negateZ = response.data.meta["negate-z"]; this.bottleneck = this.bottlenecks.find( bn => bn.properties.objnam === bottleneck @@ -307,9 +308,20 @@ this.depthReference = depthReference; this.importDate = new Date(date).toISOString().split("T")[0]; this.projection = epsg; - this.beamType = singlebeam - ? this.$options.BEAMTYPES.SINGLEBEAM - : this.$options.BEAMTYPES.MULTIBEAM; + switch (surveyType) { + case "single": + this.beamType = this.$options.BEAMTYPES.SINGLEBEAM; + break; + case "multi": + this.beamType = this.$options.BEAMTYPES.MULTIBEAM; + break; + case "marking": + this.beamType = this.$options.BEAMTYPES.MARKING; + break; + default: + this.beamType = this.$options.BEAMTYPES.MULTIBEAM; + break; + } } this.importState = IMPORTSTATE.EDIT; this.token = response.data.token; @@ -337,11 +349,7 @@ if (this.depthReference) formData.append("depth-reference", this.depthReference); if (this.projection) formData.append("epsg", this.projection); - if (this.beamType) - formData.append( - "single-beam", - this.beamType === this.$options.BEAMTYPES.SINGLEBEAM - ); + if (this.beamType) formData.append("survey-type", this.beamType); formData.append("negate-z", this.negateZ == true); HTTP.post("/imports/sr", formData, { headers: { @@ -458,8 +466,7 @@ "depth-reference": this.depthReference, bottleneck: this.bottleneck.properties.objnam, date: this.importDate, - "single-beam": - this.beamType === this.$options.BEAMTYPES.SINGLEBEAM, + "survey-type": this.beamType, epsg: Number(this.projection), "negate-z": this.negateZ == true }) @@ -487,8 +494,9 @@ } }, BEAMTYPES: { - MULTIBEAM: "multi-beam", - SINGLEBEAM: "single-beam" + MULTIBEAM: "multi", + SINGLEBEAM: "single", + MARKING: "marking" }, UPLOADLABEL: "choose a .zip or .txt file", on: "on",
--- a/client/src/components/layers/layers.js Wed Jul 14 16:02:43 2021 +0200 +++ b/client/src/components/layers/layers.js Wed Jul 14 17:48:48 2021 +0200 @@ -386,7 +386,7 @@ projection: "EPSG:3857", url: window.location.origin + "/api/internal/wms", params: { - LAYERS: "sounding_results_areas_geoserver", + LAYERS: "sounding_results", VERSION: "1.1.1", TILED: true },
--- a/client/src/lib/mixins.js Wed Jul 14 16:02:43 2021 +0200 +++ b/client/src/lib/mixins.js Wed Jul 14 17:48:48 2021 +0200 @@ -440,17 +440,36 @@ if (text.includes("{date}")) { text = text.replace("{date}", new Date().toLocaleString(locale2)); } + const shortDate = d => { + return ( + (d.getDate() < 10 ? "0" : "") + + d.getDate() + + "." + + (d.getMonth() + 1 < 10 ? "0" : "") + + (d.getMonth() + 1) + + "." + + d.getFullYear() + ); + }; + const hasVisibleSurvey = + this.soundingInfo && + this.bottleneckForPrint && + this.selectedSurvey && + this.openLayersMap() + .getLayer("BOTTLENECKISOLINE") + .getVisible(); + if (text.includes("{surveydate}") && hasVisibleSurvey) { + const dateFromSurvey = new Date(this.selectedSurvey["date_info"]); + let dt = shortDate(dateFromSurvey); + text = text.replace("{surveydate}", dt.toLocaleString(locale2)); + } else { + let dt = shortDate(new Date()); + text = text.replace("{surveydate}", dt.toLocaleString(locale2)); + } // get only day,month and year from the Date object if (text.includes("{date-minor}")) { var date = new Date(); - var dt = - (date.getDate() < 10 ? "0" : "") + - date.getDate() + - "." + - (date.getMonth() + 1 < 10 ? "0" : "") + - (date.getMonth() + 1) + - "." + - date.getFullYear(); + let dt = shortDate(date); text = text.replace("{date-minor}", dt.toLocaleString(locale2)); } if (text.includes("{user}")) {
--- a/pkg/controllers/diff.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/controllers/diff.go Wed Jul 14 17:48:48 2021 +0200 @@ -251,7 +251,7 @@ log.Printf("info: z range: %.3f - %.3f\n", zMin, zMax) - var heights []float64 + var heights mesh.ClassBreaks heights, err = mesh.LoadClassBreaks( ctx, tx, @@ -261,7 +261,7 @@ err = nil heights = mesh.SampleDiffHeights(zMin, zMax, contourStep) } else { - heights = mesh.ExtrapolateClassBreaks(heights, zMin, zMax) + heights = heights.ExtrapolateClassBreaks(zMin, zMax) } heights = common.DedupFloat64s(heights)
--- a/pkg/controllers/srimports.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/controllers/srimports.go Wed Jul 14 17:48:48 2021 +0200 @@ -112,17 +112,33 @@ sr.NegateZ = &negateZ } + // Kept this in for compat. if v := req.FormValue("single-beam"); v != "" { - var singleBeam bool + var surveyType models.SurveyType switch strings.ToLower(v) { case "true", "1", "singlebeam", "single-beam": - singleBeam = true + surveyType = models.SurveyTypeSingleBeam case "false", "0", "multibeam", "multi-beam": - singleBeam = false + surveyType = models.SurveyTypeMultiBeam default: return fmt.Errorf("unknown single-beam '%s'", v) } - sr.SingleBeam = &singleBeam + sr.SurveyType = &surveyType + } + + if v := req.FormValue("survey-type"); v != "" { + var surveyType models.SurveyType + switch strings.ToLower(v) { + case "2", "marking": + surveyType = models.SurveyTypeMarking + case "true", "1", "singlebeam", "single-beam", "single": + surveyType = models.SurveyTypeSingleBeam + case "false", "0", "multibeam", "multi-beam", "multi": + surveyType = models.SurveyTypeMultiBeam + default: + return fmt.Errorf("unknown survey-type '%s'", v) + } + sr.SurveyType = &surveyType } return nil
--- a/pkg/controllers/surveys.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/controllers/surveys.go Wed Jul 14 17:48:48 2021 +0200 @@ -33,7 +33,8 @@ s.date_info::text, s.depth_reference, COALESCE(g.objname, 'ERROR: MISSING GAUGE') AS gauge_objname, - r.value AS waterlevel_value + r.value AS waterlevel_value, + COALESCE(s.surtyp, 'ERROR: MISSING SURVEY TYPE') AS surtype FROM waterway.bottlenecks AS b JOIN waterway.sounding_results AS s ON b.bottleneck_id = s.bottleneck_id LEFT JOIN waterway.gauges AS g @@ -63,6 +64,7 @@ // (like done in controllers/search.go) for rows.Next() { var survey models.Survey + var surType string var level sql.NullInt64 if err = rows.Scan( &survey.BottleneckID, @@ -70,12 +72,14 @@ &survey.DepthReference, &survey.ReferenceGauge, &level, + &surType, ); err != nil { return } if level.Valid { survey.WaterLevelValue = &level.Int64 } + survey.SurveyType = models.SurveyType(surType) surveys = append(surveys, &survey) }
--- a/pkg/controllers/system.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/controllers/system.go Wed Jul 14 17:48:48 2021 +0200 @@ -194,9 +194,13 @@ return nil, err } + styles := strings.Split(which, ",") + doBoth := func(req *http.Request) { log.Printf("info: Trigger re-calculation of %s.", which) - geoserver.ReconfigureStyle(which) + for _, style := range styles { + geoserver.ReconfigureStyle(style) + } recalc(req) } @@ -228,7 +232,11 @@ // Only the color changed -> no expensive recalc needed. if colorChanged { log.Println("info: Only colors changed.") - return func(*http.Request) { geoserver.ReconfigureStyle(which) }, nil + return func(*http.Request) { + for _, style := range styles { + geoserver.ReconfigureStyle(style) + } + }, nil } return nil, nil @@ -239,7 +247,7 @@ func(old sql.NullString, curr string) (func(*http.Request), error) { return reconfigureClassBreaks( old, curr, - "sounding_results_areas_geoserver", + "sounding_results_areas_geoserver,sounding_results_marking_points_geoserver", func(req *http.Request) { if s, ok := auth.GetSession(req); ok { triggerSoundingResultsContoursRecalc(s.User, curr)
--- a/pkg/geoserver/boot.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/geoserver/boot.go Wed Jul 14 17:48:48 2021 +0200 @@ -308,6 +308,18 @@ } if srs := tables[i].SRS; srs != nil { ft["srs"] = *srs + // A bit of a hack! + if *srs == "EPSG:4326" { + box := map[string]interface{}{ + "minx": -180, + "maxx": +180, + "miny": -90, + "maxy": +90, + "crs": "EPSG:4326", + } + ft["nativeBoundingBox"] = box + ft["latLonBoundingBox"] = box + } } var entries []map[string]interface{}
--- a/pkg/geoserver/templates.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/geoserver/templates.go Wed Jul 14 17:48:48 2021 +0200 @@ -38,6 +38,9 @@ "sounding_results_areas_geoserver", templateContourLinesFunc("morphology_classbreaks")) RegisterStylePreprocessor( + "sounding_results_marking_points_geoserver", + templateContourLinesFunc("morphology_classbreaks")) + RegisterStylePreprocessor( "sounding_differences", templateContourLinesFunc("morphology_classbreaks_compare")) RegisterStylePreprocessor(
--- a/pkg/imports/isr.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/imports/isr.go Wed Jul 14 17:48:48 2021 +0200 @@ -16,10 +16,11 @@ import ( "context" "database/sql" + "log" "time" - "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/mesh" + "gemma.intevation.de/gemma/pkg/models" ) // IsoRefresh is an import job to refresh the pre-calculated @@ -52,23 +53,40 @@ const ( fetchSoundingResultsIDsSQL = ` -SELECT bottleneck_id, id +SELECT + bottleneck_id, + id, + surtyp FROM waterway.sounding_results -ORDER BY bottleneck_id -` +ORDER BY bottleneck_id` + deleteIsoAreasSQL = ` DELETE FROM waterway.sounding_results_iso_areas -WHERE sounding_result_id = $1 -` +WHERE sounding_result_id = $1` + + fetchMarkingPointsSQL = ` +SELECT ST_AsBinary(points::geometry(MULTIPOINTZ)) +FROM waterway.sounding_results_marking_points +WHERE sounding_result_id = $1` + + deleteMarkingPointsSQL = ` +DELETE FROM waterway.sounding_results_marking_points +WHERE sounding_result_id = $1` ) // CleanUp of a iso refresh import is a NOP. func (isr *IsoRefresh) CleanUp() error { return nil } -type bottleneckSoundingResults struct { - bn string - srs []int64 -} +type ( + scanResult struct { + id int64 + surveyType models.SurveyType + } + bottleneckSoundingResults struct { + bn string + srs []scanResult + } +) func fetchBottleneckResults( ctx context.Context, @@ -81,33 +99,32 @@ } defer rows.Close() - var ids []bottleneckSoundingResults + var bsrs []bottleneckSoundingResults for rows.Next() { - var bn string - var sr int64 - if err := rows.Scan(&bn, &sr); err != nil { + var ( + bn string + id int64 + st string + ) + if err := rows.Scan(&bn, &id, &st); err != nil { return nil, err } - if len(ids) > 0 { - if ids[len(ids)-1].bn != bn { - ids = append(ids, bottleneckSoundingResults{ - bn: bn, - srs: []int64{sr}, - }) - } else { - ids[len(ids)-1].srs = append(ids[len(ids)-1].srs, sr) - } + sr := scanResult{id: id, surveyType: models.SurveyType(st)} + + if l := len(bsrs); l == 0 || bsrs[l-1].bn != bn { + bsrs = append(bsrs, bottleneckSoundingResults{ + bn: bn, + srs: []scanResult{sr}, + }) } else { - ids = []bottleneckSoundingResults{ - {bn: bn, srs: []int64{sr}}, - } + bsrs[l-1].srs = append(bsrs[l-1].srs, sr) } } if err := rows.Err(); err != nil { return nil, err } - return ids, nil + return bsrs, nil } // Do executes the actual refreshing of the iso areas. @@ -131,17 +148,25 @@ if err != nil { return nil, err } + heights = heights.Dedup() bns, err := fetchBottleneckResults(ctx, conn) if err != nil { return nil, err } + isrs, err := newISRStmts(ctx, conn) + if err != nil { + return nil, err + } + defer isrs.close() + for i := range bns { start := time.Now() feedback.Info("Processing bottleneck '%s' ...", bns[i].bn) err := isr.processBottleneck( - ctx, conn, + ctx, conn, feedback, + isrs, heights, &bns[i], ) @@ -154,10 +179,55 @@ return nil, nil } +type isrStmts struct { + insertAreas *sql.Stmt + deleteAreas *sql.Stmt + fetchMarkingPoints *sql.Stmt + deleteMarkingPoints *sql.Stmt + insertMarkingPoints *sql.Stmt +} + +func newISRStmts(ctx context.Context, conn *sql.Conn) (*isrStmts, error) { + var isrs isrStmts + for _, x := range []struct { + stmt **sql.Stmt + query string + }{ + {&isrs.insertAreas, insertIsoAreasSQL}, + {&isrs.deleteAreas, deleteIsoAreasSQL}, + {&isrs.fetchMarkingPoints, fetchMarkingPointsSQL}, + {&isrs.deleteMarkingPoints, deleteMarkingPointsSQL}, + {&isrs.insertMarkingPoints, insertMarkingPointsSQL}, + } { + var err error + if *x.stmt, err = conn.PrepareContext(ctx, x.query); err != nil { + isrs.close() + return nil, err + } + } + return &isrs, nil +} + +func (isrs *isrStmts) close() { + for _, x := range []**sql.Stmt{ + &isrs.insertAreas, + &isrs.deleteAreas, + &isrs.fetchMarkingPoints, + &isrs.deleteMarkingPoints, + &isrs.insertMarkingPoints, + } { + if *x != nil { + (*x).Close() + } + } +} + func (isr *IsoRefresh) processBottleneck( ctx context.Context, conn *sql.Conn, - heights []float64, + feedback Feedback, + isrs *isrStmts, + heights mesh.ClassBreaks, bn *bottleneckSoundingResults, ) error { // Do one transaction per bottleneck. @@ -167,52 +237,114 @@ } defer tx.Rollback() - insertAreasStmt, err := tx.Prepare(insertIsoAreasSQL) - if err != nil { - return err - } - defer insertAreasStmt.Close() + var ( + insertAreas = tx.StmtContext(ctx, isrs.insertAreas) + deleteAreas = tx.StmtContext(ctx, isrs.deleteAreas) + fetchMarkingPoints = tx.StmtContext(ctx, isrs.fetchMarkingPoints) + deleteMarkingPoints = tx.StmtContext(ctx, isrs.deleteMarkingPoints) + insertMarkingPoints = tx.StmtContext(ctx, isrs.insertMarkingPoints) + ) + + var markings, beams int // For all sounding results in bottleneck. for _, sr := range bn.srs { - tree, err := mesh.FetchMeshDirectly(ctx, tx, sr) - if err != nil { - return err - } - hs := mesh.ExtrapolateClassBreaks(heights, tree.Min().Z, tree.Max().Z) - hs = common.DedupFloat64s(hs) + switch sr.surveyType { + case models.SurveyTypeMarking: + markings++ + + // Read all points back in. - // Delete the old iso areas. - if _, err := tx.ExecContext(ctx, deleteIsoAreasSQL, sr); err != nil { - return err - } + var points mesh.MultiPointZ + + if err := func() error { + rows, err := fetchMarkingPoints.QueryContext(ctx, sr.id) + if err != nil { + return err + } + defer rows.Close() - // Calculate and store the iso areas. - box := mesh.Box2D{ - X1: tree.Min().X, - Y1: tree.Min().Y, - X2: tree.Max().X, - Y2: tree.Max().Y, - } + for rows.Next() { + var buf []byte + if err := rows.Scan(&buf); err != nil { + return err + } + var npoints mesh.MultiPointZ + if err := npoints.FromWKB(buf); err != nil { + return err + } + points = append(points, npoints...) + } + return rows.Err() + }(); err != nil { + return err + } - raster := mesh.NewRaster(box, isoCellSize) - raster.Rasterize(tree.Value) - areas := raster.Trace(hs) - - for i, a := range areas { - if len(a) == 0 { - continue - } - if _, err := insertAreasStmt.ExecContext( - ctx, - sr, hs[i], tree.EPSG(), - a.AsWKB(), - contourTolerance, - ); err != nil { + // Delete old points + if _, err := deleteMarkingPoints.ExecContext(ctx, sr.id); err != nil { return err } + + // Re-classify points. + classes := heights.Classify(points) + + // Re-insert points + for i, class := range classes { + // Ignore empty classes + if len(class) == 0 { + continue + } + if _, err := insertMarkingPoints.ExecContext( + ctx, sr.id, heights[i], 4326, class.AsWKB(), + ); err != nil { + return err + } + } + + case models.SurveyTypeMultiBeam, models.SurveyTypeSingleBeam: + beams++ + + tree, err := mesh.FetchMeshDirectly(ctx, tx, sr.id) + if err != nil { + return err + } + hs := heights.ExtrapolateClassBreaks(tree.Min().Z, tree.Max().Z).Dedup() + + // Delete the old iso areas. + if _, err := deleteAreas.ExecContext(ctx, sr.id); err != nil { + return err + } + + // Calculate and store the iso areas. + box := mesh.Box2D{ + X1: tree.Min().X, + Y1: tree.Min().Y, + X2: tree.Max().X, + Y2: tree.Max().Y, + } + + raster := mesh.NewRaster(box, isoCellSize) + raster.Rasterize(tree.Value) + areas := raster.Trace(hs) + + for i, a := range areas { + if len(a) == 0 { + continue + } + if _, err := insertAreas.ExecContext( + ctx, + sr.id, hs[i], tree.EPSG(), + a.AsWKB(), + contourTolerance, + ); err != nil { + return err + } + } + default: + log.Printf("error: unknown survey type '%s'\n", sr.surveyType) } } + feedback.Info("Scan types: Single/Multi: %d Marking: %d", beams, markings) return tx.Commit() }
--- a/pkg/imports/sr.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/imports/sr.go Wed Jul 14 17:48:48 2021 +0200 @@ -58,8 +58,10 @@ // DepthReference if given overides the DepthReference value // from the meta.json. DepthReference *string `json:"depth-reference,omitempty"` - // SingleBeam indicates that the sounding is a single beam scan. + // SingleBeam is kept in for compat. SingleBeam *bool `json:"single-beam,omitempty"` + // SurveyType indicates that the sounding is a single beam scan. + SurveyType *models.SurveyType `json:"survey-type,omitempty"` // NegateZ indicated that the Z values of thy XYZ input should be // multiplied by -1. NegateZ *bool `json:"negate-z,omitempty"` @@ -104,7 +106,8 @@ func (srJobCreator) Depends() [2][]string { return [2][]string{ - {"sounding_results", "sounding_results_iso_areas"}, + {"sounding_results", "sounding_results_iso_areas", + "sounding_results_marking_points"}, {"bottlenecks"}, } } @@ -195,6 +198,19 @@ FROM waterway.sounding_results sr WHERE id = $1 ` + insertMarkingPointsSQL = ` +INSERT INTO waterway.sounding_results_marking_points ( + sounding_result_id, + height, + points +) +SELECT + $1, + $2, + ST_Transform(ST_GeomFromWKB($4, $3::integer), 4326) +FROM waterway.sounding_results sr +WHERE id = $1 +` selectGaugeLDCSQL = ` SELECT @@ -237,18 +253,21 @@ if sr.NegateZ != nil && *sr.NegateZ { descs = append(descs, "negateZ") } + if sr.surveyType != nil { + descs = append(descs, string(*sr.SurveyType)) + } return strings.Join(descs, "|"), nil } -func (sr *SoundingResult) singleBeam() bool { - return sr.SingleBeam != nil && *sr.SingleBeam +func (sr *SoundingResult) surveyType() models.SurveyType { + if sr.SurveyType != nil { + return *sr.SurveyType + } + return models.SurveyTypeMultiBeam } func (sr *SoundingResult) surtype() string { - if sr.singleBeam() { - return "single" - } - return "multi" + return string(sr.surveyType()) } func (sr *SoundingResult) negateZ() bool { @@ -419,24 +438,19 @@ zpgException bool, ) (interface{}, error) { - if sr.singleBeam() { - feedback.Info("Processing as single beam scan.") - } else { - feedback.Info("Processing as multi beam scan.") - } + feedback.Info("Processing as %s beam scan.", sr.surtype()) feedback.Info("Reproject XYZ data.") start := time.Now() - xyzWKB := xyz.AsWKB() var reproj []byte var epsg uint32 if err := tx.QueryRowContext( ctx, reprojectPointsSingleBeamSQL, - xyzWKB, + xyz.AsWKB(), m.EPSG, ).Scan(&reproj, &epsg); err != nil { return nil, err @@ -465,7 +479,6 @@ removed map[int32]struct{} polygonArea float64 clippingPolygonWKB []byte - tin *mesh.Tin ) if boundary == nil { @@ -532,7 +545,7 @@ removed = str.Clip(&clippingPolygon) } - if sr.singleBeam() { + if sr.surveyType() == models.SurveyTypeSingleBeam { origDensity := float64(len(xyz)) / polygonArea @@ -604,27 +617,41 @@ multibeam: - start = time.Now() - tin = tri.Tin() - tin.EPSG = epsg + final := mesh.STRTree{Entries: 16} + + if sr.surveyType() != models.SurveyTypeMarking { - var str mesh.STRTree - str.Build(tin) - feedback.Info("Building clipping index took %v", time.Since(start)) + start = time.Now() + tin := tri.Tin() + tin.EPSG = epsg - start = time.Now() + var str mesh.STRTree + str.Build(tin) + feedback.Info("Building clipping index took %v", time.Since(start)) + + start = time.Now() - clippingPolygonBuffered.Indexify() - removed = str.Clip(&clippingPolygonBuffered) - feedback.Info("Clipping took %v.", time.Since(start)) - feedback.Info("Number of triangles to clip %d.", len(removed)) + clippingPolygonBuffered.Indexify() + removed = str.Clip(&clippingPolygonBuffered) + feedback.Info("Clipping took %v.", time.Since(start)) + feedback.Info("Number of triangles to clip %d.", len(removed)) + + start = time.Now() + final.BuildWithout(tin, removed) - start = time.Now() - final := mesh.STRTree{Entries: 16} - final.BuildWithout(tin, removed) - - feedback.Info("Building final mesh took %v.", time.Since(start)) - feedback.Info("Store mesh.") + feedback.Info("Building final mesh took %v.", time.Since(start)) + feedback.Info("Store mesh.") + } else { + start = time.Now() + clippingPolygonBuffered.Indexify() + before := len(xyz) + xyz = xyz.Filter(func(v mesh.Vertex) bool { + return clippingPolygonBuffered.IntersectionBox2D(v.Box2D()) != + mesh.IntersectionOutSide + }) + feedback.Info("Clipping took %v.", time.Since(start)) + feedback.Info("Number of points to clip: %d.", before-len(xyz)) + } start = time.Now() @@ -663,20 +690,32 @@ return nil, err } - index, err := final.Bytes() - if err != nil { - return nil, err - } + if sr.surveyType() != models.SurveyTypeMarking { + + index, err := final.Bytes() + if err != nil { + return nil, err + } - h := sha1.New() - h.Write(index) - checksum := hex.EncodeToString(h.Sum(nil)) - _, err = tx.ExecContext(ctx, insertMeshSQL, id, checksum, index) - if err != nil { - return nil, err + h := sha1.New() + h.Write(index) + checksum := hex.EncodeToString(h.Sum(nil)) + _, err = tx.ExecContext(ctx, insertMeshSQL, id, checksum, index) + if err != nil { + return nil, err + } + feedback.Info("Storing mesh index took %s.", time.Since(start)) + err = generateIsoAreas( + ctx, tx, feedback, + &final, + loadClassBreaks(ctx, tx, feedback, final.Min().Z, final.Max().Z), + id) + } else { // SurveyTypeMarking + err = generateMarkingPoints( + ctx, tx, feedback, + xyz, epsg, + id) } - feedback.Info("Storing mesh index took %s.", time.Since(start)) - err = generateIsos(ctx, tx, feedback, &final, id) if err != nil { return nil, err } @@ -712,7 +751,7 @@ return sr.Bottleneck != nil && sr.Date != nil && sr.DepthReference != nil && - sr.SingleBeam != nil && + sr.SurveyType != nil && sr.NegateZ != nil } @@ -729,7 +768,7 @@ Bottleneck: *sr.Bottleneck, EPSG: epsg, DepthReference: *sr.DepthReference, - SingleBeam: sr.singleBeam(), + SurveyType: sr.surveyType(), NegateZ: sr.negateZ(), }, nil } @@ -756,8 +795,18 @@ if sr.DepthReference != nil { m.DepthReference = *sr.DepthReference } + + // Kept in for compat if sr.SingleBeam != nil { - m.SingleBeam = *sr.SingleBeam + if *sr.SingleBeam { + m.SurveyType = models.SurveyTypeSingleBeam + } else { + m.SurveyType = models.SurveyTypeSingleBeam + } + } + + if sr.SurveyType != nil { + m.SurveyType = *sr.SurveyType } if sr.NegateZ != nil { m.NegateZ = *sr.NegateZ @@ -879,43 +928,79 @@ return shapeToPolygon(s) } -func generateIsos( +func defaultClassBreaks(min, max float64) mesh.ClassBreaks { + var heights mesh.ClassBreaks + h := contourStepWidth * math.Ceil(min/contourStepWidth) + for ; h <= max; h += contourStepWidth { + heights = append(heights, h) + } + return heights +} + +func generateMarkingPoints( ctx context.Context, tx *sql.Tx, feedback Feedback, - tree *mesh.STRTree, + xyz mesh.MultiPointZ, + epsg uint32, id int64, ) error { - heights, err := mesh.LoadClassBreaks( ctx, tx, - "morphology_classbreaks", - ) - - minZ, maxZ := tree.Min().Z, tree.Max().Z + "morphology_classbreaks") if err != nil { feedback.Warn("Loading class breaks failed: %v", err) feedback.Info("Using default class breaks") - heights = nil - h := contourStepWidth * math.Ceil(minZ/contourStepWidth) - for ; h <= maxZ; h += contourStepWidth { - heights = append(heights, h) - } - } else { - heights = mesh.ExtrapolateClassBreaks(heights, minZ, maxZ) + min, max := xyz.MinMax() + heights = defaultClassBreaks(min.Z, max.Z) } - /* - for i, v := range heights { - fmt.Printf("%d %.2f\n", i, v) + heights = heights.Dedup() + + classes := heights.Classify(xyz) + + stmt, err := tx.PrepareContext(ctx, insertMarkingPointsSQL) + if err != nil { + return err + } + defer stmt.Close() + + for i, class := range classes { + // Ignore empty classes + if len(class) == 0 { + continue + } + if _, err := stmt.ExecContext( + ctx, id, heights[i], epsg, class.AsWKB(), + ); err != nil { + return err } - log.Printf("%.2f - %.2f\n", tree.Min.Z, tree.Max.Z) - */ + } + + return nil +} + +func loadClassBreaks( + ctx context.Context, + tx *sql.Tx, + feedback Feedback, + minZ, maxZ float64, +) mesh.ClassBreaks { - heights = common.DedupFloat64s(heights) + heights, err := mesh.LoadClassBreaks( + ctx, tx, + "morphology_classbreaks") - return generateIsoAreas(ctx, tx, feedback, tree, heights, id) + if err != nil { + feedback.Warn("Loading class breaks failed: %v", err) + feedback.Info("Using default class breaks") + heights = defaultClassBreaks(minZ, maxZ) + } else { + heights = heights.ExtrapolateClassBreaks(minZ, maxZ) + } + + return heights.Dedup() } func generateIsoAreas( @@ -923,7 +1008,7 @@ tx *sql.Tx, feedback Feedback, tree *mesh.STRTree, - heights []float64, + heights mesh.ClassBreaks, id int64, ) error { feedback.Info("Generate iso areas") @@ -955,7 +1040,7 @@ feedback Feedback, areas []wkb.MultiPolygonGeom, epsg uint32, - heights []float64, + heights mesh.ClassBreaks, id int64, ) error { feedback.Info("Store iso areas.")
--- a/pkg/mesh/classbreaks.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/mesh/classbreaks.go Wed Jul 14 17:48:48 2021 +0200 @@ -21,6 +21,8 @@ "sort" "strconv" "strings" + + "gemma.intevation.de/gemma/pkg/common" ) const ( @@ -29,8 +31,10 @@ WHERE config_key = $1` ) -func SampleDiffHeights(min, max, step float64) []float64 { - var heights []float64 +type ClassBreaks []float64 + +func SampleDiffHeights(min, max, step float64) ClassBreaks { + var heights ClassBreaks switch { case min >= 0: // All values positive. for v := 0.0; v <= max; v += step { @@ -58,10 +62,10 @@ return heights } -func ParseClassBreaks(config string) ([]float64, error) { +func ParseClassBreaks(config string) (ClassBreaks, error) { parts := strings.Split(config, ",") - classes := make([]float64, 0, len(parts)) + classes := make(ClassBreaks, 0, len(parts)) for _, part := range parts { if idx := strings.IndexRune(part, ':'); idx >= 0 { part = part[:idx] @@ -80,7 +84,7 @@ return classes, nil } -func LoadClassBreaks(ctx context.Context, tx *sql.Tx, key string) ([]float64, error) { +func LoadClassBreaks(ctx context.Context, tx *sql.Tx, key string) (ClassBreaks, error) { var config sql.NullString @@ -102,12 +106,12 @@ return math.Round(v*10000) / 10000 } -func ExtrapolateClassBreaks(cbs []float64, min, max float64) []float64 { +func (cbs ClassBreaks) ExtrapolateClassBreaks(min, max float64) ClassBreaks { if min > max { min, max = max, min } - n := make([]float64, len(cbs)) + n := make(ClassBreaks, len(cbs)) copy(n, cbs) sort.Float64s(n) @@ -148,3 +152,26 @@ return n } + +func (cbs ClassBreaks) Dedup() ClassBreaks { + return ClassBreaks(common.DedupFloat64s(cbs)) +} + +func (cbs ClassBreaks) Classify(points MultiPointZ) []MultiPointZ { + if len(cbs) == 0 { + return nil + } + classes := make([]MultiPointZ, len(cbs)) + for _, v := range points { + // Place in last class if greater than all. + idx := len(cbs) - 1 + for i, cb := range cbs { + if v.Z <= cb { + idx = i + break + } + } + classes[idx] = append(classes[idx], v) + } + return classes +}
--- a/pkg/mesh/raster.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/mesh/raster.go Wed Jul 14 17:48:48 2021 +0200 @@ -195,7 +195,7 @@ return min, max, min != math.MaxFloat64 } -func (r *Raster) Trace(heights []float64) []wkb.MultiPolygonGeom { +func (r *Raster) Trace(heights ClassBreaks) []wkb.MultiPolygonGeom { start := time.Now() tracer := contourmap.FromFloat64s(r.XCells+2, r.YCells+2, r.Cells)
--- a/pkg/mesh/vertex.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/mesh/vertex.go Wed Jul 14 17:48:48 2021 +0200 @@ -163,6 +163,16 @@ return math.Sqrt(v.Dot(v)) } +// Box2D constructs a Box2D of this vertex. +func (v Vertex) Box2D() Box2D { + return Box2D{ + X1: v.X, + Y1: v.Y, + X2: v.X, + Y2: v.Y, + } +} + func area(a, b, c Vertex) float64 { return (b.Y-a.Y)*(c.X-b.X) - (b.X-a.X)*(c.Y-b.Y) } @@ -1134,6 +1144,29 @@ return out } +// Filter returns a copy removed the vertices which +// don't pass the filter test. +func (mpz MultiPointZ) Filter(filter func(Vertex) bool) MultiPointZ { + n := make(MultiPointZ, 0, len(mpz)) + for _, v := range mpz { + if filter(v) { + n = append(n, v) + } + } + return n +} + +// MinMaxVertex returns the extend of the point set. +func (mpz MultiPointZ) MinMax() (Vertex, Vertex) { + min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} + max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} + for _, v := range mpz { + min.Minimize(v) + max.Maximize(v) + } + return min, max +} + // AsWKB returns a WKB representation of the given point cloud. func (mpz MultiPointZ) AsWKB() []byte { size := 1 + 4 + 4 + len(mpz)*(1+4+3*8)
--- a/pkg/models/sr.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/models/sr.go Wed Jul 14 17:48:48 2021 +0200 @@ -25,17 +25,40 @@ "gemma.intevation.de/gemma/pkg/common" ) +type SurveyType string + +const ( + SurveyTypeMultiBeam = SurveyType("multi") + SurveyTypeSingleBeam = SurveyType("single") + SurveyTypeMarking = SurveyType("marking") +) + type ( SoundingResultMeta struct { - Date Date `json:"date"` - Bottleneck string `json:"bottleneck"` - EPSG uint `json:"epsg"` - DepthReference string `json:"depth-reference"` - SingleBeam bool `json:"single-beam"` - NegateZ bool `json:"negate-z,omitempty"` + Date Date `json:"date"` + Bottleneck string `json:"bottleneck"` + EPSG uint `json:"epsg"` + DepthReference string `json:"depth-reference"` + SingleBeam *bool `json:"single-beam,omitempty"` // kept in for compat! + SurveyType SurveyType `json:"survey-type,omitempty"` + NegateZ bool `json:"negate-z,omitempty"` } ) +func (st *SurveyType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + switch x := SurveyType(s); x { + case SurveyTypeMultiBeam, SurveyTypeSingleBeam, SurveyTypeMarking: + *st = x + return nil + default: + return fmt.Errorf("unkown survey type '%s'", s) + } +} + const ( checkDepthReferenceSQL = ` SELECT EXISTS(SELECT 1 @@ -57,11 +80,37 @@ ) func (m *SoundingResultMeta) Decode(r io.Reader) error { - err := json.NewDecoder(r).Decode(m) - if err == nil && m.EPSG == 0 { + if err := json.NewDecoder(r).Decode(m); err != nil { + return err + } + + if m.EPSG == 0 { m.EPSG = WGS84 } - return err + + if m.SingleBeam != nil { + // Check if single-beam and survey-type match. + if m.SurveyType != "" { + if (*m.SingleBeam && m.SurveyType != SurveyTypeSingleBeam) || + (!*m.SingleBeam && m.SurveyType != SurveyTypeMultiBeam) { + return errors.New("'single-beam' and 'survey-type' mismatch") + } + } else { // Only single-beam given + if *m.SingleBeam { + m.SurveyType = SurveyTypeSingleBeam + } else { + m.SurveyType = SurveyTypeMultiBeam + } + } + // Kill single-beam + m.SingleBeam = nil + } + + if m.SurveyType == "" { // default to multi-beam + m.SurveyType = SurveyTypeMultiBeam + } + + return nil } func (m *SoundingResultMeta) Validate(ctx context.Context, conn *sql.Conn) []error {
--- a/pkg/models/surveys.go Wed Jul 14 16:02:43 2021 +0200 +++ b/pkg/models/surveys.go Wed Jul 14 17:48:48 2021 +0200 @@ -15,10 +15,11 @@ type ( Survey struct { - BottleneckID string `json:"bottleneck_id"` - DateInfo string `json:"date_info"` - DepthReference string `json:"depth_reference"` - ReferenceGauge string `json:"gauge_objname"` - WaterLevelValue *int64 `json:"waterlevel_value,omitempty"` + BottleneckID string `json:"bottleneck_id"` + DateInfo string `json:"date_info"` + DepthReference string `json:"depth_reference"` + ReferenceGauge string `json:"gauge_objname"` + SurveyType SurveyType `json:"survey_type"` + WaterLevelValue *int64 `json:"waterlevel_value,omitempty"` } )
--- a/schema/default_sysconfig.sql Wed Jul 14 16:02:43 2021 +0200 +++ b/schema/default_sysconfig.sql Wed Jul 14 17:48:48 2021 +0200 @@ -218,6 +218,16 @@ FROM waterway.sounding_results_iso_areas ia JOIN waterway.sounding_results sr ON sr.id = ia.sounding_result_id $$), + ('waterway', 'sounding_results_marking_points_geoserver', 4326, NULL, $$ + SELECT bottleneck_id, + to_char(date_info, 'YYYY-MM-DD') AS date_info, + height, + points::geometry(MULTIPOINTZ, 4326), + surtyp, + zpg_exception + FROM waterway.sounding_results_marking_points mp + JOIN waterway.sounding_results sr ON sr.id = mp.sounding_result_id + $$), ('waterway', 'bottleneck_overview', 4326, NULL, $$ SELECT objnam AS name, @@ -400,7 +410,9 @@ -- -- group layers -- -INSERT INTO sys_admin.layer_groups VALUES ('fairway_marks'); +INSERT INTO sys_admin.layer_groups VALUES + ('fairway_marks'), + ('sounding_results'); INSERT INTO sys_admin.grouped_layers VALUES ('fairway_marks', 'waterway', 'fairway_marks_boylat_hydro', 0), @@ -415,7 +427,9 @@ ('fairway_marks', 'waterway', 'fairway_marks_lights', 6), ('fairway_marks', 'waterway', 'fairway_marks_rtpbcn', 7), ('fairway_marks', 'waterway', 'fairway_marks_topmar', 8), - ('fairway_marks', 'waterway', 'fairway_marks_notmrk', 9); + ('fairway_marks', 'waterway', 'fairway_marks_notmrk', 9), + ('sounding_results', 'waterway', 'sounding_results_areas_geoserver', 0), + ('sounding_results', 'waterway', 'sounding_results_marking_points_geoserver', 1); -- -- Settings
--- a/schema/gemma.sql Wed Jul 14 16:02:43 2021 +0200 +++ b/schema/gemma.sql Wed Jul 14 17:48:48 2021 +0200 @@ -353,7 +353,7 @@ survey_type varchar PRIMARY KEY ); -INSERT INTO survey_types (survey_type) VALUES ('single'), ('multi'); +INSERT INTO survey_types (survey_type) VALUES ('single'), ('multi'), ('marking'); CREATE TABLE coverage_types ( coverage_type varchar PRIMARY KEY @@ -828,6 +828,18 @@ -- CHECK(ST_IsSimple(CAST(areas AS geometry))), PRIMARY KEY (sounding_result_id, height) ) + + CREATE TABLE sounding_results_marking_points ( + sounding_result_id int NOT NULL REFERENCES sounding_results + ON DELETE CASCADE, + height numeric NOT NULL, + -- XXX: GeoServer does not like geography(MULTIPOINTZ) + -- We need to track this down. Maybe with + -- GeoServer upstream. + points geography(MULTIPOINTZ, 4326) NOT NULL, + PRIMARY KEY (sounding_result_id, height) + ) + -- -- Fairway availability --
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1460/01.markings.sql Wed Jul 14 17:48:48 2021 +0200 @@ -0,0 +1,15 @@ +INSERT INTO survey_types (survey_type) VALUES ('marking'); + +CREATE TABLE waterway.sounding_results_marking_points ( + sounding_result_id int NOT NULL REFERENCES waterway.sounding_results + ON DELETE CASCADE, + height numeric NOT NULL, + points geography(MULTIPOINTZ, 4326) NOT NULL, + PRIMARY KEY (sounding_result_id, height) +); + +GRANT INSERT, UPDATE, DELETE ON waterway.sounding_results_marking_points + TO waterway_admin; + +GRANT SELECT ON waterway.sounding_results_marking_points + TO waterway_user;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1461/01.config.sql Wed Jul 14 17:48:48 2021 +0200 @@ -0,0 +1,15 @@ +-- GeoServer SQL views without time support +INSERT INTO sys_admin.published_services ( + schema, name, srid, key_column, view_def +) VALUES + ('waterway', 'sounding_results_marking_points_geoserver', 4326, NULL, $$ + SELECT bottleneck_id, + to_char(date_info, 'YYYY-MM-DD') AS date_info, + height, + points, + surtyp, + zpg_exception + FROM waterway.sounding_results_marking_points mp + JOIN waterway.sounding_results sr ON sr.id = mp.sounding_result_id + $$); +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1462/01.refine_marking_view.sql Wed Jul 14 17:48:48 2021 +0200 @@ -0,0 +1,11 @@ +UPDATE sys_admin.published_services SET view_def = $$ +SELECT bottleneck_id, + to_char(date_info, 'YYYY-MM-DD') AS date_info, + height, + points::geometry(multipointz, 4326) as points, + surtyp, + zpg_exception +FROM waterway.sounding_results_marking_points mp + JOIN waterway.sounding_results sr ON sr.id = mp.sounding_result_id +$$ +WHERE name = 'sounding_results_marking_points_geoserver';
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1463/01.sounding_result_group_layer.sql Wed Jul 14 17:48:48 2021 +0200 @@ -0,0 +1,6 @@ +INSERT INTO sys_admin.layer_groups VALUES + ('sounding_results'); + +INSERT INTO sys_admin.grouped_layers VALUES + ('sounding_results', 'waterway', 'sounding_results_areas_geoserver', 0), + ('sounding_results', 'waterway', 'sounding_results_marking_points_geoserver', 1);
--- a/schema/version.sql Wed Jul 14 16:02:43 2021 +0200 +++ b/schema/version.sql Wed Jul 14 17:48:48 2021 +0200 @@ -1,1 +1,1 @@ -INSERT INTO gemma_schema_version(version) VALUES (1454); +INSERT INTO gemma_schema_version(version) VALUES (1463);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/style-templates/sounding_results_marking_points_geoserver.sld-template Wed Jul 14 17:48:48 2021 +0200 @@ -0,0 +1,275 @@ +<?xml version="1.0" encoding="UTF-8"?> +<StyledLayerDescriptor + xmlns="http://www.opengis.net/sld" + xmlns:se="http://www.opengis.net/se" + xmlns:ogc="http://www.opengis.net/ogc" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd" + version="1.1.0"> + <NamedLayer> + <se:Name>sounding_results_areas</se:Name> + <UserStyle> + <se:Name>sounding_results_areas</se:Name> + <se:FeatureTypeStyle> + <se:Name>area_colours</se:Name> + <se:Description> + <se:Abstract> + FeatureTypeStyle defining colour classes for height attribute + </se:Abstract> + </se:Description> + {{ range . -}} + <se:Rule> + {{- if not .HasLow }} + <se:Name>≤ {{ printf "%g" .High }}</se:Name> + <ogc:Filter> + <ogc:PropertyIsLessThanOrEqualTo> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal> + </ogc:PropertyIsLessThanOrEqualTo> + </ogc:Filter> + {{- else if not .HasHigh }} + <se:Name>> {{ printf "%g" .Low }}</se:Name> + <ogc:Filter> + <ogc:PropertyIsGreaterThanOrEqualTo> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal> + </ogc:PropertyIsGreaterThanOrEqualTo> + </ogc:Filter> + {{- else }} + <se:Name>≤ {{ printf "%g" .High }}</se:Name> + <ogc:Filter> + <ogc:And> + <ogc:PropertyIsGreaterThan> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal> + </ogc:PropertyIsGreaterThan> + <ogc:PropertyIsLessThanOrEqualTo> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal> + </ogc:PropertyIsLessThanOrEqualTo> + </ogc:And> + </ogc:Filter> + {{- end }} + <se:MaxScaleDenominator>34e3</se:MaxScaleDenominator> + <se:PointSymbolizer> + <se:Graphic> + <se:Mark> + <se:WellKnownName>circle</se:WellKnownName> + <se:Fill> + <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter> + </se:Fill> + </se:Mark> + <se:Size>6</se:Size> + </se:Graphic> + </se:PointSymbolizer> + </se:Rule> + <se:Rule> + {{- if not .HasLow }} + <se:Name>≤ {{ printf "%g" .High }}</se:Name> + <ogc:Filter> + <ogc:PropertyIsLessThanOrEqualTo> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal> + </ogc:PropertyIsLessThanOrEqualTo> + </ogc:Filter> + {{- else if not .HasHigh }} + <se:Name>> {{ printf "%g" .Low }}</se:Name> + <ogc:Filter> + <ogc:PropertyIsGreaterThanOrEqualTo> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal> + </ogc:PropertyIsGreaterThanOrEqualTo> + </ogc:Filter> + {{- else }} + <se:Name>≤ {{ printf "%g" .High }}</se:Name> + <ogc:Filter> + <ogc:And> + <ogc:PropertyIsGreaterThan> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .Low }}</ogc:Literal> + </ogc:PropertyIsGreaterThan> + <ogc:PropertyIsLessThanOrEqualTo> + <ogc:PropertyName>height</ogc:PropertyName> + <ogc:Literal>{{ printf "%f" .High }}</ogc:Literal> + </ogc:PropertyIsLessThanOrEqualTo> + </ogc:And> + </ogc:Filter> + {{- end }} + <se:MinScaleDenominator>34e3</se:MinScaleDenominator> + <se:PointSymbolizer> + <se:Graphic> + <se:Mark> + <se:WellKnownName>circle</se:WellKnownName> + <se:Fill> + <se:SvgParameter name="fill">{{ .Color }}</se:SvgParameter> + </se:Fill> + </se:Mark> + <se:Size>6</se:Size> + </se:Graphic> + </se:PointSymbolizer> + </se:Rule> + <se:VendorOption name="sortBy">height</se:VendorOption> + {{ end }} + </se:FeatureTypeStyle> + <se:FeatureTypeStyle> + <se:Name>area_labels</se:Name> + <se:Description> + <se:Abstract> + FeatureTypeStyle for labels at colour areas + </se:Abstract> + </se:Description> + <se:Rule> + <se:MaxScaleDenominator>3e2</se:MaxScaleDenominator> + <se:TextSymbolizer> + <Halo></Halo> + <se:VendorOption name="spaceAround">50</se:VendorOption> + <se:Label> + <ogc:Function name="Recode"> + <ogc:Function name="numberFormat"> + <ogc:Literal>0.000000</ogc:Literal> + <ogc:PropertyName>height</ogc:PropertyName> + </ogc:Function> + {{ range . -}} + {{ if .HasHigh -}} + <ogc:Literal> + {{- printf "%f" .High -}} + </ogc:Literal><ogc:Literal> + {{- printf "%g" .High -}} + </ogc:Literal> + {{ end -}} + {{ end }} + </ogc:Function> + </se:Label> + <se:Font> + <se:SvgParameter name="font-size">80</se:SvgParameter> + <se:SvgParameter name="font-weight">bold</se:SvgParameter> + <se:SvgParameter name="font-family">Sans Serif</se:SvgParameter> + </se:Font> + <se:LabelPlacement> + <se:LinePlacement> + <se:PerpendicularOffset>5</se:PerpendicularOffset> + </se:LinePlacement> + </se:LabelPlacement> + <se:Fill> + <se:SvgParameter name="fill">#000000</se:SvgParameter> + </se:Fill> + </se:TextSymbolizer> + </se:Rule> + <se:Rule> + <se:MaxScaleDenominator>5e2</se:MaxScaleDenominator> + <se:MinScaleDenominator>3e2</se:MinScaleDenominator> + <se:TextSymbolizer> + <Halo></Halo> + <se:VendorOption name="spaceAround">80</se:VendorOption> + <se:Label> + <ogc:Function name="Recode"> + <ogc:Function name="numberFormat"> + <ogc:Literal>0.000000</ogc:Literal> + <ogc:PropertyName>height</ogc:PropertyName> + </ogc:Function> + {{ range . -}} + {{ if .HasHigh -}} + <ogc:Literal> + {{- printf "%f" .High -}} + </ogc:Literal><ogc:Literal> + {{- printf "%g" .High -}} + </ogc:Literal> + {{ end -}} + {{ end }} + </ogc:Function> + </se:Label> + <se:Font> + <se:SvgParameter name="font-size">40</se:SvgParameter> + <se:SvgParameter name="font-weight">bold</se:SvgParameter> + <se:SvgParameter name="font-family">Sans Serif</se:SvgParameter> + </se:Font> + <se:LabelPlacement> + <se:LinePlacement> + <se:PerpendicularOffset>5</se:PerpendicularOffset> + </se:LinePlacement> + </se:LabelPlacement> + <se:Fill> + <se:SvgParameter name="fill">#000000</se:SvgParameter> + </se:Fill> + </se:TextSymbolizer> + </se:Rule> + <se:Rule> + <se:MaxScaleDenominator>7e2</se:MaxScaleDenominator> + <se:MinScaleDenominator>5e2</se:MinScaleDenominator> + <se:TextSymbolizer> + <Halo></Halo> + <se:VendorOption name="spaceAround">50</se:VendorOption> + <se:Label> + <ogc:Function name="Recode"> + <ogc:Function name="numberFormat"> + <ogc:Literal>0.000000</ogc:Literal> + <ogc:PropertyName>height</ogc:PropertyName> + </ogc:Function> + {{ range . -}} + {{ if .HasHigh -}} + <ogc:Literal> + {{- printf "%f" .High -}} + </ogc:Literal><ogc:Literal> + {{- printf "%g" .High -}} + </ogc:Literal> + {{ end -}} + {{ end }} + </ogc:Function> + </se:Label> + <se:Font> + <se:SvgParameter name="font-size">20</se:SvgParameter> + <se:SvgParameter name="font-weight">bold</se:SvgParameter> + <se:SvgParameter name="font-family">Sans Serif</se:SvgParameter> + </se:Font> + <se:LabelPlacement> + <se:LinePlacement> + <se:PerpendicularOffset>5</se:PerpendicularOffset> + </se:LinePlacement> + </se:LabelPlacement> + <se:Fill> + <se:SvgParameter name="fill">#000000</se:SvgParameter> + </se:Fill> + </se:TextSymbolizer> + </se:Rule> + <se:Rule> + <se:MaxScaleDenominator>6e3</se:MaxScaleDenominator> + <se:MinScaleDenominator>7e2</se:MinScaleDenominator> + <se:TextSymbolizer> + <Halo></Halo> + <se:VendorOption name="spaceAround">50</se:VendorOption> + <se:Label> + <ogc:Function name="Recode"> + <ogc:Function name="numberFormat"> + <ogc:Literal>0.000000</ogc:Literal> + <ogc:PropertyName>height</ogc:PropertyName> + </ogc:Function> + {{ range . -}} + {{ if .HasHigh -}} + <ogc:Literal> + {{- printf "%f" .High -}} + </ogc:Literal><ogc:Literal> + {{- printf "%g" .High -}} + </ogc:Literal> + {{ end -}} + {{ end }} + </ogc:Function> + </se:Label> + <se:Font> + <se:SvgParameter name="font-size">12</se:SvgParameter> + <se:SvgParameter name="font-weight">bold</se:SvgParameter> + <se:SvgParameter name="font-family">Sans Serif</se:SvgParameter> + </se:Font> + <se:LabelPlacement> + <se:LinePlacement> + <se:PerpendicularOffset>5</se:PerpendicularOffset> + </se:LinePlacement> + </se:LabelPlacement> + <se:Fill> + <se:SvgParameter name="fill">#000000</se:SvgParameter> + </se:Fill> + </se:TextSymbolizer> + </se:Rule> + </se:FeatureTypeStyle> + </UserStyle> + </NamedLayer> +</StyledLayerDescriptor>