# HG changeset patch # User Thomas Junk # Date 1563361219 -7200 # Node ID e0751da6272c1b8aab477d368691777426a161a7 # Parent ea4e1ea04e44fc397591f98fe1fe324061ca92c9# Parent 5396581cf20334cbc5e69280e5d9b192640d96b9 merge with default diff -r ea4e1ea04e44 -r e0751da6272c client/package.json --- a/client/package.json Fri Jul 12 16:18:49 2019 +0200 +++ b/client/package.json Wed Jul 17 13:00:19 2019 +0200 @@ -1,6 +1,6 @@ { "name": "gemmajs", - "version": "3.1.0", + "version": "4.0.0-dev", "license": "AGPL-3.0-or-later", "repository": { "type": "hg", diff -r ea4e1ea04e44 -r e0751da6272c client/src/components/Pdftool.vue --- a/client/src/components/Pdftool.vue Fri Jul 12 16:18:49 2019 +0200 +++ b/client/src/components/Pdftool.vue Wed Jul 17 13:00:19 2019 +0200 @@ -209,6 +209,12 @@ close() { this.$store.commit("application/showPdfTool", false); }, + millimeter2pixels(length, dpi) { + return (dpi * length) / 25.4; + }, + pixel2millimeter(pixels, dpi) { + return (pixels * 25.4) / dpi; + }, // When a template is chosen from the dropdown, its propoerties are // applied to the rest of the form. applyTemplateToForm() { @@ -309,144 +315,186 @@ 1000 * pixelsPerMapMillimeter * metersPerPixel ); console.log("scaleDenominator = ", scaleDenominator); - - var data = canvas.toDataURL("image/jpeg"); - this.pdf.doc.addImage( - data, - "JPEG", - 0, - 0, + const width80DPI = this.millimeter2pixels(this.pdf.width, 80); + const height80DPI = this.millimeter2pixels(this.pdf.height, 80); + const currentHeight = this.millimeter2pixels( + this.pdf.height, + this.form.resolution + ); + const currentWidth = this.millimeter2pixels( this.pdf.width, - this.pdf.height + this.form.resolution ); + //const scaleFactor = this.form.resolution / 80; + var snapshot = canvas.toDataURL("image/jpeg"); + const offscreen = document.createElement("canvas"); + offscreen.width = currentWidth; + offscreen.height = currentHeight; + const image = new Image(); + image.src = snapshot; + image.onload = () => { + offscreen + .getContext("2d") + .drawImage( + image, + (currentWidth - width80DPI) / 2, + (currentHeight - height80DPI) / 2, + width80DPI, + height80DPI, + 0, + 0, + currentWidth, + currentHeight + ); + const data = offscreen.toDataURL("image/jpeg"); + this.pdf.doc.addImage( + data, + "JPEG", + 0, + 0, + this.pdf.width, + this.pdf.height + ); - if (this.templateData) { - this.pdf.doc.setFont("linbiolinum", "normal"); - let defaultFontSize = 11, - defaultRounding = 2, - defaultTextColor = "black", - defaultBgColor = "white", - defaultPadding = 3, - defaultOffset = { x: 0, y: 0 }, - defaultBorderColor = "white"; - this.templateData.elements.forEach(e => { - switch (e.type) { - case "text": { - this.addText( - e.position, - e.offset || defaultOffset, - e.width, - e.fontSize || defaultFontSize, - e.color || defaultTextColor, - e.text - ); - break; - } - case "box": { - this.addBox( - e.position, - e.offset || defaultOffset, - e.width, - e.height, - // handling the case when the rectangle not rounded (rounding = 0) - e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, - e.color || defaultBgColor, - e.brcolor || defaultBorderColor - ); - break; + if (this.templateData) { + this.pdf.doc.setFont("linbiolinum", "normal"); + let defaultFontSize = 11, + defaultRounding = 2, + defaultTextColor = "black", + defaultBgColor = "white", + defaultPadding = 3, + defaultOffset = { x: 0, y: 0 }, + defaultBorderColor = "white", + defaultWidth = 100; + this.templateData.elements.forEach(e => { + switch (e.type) { + case "text": { + this.addText( + e.position, + e.offset || defaultOffset, + e.width || defaultWidth, + e.fontSize || defaultFontSize, + e.color || defaultTextColor, + e.text + ); + break; + } + case "box": { + this.addBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + // handling the case when the rectangle not rounded (rounding = 0) + e.rounding === 0 || e.rounding + ? e.rounding + : defaultRounding, + e.color || defaultBgColor, + e.brcolor || defaultBorderColor + ); + break; + } + case "textbox": { + this.addTextBox( + e.position, + e.offset || defaultOffset, + e.width, + e.height, + e.rounding === 0 || e.rounding + ? e.rounding + : defaultRounding, + e.padding || defaultPadding, + e.fontSize || defaultFontSize, + e.color || defaultTextColor, + e.background || defaultBgColor, + e.text, + e.brcolor || defaultBorderColor + ); + break; + } + case "image": { + this.addImage( + e.url, + e.format, + e.position, + e.offset || defaultOffset, + e.width, + e.height + ); + break; + } + case "bottleneck": { + this.addBottleneckInfo( + e.position, + e.offset || defaultOffset, + e.rounding === 0 || e.rounding + ? e.rounding + : defaultRounding, + e.color || defaultTextColor, + e.brcolor || defaultBorderColor + ); + break; + } + case "legend": { + this.addLegend( + e.position, + e.offset || defaultOffset, + e.rounding === 0 || e.rounding + ? e.rounding + : defaultRounding, + e.brcolor || defaultBorderColor + ); + break; + } + case "scalebar": { + this.addScaleBar( + scaleDenominator, + e.position, + e.offset || defaultOffset, + e.rounding === 0 || e.rounding + ? e.rounding + : defaultRounding, + e.brcolor || defaultBorderColor + ); + break; + } + case "scale": { + this.addScale( + scaleDenominator, + e.position, + e.width, + e.offset || defaultOffset, + e.fontSize || defaultFontSize, + e.color || defaultTextColor + ); + break; + } + case "northarrow": { + this.addNorthArrow( + e.position, + e.offset || defaultOffset, + e.size + ); + break; + } } - case "textbox": { - this.addTextBox( - e.position, - e.offset || defaultOffset, - e.width, - e.height, - e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, - e.padding || defaultPadding, - e.fontSize || defaultFontSize, - e.color || defaultTextColor, - e.background || defaultBgColor, - e.text, - e.brcolor || defaultBorderColor - ); - break; - } - case "image": { - this.addImage( - e.url, - e.format, - e.position, - e.offset || defaultOffset, - e.width, - e.height - ); - break; - } - case "bottleneck": { - this.addBottleneckInfo( - e.position, - e.offset || defaultOffset, - e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, - e.color || defaultTextColor, - e.brcolor || defaultBorderColor - ); - break; - } - case "legend": { - this.addLegend( - e.position, - e.offset || defaultOffset, - e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, - e.brcolor || defaultBorderColor - ); - break; - } - case "scalebar": { - this.addScaleBar( - scaleDenominator, - e.position, - e.offset || defaultOffset, - e.rounding === 0 || e.rounding ? e.rounding : defaultRounding, - e.brcolor || defaultBorderColor - ); - break; - } - case "scale": { - this.addScale( - scaleDenominator, - e.position, - e.width, - e.offset || defaultOffset, - e.fontSize || defaultFontSize, - e.color || defaultTextColor - ); - break; - } - case "northarrow": { - this.addNorthArrow( - e.position, - e.offset || defaultOffset, - e.size - ); - break; - } - } + }); + + this.pdf.doc.save(this.filename); + } + + // reset to original size + map.setSize(this.mapSize); + map.getView().fit(this.mapExtent, { + size: this.mapSize, + // necessary to get to the previous zoom level in all cases + // details see https://github.com/openlayers/openlayers/issues/9235 + constrainResolution: false }); - this.pdf.doc.save(this.filename); - } - - // reset to original size - map.setSize(this.mapSize); - map.getView().fit(this.mapExtent, { - size: this.mapSize, - // necessary to get to the previous zoom level in all cases - // details see https://github.com/openlayers/openlayers/issues/9235 - constrainResolution: false - }); - - // as we are done: re-enable button - this.readyToGenerate = true; + // as we are done: re-enable button + this.readyToGenerate = true; + }; }); // trigger rendering diff -r ea4e1ea04e44 -r e0751da6272c client/src/components/importconfiguration/types/Soundingresults.vue --- a/client/src/components/importconfiguration/types/Soundingresults.vue Fri Jul 12 16:18:49 2019 +0200 +++ b/client/src/components/importconfiguration/types/Soundingresults.vue Wed Jul 17 13:00:19 2019 +0200 @@ -106,6 +106,21 @@ +
+
+ + Negate Z values in XYZ files + +
+
+ +
+
@@ -219,7 +234,8 @@ disableUpload: false, token: null, messages: [], - eMailNotification: false + eMailNotification: false, + negateZ: false }; }, methods: { @@ -280,6 +296,7 @@ const { bottleneck, date, epsg } = response.data.meta; const depthReference = response.data.meta["depth-reference"]; const singlebeam = response.data.meta["single-beam"]; + this.negateZ = response.data.meta["negate-z"]; this.bottleneck = this.bottlenecks.find( bn => bn.properties.objnam === bottleneck ); @@ -318,7 +335,7 @@ "single-beam", this.beamType === this.$options.BEAMTYPES.SINGLEBEAM ); - + formData.append("negate-z", this.negateZ == true); HTTP.post("/imports/sr", formData, { headers: { "X-Gemma-Auth": localStorage.getItem("token"), @@ -432,7 +449,8 @@ date: this.importDate, "single-beam": this.beamType === this.$options.BEAMTYPES.SINGLEBEAM, - epsg: Number(this.projection) + epsg: Number(this.projection), + "negate-z": this.negateZ == true }) ) ); diff -r ea4e1ea04e44 -r e0751da6272c cmd/gemma/main.go --- a/cmd/gemma/main.go Fri Jul 12 16:18:49 2019 +0200 +++ b/cmd/gemma/main.go Wed Jul 17 13:00:19 2019 +0200 @@ -91,7 +91,7 @@ done <- server.ListenAndServe() }() - sigChan := make(chan os.Signal) + sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, os.Kill, syscall.SIGTERM) select { diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/bottlenecks.go --- a/pkg/controllers/bottlenecks.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/bottlenecks.go Wed Jul 17 13:00:19 2019 +0200 @@ -519,7 +519,6 @@ // Too late for HTTP status message. log.Printf("error: %v\n", err) } - return } func bottleneckAvailableFairwayDepth(rw http.ResponseWriter, req *http.Request) { diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/diff.go --- a/pkg/controllers/diff.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/diff.go Wed Jul 17 13:00:19 2019 +0200 @@ -268,7 +268,7 @@ tree.Min.Z, tree.Max.Z, contourStep) } else { heights = octree.ExtrapolateClassBreaks(heights, tree.Min.Z, tree.Max.Z) - heights = octree.InBetweenClassBreaks(heights, 0.05, 2) + // heights = octree.InBetweenClassBreaks(heights, 0.05, 2) } log.Printf("info: num heights: %d\n", len(heights)) diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/importconfig.go --- a/pkg/controllers/importconfig.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/importconfig.go Wed Jul 17 13:00:19 2019 +0200 @@ -105,7 +105,7 @@ oldPasswd, ok := pc.Attributes["password"] pc.User = session.User pc.Attributes = common.Attributes{} - if ok == true { + if ok { pc.Attributes["password"] = oldPasswd } pc.Attributes.Marshal(config) // Marshal only overwrites keys present in config diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/pwreset.go --- a/pkg/controllers/pwreset.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/pwreset.go Wed Jul 17 13:00:19 2019 +0200 @@ -21,14 +21,17 @@ "database/sql" "encoding/hex" "errors" + "io" "log" "net/http" "os/exec" "strconv" "strings" - "text/template" "time" + htmlTemplate "html/template" + textTemplate "text/template" + "github.com/gorilla/mux" "gemma.intevation.de/gemma/pkg/auth" @@ -40,17 +43,15 @@ const ( insertRequestSQL = `INSERT INTO sys_admin.password_reset_requests - (hash, username) VALUES ($1, $2)` + (hash, username) VALUES ($1, $2) + ON CONFLICT (username) DO UPDATE SET hash = $1` countRequestsSQL = `SELECT count(*) FROM sys_admin.password_reset_requests` - countRequestsUserSQL = `SELECT count(*) FROM sys_admin.password_reset_requests - WHERE username = $1` - deleteRequestSQL = `DELETE FROM sys_admin.password_reset_requests WHERE hash = $1` - findRequestSQL = `SELECT lu.email_address, lu.username + findRequestSQL = `SELECT lu.username FROM sys_admin.password_reset_requests prr JOIN users.list_users lu on prr.username = lu.username WHERE prr.hash = $1` @@ -63,6 +64,10 @@ updatePasswordSQL = `UPDATE users.list_users SET pw = $1 WHERE username = $2` + + deletePasswordResetRequestSQL = ` + DELETE FROM sys_admin.password_reset_requests + WHERE username = $1` ) const ( @@ -77,41 +82,44 @@ const pwResetRole = "sys_admin" var ( - errTooMuchPasswordResets = errors.New("Too many password resets") - errTooMuchPasswordResetsPerUser = errors.New("Too many password resets per user") - errNoSuchUser = errors.New("User does not exist") - errInvalidUser = errors.New("Invalid user") + errTooMuchPasswordResets = errors.New("too many password resets") + errNoSuchUser = errors.New("user does not exist") + errInvalidUser = errors.New("invalid user") ) var ( - passwordResetRequestMailTmpl = template.Must( - template.New("request").Parse(`You or someone else has requested a password change + passwordResetRequestMailTmpl = textTemplate.Must( + textTemplate.New("request").Parse(`You or someone else has requested a password change for your account {{ .User }} on -{{ .HTTPS }}://{{ .Server }} +{{ .Server }} -Please follow this link to have a new password generated and mailed to you: +Please follow this link to have a new password generated: -{{ .HTTPS }}://{{ .Server }}/api/users/passwordreset/{{ .Hash }} +{{ .Server }}/api/users/passwordreset/{{ .Hash }} The link is only valid for 12 hours. If you did not initiate this password reset or do not want to reset the -password, just ignore this email. +password, just ignore this email. Logging in with your old password +before following the link will cancel this password reset request, too. Best regards Your service team`)) - passwordResetMailTmpl = template.Must( - template.New("reset").Parse(`Your password for your account {{ .User }} on -{{ .HTTPS }}://{{ .Server }} - -has been changed to - {{ .Password }} - -Change it as soon as possible. - -Best regards - Your service team`)) + passwordResetPage = htmlTemplate.Must( + htmlTemplate.New("page").Parse(` + + + + Password reset done + + +

The password reset for user {{ .User }} successfully done.

+

New password: {{ .Password }}

+

Go to login page.

+ + +`)) ) func init() { @@ -137,15 +145,13 @@ } } -func requestMessageBody(https, user, hash, server string) string { +func requestMessageBody(user, hash, server string) string { var content = struct { User string - HTTPS string Server string Hash string }{ User: user, - HTTPS: https, Server: server, Hash: hash, } @@ -156,31 +162,15 @@ return buf.String() } -func changedMessageBody(https, user, password, server string) string { +func changedMessageBody(w io.Writer, user, password string) error { var content = struct { User string - HTTPS string - Server string Password string }{ User: user, - HTTPS: https, - Server: server, Password: password, } - var buf bytes.Buffer - if err := passwordResetMailTmpl.Execute(&buf, &content); err != nil { - log.Printf("error: %v\n", err) - } - return buf.String() -} - -func useHTTPS(req *http.Request) string { - if req.Header.Get("X-Use-Protocol") == "https" || - req.URL.Scheme == "https" { - return "https" - } - return "http" + return passwordResetPage.Execute(w, &content) } func generateHash() string { @@ -198,7 +188,7 @@ return common.RandomString(passwordLength) } -func backgroundRequest(https, host string, user *models.PWResetUser) error { +func backgroundRequest(host string, user *models.PWResetUser) error { if user.User == "" { return errInvalidUser @@ -232,16 +222,6 @@ return err } - if err := conn.QueryRowContext( - ctx, countRequestsUserSQL, user.User).Scan(&count); err != nil { - return err - } - - // Limit requests per user - if count >= maxPasswordRequestsPerUser { - return errTooMuchPasswordResetsPerUser - } - hash = generateHash() _, err = conn.ExecContext(ctx, insertRequestSQL, hash, user.User) return err @@ -250,20 +230,11 @@ return err } - body := requestMessageBody(https, user.User, hash, host) + body := requestMessageBody(user.User, hash, host) return misc.SendMail(email, "Password Reset Link", body) } -// host checks if we are behind a proxy and returns the name -// of the up-front server. -func host(req *http.Request) string { - if fwd := req.Header.Get("X-Forwarded-Host"); fwd != "" { - return fwd - } - return req.Host -} - func passwordResetRequest( input interface{}, req *http.Request, @@ -272,11 +243,13 @@ // We do the checks and the emailing in background // no reduce the risks of timing attacks. - go func(https, host string, user *models.PWResetUser) { - if err := backgroundRequest(https, host, user); err != nil { + go func(user *models.PWResetUser) { + config.WaitReady() + host := config.ExternalURL() + if err := backgroundRequest(host, user); err != nil { log.Printf("error: %v\n", err) } - }(useHTTPS(req), host(req), input.(*models.PWResetUser)) + }(input.(*models.PWResetUser)) // Send a neutral message to avoid being an user oracle. const neutralMessage = "If this account exists, a reset link will be mailed." @@ -296,42 +269,65 @@ return } - var email, user, password string + var user, password string ctx := req.Context() - if err := auth.RunAs( - ctx, pwResetRole, func(conn *sql.Conn) error { - err := conn.QueryRowContext(ctx, findRequestSQL, hash).Scan(&email, &user) + err := auth.RunAs( + ctx, pwResetRole, + func(conn *sql.Conn) error { + tx, err := conn.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + err = tx.QueryRowContext(ctx, findRequestSQL, hash).Scan(&user) switch { case err == sql.ErrNoRows: - return JSONError{http.StatusNotFound, "No such hash"} + return errors.New("this URL is no longer valid") case err != nil: return err } password = generateNewPassword() - res, err := conn.ExecContext(ctx, updatePasswordSQL, password, user) + res, err := tx.ExecContext(ctx, updatePasswordSQL, password, user) if err != nil { return err } if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { - return JSONError{http.StatusNotFound, "User not found"} + return errors.New("user not found") + } + if _, err = tx.ExecContext(ctx, deleteRequestSQL, hash); err != nil { + return err } - _, err = conn.ExecContext(ctx, deleteRequestSQL, hash) - return err - }); err == nil { - https := useHTTPS(req) - server := host(req) - body := changedMessageBody(https, user, password, server) - if err = misc.SendMail(email, "Password Reset Done", body); err != nil { - log.Printf("error: %v\n", err) - http.Error( - rw, - http.StatusText(http.StatusInternalServerError), - http.StatusInternalServerError) - return - } - var url = https + "://" + server - http.Redirect(rw, req, url, http.StatusSeeOther) + return tx.Commit() + }, + ) + + switch { + case err == sql.ErrNoRows: + http.Error(rw, "No such request", http.StatusNotFound) + return + case err != nil: + http.Error(rw, "Error: "+err.Error(), http.StatusInternalServerError) + return + } + + if err := changedMessageBody(rw, user, password); err != nil { + log.Printf("error: %v\n", err) } } + +func deletePasswordResetRequest(user string) { + ctx := context.Background() + if err := auth.RunAs( + ctx, + pwResetRole, + func(conn *sql.Conn) error { + _, err := conn.ExecContext(ctx, deletePasswordResetRequestSQL, user) + return err + }, + ); err != nil { + log.Printf("error: %v\n", err) + } +} diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/search.go --- a/pkg/controllers/search.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/search.go Wed Jul 17 13:00:19 2019 +0200 @@ -17,7 +17,6 @@ import ( "database/sql" "net/http" - "regexp" "strings" "gemma.intevation.de/gemma/pkg/models" @@ -39,10 +38,6 @@ ` ) -var rkmRegex = regexp.MustCompile( - "^[[:space:]]*([0-9]+)([,.]([0-9]))?[[:space:]]*$", -) - func searchFeature( input interface{}, req *http.Request, diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/srimports.go --- a/pkg/controllers/srimports.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/srimports.go Wed Jul 17 13:00:19 2019 +0200 @@ -97,6 +97,19 @@ sr.Bottleneck = &v } + if v := req.FormValue("negate-z"); v != "" { + var negateZ bool + switch strings.ToLower(v) { + case "true", "1": + negateZ = true + case "false", "0": + negateZ = false + default: + return fmt.Errorf("unknown negate-z '%s'", v) + } + sr.NegateZ = &negateZ + } + if v := req.FormValue("single-beam"); v != "" { var singleBeam bool switch strings.ToLower(v) { @@ -105,7 +118,7 @@ case "false", "0", "multibeam", "multi-beam": singleBeam = false default: - return fmt.Errorf("Unknown single-beam '%s'", v) + return fmt.Errorf("unknown single-beam '%s'", v) } sr.SingleBeam = &singleBeam } diff -r ea4e1ea04e44 -r e0751da6272c pkg/controllers/token.go --- a/pkg/controllers/token.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/controllers/token.go Wed Jul 17 13:00:19 2019 +0200 @@ -102,5 +102,7 @@ Roles: session.Roles, } + go deletePasswordResetRequest(session.User) + SendJSON(rw, http.StatusCreated, &result) } diff -r ea4e1ea04e44 -r e0751da6272c pkg/imports/sr.go --- a/pkg/imports/sr.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/imports/sr.go Wed Jul 17 13:00:19 2019 +0200 @@ -60,6 +60,9 @@ DepthReference *string `json:"depth-reference,omitempty"` // SingleBeam indicates that the sounding is a single beam scan. SingleBeam *bool `json:"single-beam,omitempty"` + // NegateZ indicated that the Z values of thy XYZ input should be + // multiplied by -1. + NegateZ *bool `json:"negate-z,omitempty"` } const ( @@ -138,9 +141,6 @@ ST_AsBinary(ST_Transform(area::geometry, best_utm(area))) ` - reprojectPointsSQL = ` -SELECT ST_AsBinary(ST_Transform(ST_GeomFromWKB($1, $2::integer), $3::integer))` - reprojectPointsBufferedSQL = ` SELECT ST_AsBinary(ST_Transform(ST_GeomFromWKB($1, $2::integer), $3::integer)), @@ -206,10 +206,14 @@ ` ) -func (sr *SoundingResult) isSingleBeam() bool { +func (sr *SoundingResult) singleBeam() bool { return sr.SingleBeam != nil && *sr.SingleBeam } +func (sr *SoundingResult) negateZ() bool { + return sr.NegateZ != nil && *sr.NegateZ +} + // Do executes the actual sounding result import. func (sr *SoundingResult) Do( ctx context.Context, @@ -249,6 +253,12 @@ var xform vertexTransform + if sr.negateZ() { + xform = negateZTransform + } else { + xform = identityTransform + } + if m.DepthReference == "ZPG" { feedback.Info("Found ZPG as reference system -> translating Z values to LDC") var ldc float64 @@ -270,9 +280,11 @@ // LDC is cm. The data is in m. ldc /= 100 - xform = func(v octree.Vertex) octree.Vertex { - return octree.Vertex{X: v.X, Y: v.Y, Z: v.Z + ldc} - } + xform = chainTransforms( + xform, + func(v octree.Vertex) octree.Vertex { + return octree.Vertex{X: v.X, Y: v.Y, Z: v.Z + ldc} + }) m.DepthReference = depthReference } @@ -319,7 +331,7 @@ var summary interface{} - if sr.isSingleBeam() { + if sr.singleBeam() { summary, err = sr.processScan( ctx, tx, @@ -541,8 +553,6 @@ feedback.Info("Number triangles: %d.", len(tri.Triangles)/3) feedback.Info("Clipping triangles from new mesh.") - } else { // multi beam - // Nothing special } start = time.Now() @@ -655,7 +665,8 @@ return sr.Bottleneck != nil && sr.Date != nil && sr.DepthReference != nil && - sr.SingleBeam != nil + sr.SingleBeam != nil && + sr.NegateZ != nil } func (sr *SoundingResult) loadMeta(f *zip.File) (*models.SoundingResultMeta, error) { @@ -671,6 +682,8 @@ Bottleneck: *sr.Bottleneck, EPSG: epsg, DepthReference: *sr.DepthReference, + SingleBeam: sr.singleBeam(), + NegateZ: sr.negateZ(), }, nil } r, err := f.Open() @@ -699,18 +712,29 @@ if sr.SingleBeam != nil { m.SingleBeam = *sr.SingleBeam } + if sr.NegateZ != nil { + m.NegateZ = *sr.NegateZ + } return &m, nil } type vertexTransform func(octree.Vertex) octree.Vertex +func identityTransform(v octree.Vertex) octree.Vertex { return v } + +func negateZTransform(v octree.Vertex) octree.Vertex { + return octree.Vertex{X: v.X, Y: v.Y, Z: -v.Z} +} + +func chainTransforms(a, b vertexTransform) vertexTransform { + return func(v octree.Vertex) octree.Vertex { return b(a(v)) } +} + func loadXYZReader(r io.Reader, feedback Feedback, xform vertexTransform) (octree.MultiPointZ, error) { mpz := make(octree.MultiPointZ, 0, 250000) s := bufio.NewScanner(r) - var hasNegZ bool - warnLimiter := misc.WarningLimiter{Log: feedback.Warn, MaxWarnings: 100} warn := warnLimiter.Warn defer warnLimiter.Close() @@ -743,16 +767,7 @@ warn("format error in line %d: %v", line, err) continue } - if p.Z < 0 { - p.Z = -p.Z - if !hasNegZ { - hasNegZ = true - warn("Negative Z value found: Using -Z") - } - } - if xform != nil { - p = xform(p) - } + p = xform(p) mpz = append(mpz, p) } @@ -844,7 +859,12 @@ } } else { heights = octree.ExtrapolateClassBreaks(heights, tree.Min.Z, tree.Max.Z) - heights = octree.InBetweenClassBreaks(heights, 0.05, 2) + // We set steps for InBetweenClassBreaks to 1, so it + // becomes a null operation. The extra class breaks + // were considered unexpected and confusing by the + // users. Once we get filled polygones the visual will + // be considerably different anyway. -- sw + // heights = octree.InBetweenClassBreaks(heights, 0.05, 1) } /* diff -r ea4e1ea04e44 -r e0751da6272c pkg/models/sr.go --- a/pkg/models/sr.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/models/sr.go Wed Jul 17 13:00:19 2019 +0200 @@ -32,6 +32,7 @@ EPSG uint `json:"epsg"` DepthReference string `json:"depth-reference"` SingleBeam bool `json:"single-beam"` + NegateZ bool `json:"negate-z,omitempty"` } ) diff -r ea4e1ea04e44 -r e0751da6272c pkg/octree/builder.go --- a/pkg/octree/builder.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/octree/builder.go Wed Jul 17 13:00:19 2019 +0200 @@ -37,7 +37,7 @@ type buildStep func(chan buildStep) -var cubes = [8][2]Vertex{ +var cubes = [8]Box{ makeCube(0), makeCube(1), makeCube(2), @@ -48,7 +48,7 @@ makeCube(7), } -func makeCube(i int) [2]Vertex { +func makeCube(i int) Box { var d Vertex if i&1 == 1 { d.X = 0.5 @@ -59,12 +59,19 @@ if i&4 == 4 { d.Z = 0.5 } - return [2]Vertex{ + return Box{ Vertex{0.0, 0.0, 0.0}.Add(d), Vertex{0.5, 0.5, 0.5}.Add(d), } } +func twoElseOne(b bool) int { + if b { + return 2 + } + return 1 +} + // NewBuilder creates a new Builder for a TIN. func NewBuilder(t *Tin) *Builder { return &Builder{t: t} @@ -143,15 +150,28 @@ parent(tb.buildRecursive(triangles, min, max, depth)) return } + box := Box{min, max} - bbox := Interpolate(min, max) + xLimit := twoElseOne(box.HasX()) + yLimit := twoElseOne(box.HasY()) + zLimit := twoElseOne(box.HasZ()) - bboxes := make([][2]Vertex, len(cubes)) + indices := make([]byte, 0, 8) + + bbox := box.Interpolate() - for i := range cubes { - bboxes[i] = [2]Vertex{ - bbox(cubes[i][0]), - bbox(cubes[i][1]), + var bboxes [8]Box + + for x := 0; x < xLimit; x++ { + for y := 0; y < yLimit; y++ { + for z := 0; z < zLimit; z++ { + idx := byte(z<<2 | y<<1 | x) + bboxes[idx] = Box{ + bbox(cubes[idx][0]), + bbox(cubes[idx][1]), + } + indices = append(indices, idx) + } } } @@ -171,7 +191,7 @@ h.Maximize(v1) h.Maximize(v2) - for i := range bboxes { + for _, i := range indices { if !(h.Less(bboxes[i][0]) || bboxes[i][1].Less(l)) { quandrants[i] = append(quandrants[i], tri) } @@ -179,7 +199,7 @@ } used := new(int32) - for i := range quandrants { + for _, i := range indices { if len(quandrants[i]) > 0 { *used++ } @@ -187,7 +207,12 @@ pos := tb.allocNode() - for i := range quandrants { + if *used == 0 { + parent(pos) + return + } + + for _, i := range indices { if len(quandrants[i]) > 0 { j := int32(i) parent := func(v int32) { @@ -239,14 +264,28 @@ return int32(-(pos + 1)) } - bbox := Interpolate(min, max) + box := Box{min, max} - bboxes := make([][2]Vertex, len(cubes)) + xLimit := twoElseOne(box.HasX()) + yLimit := twoElseOne(box.HasY()) + zLimit := twoElseOne(box.HasZ()) + + indices := make([]byte, 0, 8) + + bbox := box.Interpolate() - for i := range cubes { - bboxes[i] = [2]Vertex{ - bbox(cubes[i][0]), - bbox(cubes[i][1]), + var bboxes [8]Box + + for x := 0; x < xLimit; x++ { + for y := 0; y < yLimit; y++ { + for z := 0; z < zLimit; z++ { + idx := byte(z<<2 | y<<1 | x) + bboxes[idx] = Box{ + bbox(cubes[idx][0]), + bbox(cubes[idx][1]), + } + indices = append(indices, idx) + } } } @@ -266,7 +305,7 @@ h.Maximize(v1) h.Maximize(v2) - for i := range bboxes { + for _, i := range indices { if !(h.Less(bboxes[i][0]) || bboxes[i][1].Less(l)) { quandrants[i] = append(quandrants[i], tri) } @@ -275,7 +314,7 @@ pos := tb.allocNode() - for i := range quandrants { + for _, i := range indices { if len(quandrants[i]) > 0 { child := tb.buildRecursive( quandrants[i], diff -r ea4e1ea04e44 -r e0751da6272c pkg/octree/vertex.go --- a/pkg/octree/vertex.go Fri Jul 12 16:18:49 2019 +0200 +++ b/pkg/octree/vertex.go Wed Jul 17 13:00:19 2019 +0200 @@ -45,6 +45,9 @@ // and the second being the direction. Line [2]Vertex + // Box is a 3D box. + Box [2]Vertex + // MultiPointZ is a set of vertices. MultiPointZ []Vertex @@ -288,9 +291,10 @@ } } -// Interpolate returns a function that return s*v2 + v1 +// Interpolate returns a function that return s*b[1] + b[0] // component-wise. -func Interpolate(v1, v2 Vertex) func(Vertex) Vertex { +func (b Box) Interpolate() func(Vertex) Vertex { + v1, v2 := b[0], b[1] v2 = v2.Sub(v1) return func(s Vertex) Vertex { return Vertex{ @@ -301,6 +305,10 @@ } } +func (b Box) HasX() bool { return math.Abs(b[0].X-b[1].X) > epsPlane } +func (b Box) HasY() bool { return math.Abs(b[0].Y-b[1].Y) > epsPlane } +func (b Box) HasZ() bool { return math.Abs(b[0].Z-b[1].Z) > epsPlane } + // Less returns if one of v component is less than the // corresponing component in w. func (v Vertex) Less(w Vertex) bool { diff -r ea4e1ea04e44 -r e0751da6272c schema/auth.sql --- a/schema/auth.sql Fri Jul 12 16:18:49 2019 +0200 +++ b/schema/auth.sql Wed Jul 17 13:00:19 2019 +0200 @@ -66,7 +66,7 @@ GRANT INSERT, UPDATE ON sys_admin.system_config TO sys_admin; GRANT UPDATE ON systemconf.feature_colours TO sys_admin; GRANT UPDATE ON sys_admin.published_services TO sys_admin; -GRANT INSERT, DELETE ON sys_admin.password_reset_requests TO sys_admin; +GRANT INSERT, DELETE, UPDATE ON sys_admin.password_reset_requests TO sys_admin; -- -- Privileges assigned directly to metamorph diff -r ea4e1ea04e44 -r e0751da6272c schema/gemma.sql --- a/schema/gemma.sql Fri Jul 12 16:18:49 2019 +0200 +++ b/schema/gemma.sql Wed Jul 17 13:00:19 2019 +0200 @@ -206,6 +206,23 @@ -- +-- GEMA meta data +-- +CREATE TABLE gemma_schema_version ( + version int PRIMARY KEY, + update_date timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE OR REPLACE FUNCTION get_schema_version() RETURNS int + LANGUAGE plpgsql + AS $$ + BEGIN + RETURN ( SELECT max(version) FROM gemma_schema_version ); + END; +$$; + + +-- -- GEMMA data -- @@ -232,7 +249,7 @@ CREATE TABLE password_reset_requests ( hash varchar(32) PRIMARY KEY, issued timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - username varchar NOT NULL + username varchar NOT NULL UNIQUE REFERENCES internal.user_profiles(username) ON DELETE CASCADE ON UPDATE CASCADE ) diff -r ea4e1ea04e44 -r e0751da6272c schema/update-db.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/update-db.sh Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,109 @@ +#!/bin/bash -e +# This is Free Software under GNU Affero General Public License v >= 3.0 +# without warranty, see README.md and license for details. +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# License-Filename: LICENSES/AGPL-3.0.txt +# +# Copyright (C) 2019 by via donau +# – Österreichische Wasserstraßen-Gesellschaft mbH +# Software engineering by Intevation GmbH +# +# Author(s): +# * Sascha Wilde + +ME=`basename "$0"` +BASEDIR=`dirname "$0"` + +usage() +{ + cat <&2 "$1" + exit 23 +} + +genpw() +# $1 - length +{ + PW='' + until [ "$(grep '[^[:alnum:]]' <<<$PW)" -a "$(grep '[[:digit:]]' <<<$PW)" ] + do + PW=$(dd count=1 if=/dev/urandom 2>/dev/null \ + | tr -cd '[:alnum:],._!?-' | tail -c "$1") + done + echo "$PW" +} + +# Defaults: + +db=gemma +port=5432 + +# Parse options: + +OPTS=`getopt \ + -l help,db:,port: \ + -o d:p: -n "$ME" -- "$@"` +[ $? -eq 0 ] || { usage ; exit 1 ; } + +eval set -- "$OPTS" + +while true ; do + case "$1" in + --db|-d) + db="$2" + shift 2 + ;; + --port|-p) + port="$2" + shift 2 + ;; + --help) + { usage ; exit 0 ; } + ;; + --) + shift + break + ;; + esac +done + + +get_version() +{ + local ver + if ver=$( psql -qtA -p "$port" -d "$db" \ + -c 'SELECT get_schema_version()' 2>/dev/null ) + then + echo $ver + else + echo '-1' + fi +} + +# Main ------------------------------------------------------------ + +current_ver=$( get_version ) + +for d in $BASEDIR/updates/* ; do + new_ver=$( basename $d ) + if [ -d "$d" -a "$new_ver" -gt $current_ver ] ; then + echo "Running updates for $new_ver ..." + + sql=$( cat `echo "$d/"* | sort -n` ) + psql -1 -q -p "$port" -d "$db" -c "$sql" + fi +done diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/0000/01.add_schema_version.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/0000/01.add_schema_version.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,12 @@ +CREATE TABLE gemma_schema_version ( + version int PRIMARY KEY, + update_date timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE OR REPLACE FUNCTION get_schema_version() RETURNS int + LANGUAGE plpgsql + AS $$ + BEGIN + RETURN ( SELECT max(version) FROM gemma_schema_version ); + END; +$$; diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/0000/99.set_version.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/0000/99.set_version.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,1 @@ +INSERT INTO gemma_schema_version(version) VALUES (0); diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/0301/01.dismar-wwname.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/0301/01.dismar-wwname.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,2 @@ +ALTER TABLE waterway.distance_marks_virtual + ADD COLUMN IF NOT EXISTS wwname varchar; diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/0301/02.search_functions.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/0301/02.search_functions.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,170 @@ +-- This is Free Software under GNU Affero General Public License v >= 3.0 +-- without warranty, see README.md and license for details. + +-- SPDX-License-Identifier: AGPL-3.0-or-later +-- License-Filename: LICENSES/AGPL-3.0.txt + +-- Copyright (C) 2018,2019 by via donau +-- – Österreichische Wasserstraßen-Gesellschaft mbH +-- Software engineering by Intevation GmbH + +-- Author(s): +-- * Sascha Wilde + +CREATE OR REPLACE FUNCTION search_hectometre(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +DECLARE + _result jsonb; +BEGIN + IF search_string SIMILAR TO '[0-9]+' THEN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT (location_code).hectometre || ' rhm' AS name, + isrs_asText(location_code) AS locationcode, + wwname AS location, + ST_AsGeoJSON(geom)::json AS geom, 'rhm' AS type + FROM waterway.distance_marks_virtual + WHERE (location_code).hectometre = search_string::int) r; + ELSIF is_ISRSstring(search_string) THEN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT (location_code).hectometre || ' rhm' AS name, + isrs_asText(location_code) AS locationcode, + wwname AS location, + ST_AsGeoJSON(geom)::json AS geom, 'rhm' AS type + FROM waterway.distance_marks_virtual + WHERE location_code = isrs_fromText(search_string)) r; + ELSE + _result='[]'; + END IF; + RETURN _result; +END; +$$; + +CREATE OR REPLACE FUNCTION search_bottlenecks(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +DECLARE + _result jsonb; +BEGIN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT objnam AS name, + ST_AsGeoJSON(ST_Envelope(area::geometry))::json AS geom, + 'bottleneck' AS type + FROM waterway.bottlenecks + WHERE objnam ILIKE '%' || search_string || '%' + ORDER BY name) r; + RETURN _result; +END; +$$; + +CREATE OR REPLACE FUNCTION search_cities(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +DECLARE + _result jsonb; +BEGIN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT name, + country_code AS location, + ST_AsGeoJSON(location)::json AS geom, + 'city' AS type + FROM waterway.geonames + WHERE feature_code IN ('PPLA', 'PPLA1', 'PPLA2', 'PPLA3', 'PPLC') + AND (name ILIKE '%' || search_string || '%' + OR alternatenames ~* ('(^|,)' || search_string || '($|,)')) + ORDER BY array_position(ARRAY['PPLC', 'PPLA', 'PPLA1', 'PPLA2', 'PPLA3'], + feature_code::text), + name) r; + RETURN _result; +END; +$$; + +CREATE OR REPLACE FUNCTION search_gauges(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +DECLARE + _result jsonb; +BEGIN + IF is_ISRSstring(search_string) THEN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM ( + SELECT objname AS name, + ST_AsGeoJSON(geom)::json AS geom, + 'gauge' AS type, + isrs_asText(location) AS locationcode + FROM waterway.gauges + WHERE NOT erased AND location = isrs_fromText(search_string) + ORDER BY name) r; + ELSE + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM ( + SELECT objname AS name, + ST_AsGeoJSON(geom)::json AS geom, + 'gauge' AS type, + isrs_asText(location) AS locationcode + FROM waterway.gauges + WHERE NOT erased AND objname ILIKE '%' || search_string || '%' + ORDER BY name) r; + END IF; + RETURN _result; +END; +$$; + +CREATE OR REPLACE FUNCTION search_sections(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +DECLARE + _result jsonb; +BEGIN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT id, + objnam AS name, + ST_AsGeoJSON(ST_Envelope(area::geometry))::json AS geom, + 'section' AS type + FROM waterway.sections + WHERE objnam ILIKE '%' || search_string || '%' + OR nobjnam ILIKE '%' || search_string || '%' + ORDER BY name) r; + RETURN _result; +END; +$$; + +CREATE OR REPLACE FUNCTION search_stretches(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +DECLARE + _result jsonb; +BEGIN + SELECT COALESCE(json_agg(r),'[]') + INTO _result + FROM (SELECT id, + objnam AS name, + ST_AsGeoJSON(ST_Envelope(area::geometry))::json AS geom, + 'stretch' AS type + FROM waterway.stretches + WHERE objnam ILIKE '%' || search_string || '%' + OR nobjnam ILIKE '%' || search_string || '%' + ORDER BY name) r; + RETURN _result; +END; +$$; + +CREATE OR REPLACE FUNCTION search_most(search_string text) RETURNS jsonb + LANGUAGE plpgsql STABLE PARALLEL SAFE + AS $$ +BEGIN + RETURN search_hectometre(search_string) + || search_bottlenecks(search_string) + || search_gauges(search_string) + || search_sections(search_string) + || search_stretches(search_string) + || search_cities(search_string); +END; +$$; diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/0301/99.set_version.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/0301/99.set_version.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,1 @@ +INSERT INTO gemma_schema_version(version) VALUES (301); diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/1000/01.pwreset.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1000/01.pwreset.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,4 @@ +GRANT INSERT, DELETE, UPDATE ON sys_admin.password_reset_requests TO sys_admin; + +ALTER TABLE sys_admin.password_reset_requests + ADD CONSTRAINT password_reset_requests_username_key UNIQUE(username); diff -r ea4e1ea04e44 -r e0751da6272c schema/updates/1000/99.set_version.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/schema/updates/1000/99.set_version.sql Wed Jul 17 13:00:19 2019 +0200 @@ -0,0 +1,1 @@ +INSERT INTO gemma_schema_version(version) VALUES (1000); diff -r ea4e1ea04e44 -r e0751da6272c schema/updates_3.0.1/01.dismar-wwname.sql --- a/schema/updates_3.0.1/01.dismar-wwname.sql Fri Jul 12 16:18:49 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -ALTER TABLE waterway.distance_marks_virtual - ADD COLUMN IF NOT EXISTS wwname varchar; diff -r ea4e1ea04e44 -r e0751da6272c schema/updates_3.0.1/02.search_functions.sql --- a/schema/updates_3.0.1/02.search_functions.sql Fri Jul 12 16:18:49 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -../search_functions.sql \ No newline at end of file