Mercurial > gemma
changeset 5420:851c14d57680 marking-single-beam
Merged default into marking-single-beam branch.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Thu, 08 Jul 2021 00:14:58 +0200 |
parents | 202715173935 (diff) fbad74acd23f (current diff) |
children | c9da747d4109 |
files | schema/version.sql |
diffstat | 15 files changed, 422 insertions(+), 127 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/fairway/BottleneckDialogue.vue Wed Jul 07 20:11:17 2021 +0200 +++ b/client/src/components/fairway/BottleneckDialogue.vue Thu Jul 08 00:14:58 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,15 @@ "surveys", "surveysLoading" ]), + selectedSurveyIsMarking() { + 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 07 20:11:17 2021 +0200 +++ b/client/src/components/importconfiguration/types/Soundingresults.vue Thu Jul 08 00:14:58 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/pkg/controllers/diff.go Wed Jul 07 20:11:17 2021 +0200 +++ b/pkg/controllers/diff.go Thu Jul 08 00:14:58 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 07 20:11:17 2021 +0200 +++ b/pkg/controllers/srimports.go Thu Jul 08 00:14:58 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 07 20:11:17 2021 +0200 +++ b/pkg/controllers/surveys.go Thu Jul 08 00:14:58 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/imports/isr.go Wed Jul 07 20:11:17 2021 +0200 +++ b/pkg/imports/isr.go Thu Jul 08 00:14:58 2021 +0200 @@ -157,7 +157,7 @@ func (isr *IsoRefresh) processBottleneck( ctx context.Context, conn *sql.Conn, - heights []float64, + heights mesh.ClassBreaks, bn *bottleneckSoundingResults, ) error { // Do one transaction per bottleneck. @@ -179,7 +179,7 @@ if err != nil { return err } - hs := mesh.ExtrapolateClassBreaks(heights, tree.Min().Z, tree.Max().Z) + hs := heights.ExtrapolateClassBreaks(tree.Min().Z, tree.Max().Z) hs = common.DedupFloat64s(hs) // Delete the old iso areas.
--- a/pkg/imports/sr.go Wed Jul 07 20:11:17 2021 +0200 +++ b/pkg/imports/sr.go Thu Jul 08 00:14:58 2021 +0200 @@ -24,6 +24,7 @@ "errors" "fmt" "io" + "log" "math" "os" "path" @@ -58,8 +59,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 +107,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 +199,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 +254,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 +439,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 +480,6 @@ removed map[int32]struct{} polygonArea float64 clippingPolygonWKB []byte - tin *mesh.Tin ) if boundary == nil { @@ -532,7 +546,7 @@ removed = str.Clip(&clippingPolygon) } - if sr.singleBeam() { + if sr.surveyType() == models.SurveyTypeSingleBeam { origDensity := float64(len(xyz)) / polygonArea @@ -604,27 +618,42 @@ 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() + removed = make(map[int32]struct{}) + for i, v := range xyz { + if clippingPolygonBuffered.IntersectionBox2D(v.Box2D()) == mesh.IntersectionOutSide { + removed[int32(i)] = struct{}{} + } + } + feedback.Info("Clipping took %v.", time.Since(start)) + feedback.Info("Number of points to clip %d.", len(removed)) + } start = time.Now() @@ -663,22 +692,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 - } - feedback.Info("Storing mesh index took %s.", time.Since(start)) - err = generateIsos(ctx, tx, feedback, &final, id) - 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)) + if err := generateIsos(ctx, tx, feedback, &final, id); err != nil { + return nil, err + } + } else { // SurveyTypeMarking + if err := generateMarkingPoints( + ctx, tx, feedback, + xyz, removed, epsg, + id, + ); err != nil { + return nil, err + } } // Store for potential later removal. @@ -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,6 +928,85 @@ return shapeToPolygon(s) } +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 loadClassBreaks( + ctx context.Context, + tx *sql.Tx, + feedback Feedback, + minZ, maxZ float64, +) mesh.ClassBreaks { + + heights, err := mesh.LoadClassBreaks( + ctx, tx, + "morphology_classbreaks") + + 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 generateMarkingPoints( + ctx context.Context, + tx *sql.Tx, + feedback Feedback, + xyz mesh.MultiPointZ, + removed map[int32]struct{}, + epsg uint32, + id int64, +) error { + log.Printf("debug: generateMarkingPoints") + + min, max := mesh.MinMaxVertex(xyz.FilterRemoved(removed)) + + log.Printf("debug: min/max %.2f/%.2f\n", min.Z, max.Z) + + heights := loadClassBreaks(ctx, tx, feedback, min.Z, max.Z) + + classes := heights.Classify(xyz.FilterRemoved(removed)) + + // Should not happen ... Z values over the top. + if n := len(classes) - 1; n > 1 && len(classes[n]) > 0 { + // Place the over the top values to the class below. + classes[n-1] = append(classes[n-1], classes[n]...) + classes[n] = nil + classes = classes[:n] + } + + 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 + } + log.Printf("debug: class %d: %d\n", i, len(class)) + _, err := stmt.ExecContext(ctx, id, heights[i], epsg, class.AsWKB()) + if err != nil { + return err + } + } + + return nil +} + func generateIsos( ctx context.Context, tx *sql.Tx, @@ -887,33 +1015,7 @@ id int64, ) error { - heights, err := mesh.LoadClassBreaks( - ctx, tx, - "morphology_classbreaks", - ) - - minZ, maxZ := tree.Min().Z, tree.Max().Z - - 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) - } - - /* - for i, v := range heights { - fmt.Printf("%d %.2f\n", i, v) - } - log.Printf("%.2f - %.2f\n", tree.Min.Z, tree.Max.Z) - */ - - heights = common.DedupFloat64s(heights) + heights := loadClassBreaks(ctx, tx, feedback, tree.Min().Z, tree.Max().Z) return generateIsoAreas(ctx, tx, feedback, tree, heights, id) } @@ -923,7 +1025,7 @@ tx *sql.Tx, feedback Feedback, tree *mesh.STRTree, - heights []float64, + heights mesh.ClassBreaks, id int64, ) error { feedback.Info("Generate iso areas") @@ -955,7 +1057,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 07 20:11:17 2021 +0200 +++ b/pkg/mesh/classbreaks.go Thu Jul 08 00:14:58 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,22 @@ return n } + +func (cbs ClassBreaks) Dedup() ClassBreaks { + return ClassBreaks(common.DedupFloat64s(cbs)) +} + +func (cbs ClassBreaks) Classify(points func() (Vertex, bool)) []MultiPointZ { + classes := make([]MultiPointZ, len(cbs)+1) + for v, ok := points(); ok; v, ok = points() { + idx := len(cbs) + 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 07 20:11:17 2021 +0200 +++ b/pkg/mesh/raster.go Thu Jul 08 00:14:58 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 07 20:11:17 2021 +0200 +++ b/pkg/mesh/vertex.go Thu Jul 08 00:14:58 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,53 @@ return out } +// All returns all points as an iterator. +func (mpz MultiPointZ) All() func() (Vertex, bool) { + var idx int + return func() (v Vertex, ok bool) { + if idx >= len(mpz) { + ok = false + return + } + v, ok = mpz[idx], true + idx++ + return + } +} + +// FilterRemoved returns an iterator that only delivers the vertices +// which indices are not marked as removed. +func (mpz MultiPointZ) FilterRemoved(removed map[int32]struct{}) func() (Vertex, bool) { + var idx int32 + return func() (v Vertex, ok bool) { + for { + if idx >= int32(len(mpz)) { + ok = false + return + } + if _, rm := removed[idx]; rm { + idx++ + continue + } + break + } + v, ok = mpz[idx], true + idx++ + return + } +} + +// MinMaxVertex runs over a point iterator and figures out its boundary. +func MinMaxVertex(points func() (Vertex, bool)) (Vertex, Vertex) { + min := Vertex{math.MaxFloat64, math.MaxFloat64, math.MaxFloat64} + max := Vertex{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} + for v, ok := points(); ok; v, ok = points() { + 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 07 20:11:17 2021 +0200 +++ b/pkg/models/sr.go Thu Jul 08 00:14:58 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 @@ -58,9 +81,35 @@ func (m *SoundingResultMeta) Decode(r io.Reader) error { err := json.NewDecoder(r).Decode(m) - if err == nil && m.EPSG == 0 { + if err != nil { + return err + } + + if m.EPSG == 0 { m.EPSG = WGS84 } + + 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 err }
--- a/pkg/models/surveys.go Wed Jul 07 20:11:17 2021 +0200 +++ b/pkg/models/surveys.go Thu Jul 08 00:14:58 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/gemma.sql Wed Jul 07 20:11:17 2021 +0200 +++ b/schema/gemma.sql Thu Jul 08 00:14:58 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,15 @@ -- 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, + points geometry(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 Thu Jul 08 00:14:58 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 geometry(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;