Updated backend/frontend
This commit is contained in:
4
.devcontainer/devcontainer.json
Normal file
4
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "go-devcontainer",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm"
|
||||||
|
}
|
||||||
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Ignore VCS and IDE files
|
||||||
|
.git/
|
||||||
|
.gitea/
|
||||||
|
.devcontainer/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS/editor junk
|
||||||
|
**/.DS_Store
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
|
**/*.tmp
|
||||||
|
|
||||||
|
# Environment and secrets
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# Logs and coverage
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Node (if any subprojects)
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Python caches (if any scripts)
|
||||||
|
**/__pycache__/
|
||||||
|
**/*.pyc
|
||||||
|
|
||||||
|
# Go vendor (if used)
|
||||||
|
vendor/
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
name: Build and Publish Docker Image
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [amd64, arm64]
|
arch: [amd64, arm64]
|
||||||
runs-on: ${{ matrix.arch }}
|
runs-on: ${{ matrix.arch }}
|
||||||
|
|
||||||
#jobs:
|
#jobs:
|
||||||
# build-and-push:
|
# build-and-push:
|
||||||
# runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Gitea Container Registry
|
- name: Log in to Gitea Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: git.ztsw.de
|
registry: git.ztsw.de
|
||||||
username: ${{ secrets.CR_USERNAME }}
|
username: ${{ secrets.CR_USERNAME }}
|
||||||
password: ${{ secrets.CR_PASSWORD }}
|
password: ${{ secrets.CR_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/${{ matrix.arch }}
|
platforms: linux/${{ matrix.arch }}
|
||||||
tags: git.ztsw.de/pedan/nosori/nosori:latest
|
tags: git.ztsw.de/pedan/nosori/nosori:latest
|
||||||
92
.gitignore
vendored
92
.gitignore
vendored
@@ -1,46 +1,46 @@
|
|||||||
# Go build artifacts
|
# Go build artifacts
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# Go modules cache
|
# Go modules cache
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
# /go/
|
# /go/
|
||||||
|
|
||||||
# IDE/editor files
|
# IDE/editor files
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
|
||||||
# OS generated files
|
# OS generated files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
*.tar
|
*.tar
|
||||||
|
|
||||||
# Node/npm
|
# Node/npm
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
# Custom tiles or generated map data
|
# Custom tiles or generated map data
|
||||||
backend/custom_tiles
|
backend/custom_tiles
|
||||||
|
|
||||||
# Gitea
|
# Gitea
|
||||||
#.gitea/workflows/*.log
|
#.gitea/workflows/*.log
|
||||||
|
|
||||||
# Static build output
|
# Static build output
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|||||||
40
Dockerfile
40
Dockerfile
@@ -1,11 +1,43 @@
|
|||||||
FROM golang:1.24-alpine
|
## Build stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Install build dependencies (none needed for static build) and enable static build
|
||||||
|
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64
|
||||||
|
|
||||||
|
# Cache go modules
|
||||||
|
COPY app/web/go.mod ./app/web/
|
||||||
|
RUN cd app/web && go mod download
|
||||||
|
|
||||||
|
# Copy source
|
||||||
|
COPY app/web/ ./app/web/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
RUN cd app/web && go build -o /out/freemoto-web ./
|
||||||
|
|
||||||
|
## Runtime stage
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
WORKDIR /app/web
|
WORKDIR /app/web
|
||||||
|
|
||||||
COPY app/web/ .
|
# Add CA certificates for any outbound HTTPS (future-proofing)
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata wget
|
||||||
|
|
||||||
RUN go mod tidy
|
# Copy binary and static files
|
||||||
|
COPY --from=builder /out/freemoto-web /app/web/freemoto-web
|
||||||
|
COPY app/web/static/ /app/web/static/
|
||||||
|
|
||||||
|
# Use non-root user
|
||||||
|
RUN adduser -S -D -H -h /nonexistent appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
ENV PORT=8080
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["go", "run", "main.go"]
|
# Simple healthcheck against /healthz
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:${PORT}/healthz || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/web/freemoto-web"]
|
||||||
36
LICENSE
36
LICENSE
@@ -1,18 +1,18 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 pedan
|
Copyright (c) 2025 pedan
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
following conditions:
|
following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
portions of the Software.
|
portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -24,6 +24,13 @@ Create a `.env` file in the project root:
|
|||||||
```
|
```
|
||||||
VALHALLA_URL=http://valhalla:8002/route
|
VALHALLA_URL=http://valhalla:8002/route
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
# Optional: Nominatim base URL (defaults to https://nominatim.openstreetmap.org)
|
||||||
|
NOMINATIM_URL=https://nominatim.openstreetmap.org
|
||||||
|
# Recommended: configure a descriptive User-Agent per Nominatim usage policy
|
||||||
|
# Example format: "AppName/Version (+contact-url-or-email)"
|
||||||
|
NOMINATIM_USER_AGENT=FreeMoto/1.0 (+https://fm.ztsw.de/)
|
||||||
|
# Optional: log level (debug, info, warn, error). Default: info
|
||||||
|
LOG_LEVEL=info
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Development
|
### Local Development
|
||||||
@@ -59,6 +66,9 @@ services:
|
|||||||
#environment:
|
#environment:
|
||||||
# - VALHALLA_URL=http://10.200.0.15:8002/route
|
# - VALHALLA_URL=http://10.200.0.15:8002/route
|
||||||
# - PORT=8080
|
# - PORT=8080
|
||||||
|
# - NOMINATIM_URL=https://nominatim.openstreetmap.org
|
||||||
|
# - NOMINATIM_USER_AGENT=FreeMoto/1.0 (+https://fm.ztsw.de/)
|
||||||
|
# - LOG_LEVEL=debug
|
||||||
valhalla-scripted:
|
valhalla-scripted:
|
||||||
image: ghcr.io/valhalla/valhalla-scripted:latest
|
image: ghcr.io/valhalla/valhalla-scripted:latest
|
||||||
ports:
|
ports:
|
||||||
@@ -69,6 +79,17 @@ services:
|
|||||||
- tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf
|
- tile_urls=https://download.geofabrik.de/europe/germany-latest.osm.pbf
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Notes on Nominatim
|
||||||
|
|
||||||
|
- Please follow the official usage policy for Nominatim. Provide a meaningful `NOMINATIM_USER_AGENT` that includes a contact URL or email. The default is `FreeMoto/1.0 (+https://fm.ztsw.de/)`.
|
||||||
|
- You can point `NOMINATIM_URL` to your own Nominatim instance or keep the default public endpoint.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
- Configure verbosity with `LOG_LEVEL`.
|
||||||
|
- Supported values: `debug`, `info` (default), `warn`, `error`.
|
||||||
|
- Incoming requests are logged at `info` level. Upstream success traces (Valhalla/Nominatim) are at `debug`. Errors are at `error`.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- **Map UI:** Edit `static/index.html` and `static/main.js`
|
- **Map UI:** Edit `static/index.html` and `static/main.js`
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
VALHALLA_URL=http://10.200.0.15:8002/route
|
VALHALLA_URL=http://10.200.0.15:8002/route
|
||||||
PORT=8080
|
PORT=8080
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
module pedan/freemoto
|
module pedan/freemoto
|
||||||
|
|
||||||
go 1.24.5
|
go 1.24.5
|
||||||
|
|
||||||
require github.com/joho/godotenv v1.5.1
|
require github.com/joho/godotenv v1.5.1
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
|||||||
272
app/web/main.go
272
app/web/main.go
@@ -1,53 +1,249 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"context"
|
||||||
"log"
|
"fmt"
|
||||||
"net/http"
|
"io"
|
||||||
"os"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ctxKey string
|
||||||
|
|
||||||
|
const ctxReqID ctxKey = "reqID"
|
||||||
|
|
||||||
|
// atomic counter for simple request IDs
|
||||||
|
var reqIDCounter uint64
|
||||||
|
|
||||||
|
// log levels
|
||||||
|
const (
|
||||||
|
levelDebug = iota
|
||||||
|
levelInfo
|
||||||
|
levelWarn
|
||||||
|
levelError
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentLogLevel = levelInfo
|
||||||
|
|
||||||
|
func parseLogLevel(s string) int {
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "debug":
|
||||||
|
return levelDebug
|
||||||
|
case "info", "":
|
||||||
|
return levelInfo
|
||||||
|
case "warn", "warning":
|
||||||
|
return levelWarn
|
||||||
|
case "error", "err":
|
||||||
|
return levelError
|
||||||
|
default:
|
||||||
|
return levelInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logf(level int, format string, args ...any) {
|
||||||
|
if level < currentLogLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefix := "INFO"
|
||||||
|
switch level {
|
||||||
|
case levelDebug:
|
||||||
|
prefix = "DEBUG"
|
||||||
|
case levelInfo:
|
||||||
|
prefix = "INFO"
|
||||||
|
case levelWarn:
|
||||||
|
prefix = "WARN"
|
||||||
|
case levelError:
|
||||||
|
prefix = "ERROR"
|
||||||
|
}
|
||||||
|
// Prepend the prefix as the first argument
|
||||||
|
log.Printf("[%s] "+format, append([]any{prefix}, args...)...)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
_ = godotenv.Load(".env") // Load .env file
|
_ = godotenv.Load(".env") // Load .env file
|
||||||
|
|
||||||
valhallaURL := os.Getenv("VALHALLA_URL")
|
valhallaURL := os.Getenv("VALHALLA_URL")
|
||||||
if valhallaURL == "" {
|
if valhallaURL == "" {
|
||||||
valhallaURL = "http://10.200.0.15:8002/route"
|
valhallaURL = "http://valhalla:8002/route"
|
||||||
}
|
}
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
nominatimBase := os.Getenv("NOMINATIM_URL")
|
||||||
if port == "" {
|
if nominatimBase == "" {
|
||||||
port = "8080"
|
nominatimBase = "https://nominatim.openstreetmap.org"
|
||||||
}
|
}
|
||||||
|
nominatimUA := os.Getenv("NOMINATIM_USER_AGENT")
|
||||||
|
if nominatimUA == "" {
|
||||||
|
nominatimUA = "FreeMoto/1.0 (+https://fm.ztsw.de/)"
|
||||||
|
}
|
||||||
|
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
currentLogLevel = parseLogLevel(os.Getenv("LOG_LEVEL"))
|
||||||
http.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
proxyToValhalla(w, r, valhallaURL)
|
port := os.Getenv("PORT")
|
||||||
})
|
if port == "" {
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
port = "8080"
|
||||||
if r.URL.Path == "/" {
|
}
|
||||||
http.ServeFile(w, r, "./static/index.html")
|
|
||||||
return
|
http.Handle("/static/", withLogging(http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))))
|
||||||
}
|
http.Handle("/healthz", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, r, "./static/"+r.URL.Path[1:])
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
_, _ = w.Write([]byte("ok"))
|
||||||
log.Printf("Listening on :%s", port)
|
})))
|
||||||
http.ListenAndServe(":"+port, nil)
|
http.Handle("/route", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
proxyToValhalla(w, r, valhallaURL)
|
||||||
|
})))
|
||||||
|
// Nominatim proxy endpoints
|
||||||
|
http.Handle("/geocode", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
proxyToNominatimGET(w, r, nominatimBase+"/search", nominatimUA)
|
||||||
|
})))
|
||||||
|
http.Handle("/reverse", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
proxyToNominatimGET(w, r, nominatimBase+"/reverse", nominatimUA)
|
||||||
|
})))
|
||||||
|
http.Handle("/", withLogging(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
http.ServeFile(w, r, "./static/index.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, "./static/"+r.URL.Path[1:])
|
||||||
|
})))
|
||||||
|
logf(levelInfo, "Listening on :%s", port)
|
||||||
|
http.ListenAndServe(":"+port, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyToValhalla(w http.ResponseWriter, r *http.Request, valhallaURL string) {
|
func proxyToValhalla(w http.ResponseWriter, r *http.Request, valhallaURL string) {
|
||||||
req, _ := http.NewRequest("POST", valhallaURL, r.Body)
|
// Create outbound request with caller context and preserve headers
|
||||||
req.Header = r.Header
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, valhallaURL, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header = r.Header.Clone()
|
||||||
|
|
||||||
client := http.Client{}
|
client := http.Client{Timeout: 15 * time.Second}
|
||||||
resp, err := client.Do(req)
|
start := time.Now()
|
||||||
if err != nil {
|
resp, err := client.Do(req)
|
||||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
if err != nil {
|
||||||
return
|
if rid, ok := r.Context().Value(ctxReqID).(string); ok {
|
||||||
}
|
logf(levelError, "rid=%s upstream=valhalla error=%v", rid, err)
|
||||||
defer resp.Body.Close()
|
} else {
|
||||||
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
|
logf(levelError, "upstream=valhalla error=%v", err)
|
||||||
io.Copy(w, resp.Body)
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
// Propagate relevant headers and status code
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
// Optionally filter headers; here we forward common ones
|
||||||
|
if key == "Content-Type" || key == "Content-Encoding" || key == "Cache-Control" || key == "Vary" {
|
||||||
|
for _, v := range values {
|
||||||
|
w.Header().Add(key, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
_, _ = io.Copy(w, resp.Body)
|
||||||
|
|
||||||
|
if rid, ok := r.Context().Value(ctxReqID).(string); ok {
|
||||||
|
logf(levelDebug, "rid=%s upstream=valhalla status=%d dur=%s", rid, resp.StatusCode, time.Since(start))
|
||||||
|
} else {
|
||||||
|
logf(levelDebug, "upstream=valhalla status=%d dur=%s", resp.StatusCode, time.Since(start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyToNominatimGET proxies GET requests to Nominatim endpoints (search/reverse),
|
||||||
|
// sets a descriptive User-Agent, and enforces a timeout.
|
||||||
|
func proxyToNominatimGET(w http.ResponseWriter, r *http.Request, target string, userAgent string) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Preserve original query parameters
|
||||||
|
url := target
|
||||||
|
if rq := r.URL.RawQuery; rq != "" {
|
||||||
|
url = url + "?" + rq
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set a proper User-Agent per Nominatim usage policy
|
||||||
|
req.Header.Set("User-Agent", userAgent)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
client := http.Client{Timeout: 10 * time.Second}
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if rid, ok := r.Context().Value(ctxReqID).(string); ok {
|
||||||
|
logf(levelError, "rid=%s upstream=nominatim url=%s error=%v", rid, url, err)
|
||||||
|
} else {
|
||||||
|
logf(levelError, "upstream=nominatim url=%s error=%v", url, err)
|
||||||
|
}
|
||||||
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Forward selected headers and status code
|
||||||
|
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||||
|
w.Header().Set("Content-Type", ct)
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
_, _ = io.Copy(w, resp.Body)
|
||||||
|
|
||||||
|
if rid, ok := r.Context().Value(ctxReqID).(string); ok {
|
||||||
|
logf(levelDebug, "rid=%s upstream=nominatim status=%d dur=%s", rid, resp.StatusCode, time.Since(start))
|
||||||
|
} else {
|
||||||
|
logf(levelDebug, "upstream=nominatim status=%d dur=%s", resp.StatusCode, time.Since(start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withLogging wraps handlers to add request ID, status, duration logging
|
||||||
|
func withLogging(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rid := fmt.Sprintf("%x", atomic.AddUint64(&reqIDCounter, 1))
|
||||||
|
ctx := context.WithValue(r.Context(), ctxReqID, rid)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
lrw := &loggingResponseWriter{ResponseWriter: w, status: 200}
|
||||||
|
start := time.Now()
|
||||||
|
next.ServeHTTP(lrw, r)
|
||||||
|
dur := time.Since(start)
|
||||||
|
|
||||||
|
logf(levelInfo, "rid=%s method=%s path=%s status=%d bytes=%d dur=%s ua=\"%s\" ip=%s",
|
||||||
|
rid, r.Method, r.URL.Path, lrw.status, lrw.bytes, dur, r.UserAgent(), clientIP(r))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggingResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
bytes int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
||||||
|
lrw.status = code
|
||||||
|
lrw.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
n, err := lrw.ResponseWriter.Write(b)
|
||||||
|
lrw.bytes += n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func clientIP(r *http.Request) string {
|
||||||
|
// honor X-Forwarded-For if present (first IP)
|
||||||
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||||
|
return xff
|
||||||
|
}
|
||||||
|
return r.RemoteAddr
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
function geocode(query, callback) {
|
function geocode(query, callback) {
|
||||||
fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(query))
|
fetch('/geocode?format=json&q=' + encodeURIComponent(query))
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
@@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
navigator.geolocation.getCurrentPosition(function(position) {
|
navigator.geolocation.getCurrentPosition(function(position) {
|
||||||
var lat = position.coords.latitude;
|
var lat = position.coords.latitude;
|
||||||
var lon = position.coords.longitude;
|
var lon = position.coords.longitude;
|
||||||
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`)
|
fetch(`/reverse?format=json&lat=${lat}&lon=${lon}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById(inputId).value = data.display_name || `${lat},${lon}`;
|
document.getElementById(inputId).value = data.display_name || `${lat},${lon}`;
|
||||||
@@ -54,7 +54,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
suggestionsBox.style.display = 'none';
|
suggestionsBox.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch('https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&q=' + encodeURIComponent(value))
|
fetch('/geocode?format=json&addressdetails=1&q=' + encodeURIComponent(value))
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
suggestionsBox.innerHTML = '';
|
suggestionsBox.innerHTML = '';
|
||||||
|
|||||||
@@ -1,171 +1,171 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>FreeMoto Navigation</title>
|
<title>FreeMoto Navigation</title>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||||
<style>
|
<style>
|
||||||
html, body, #map {
|
html, body, #map {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#map {
|
#map {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.nav-panel {
|
.nav-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
background: rgba(255,255,255,0.98);
|
background: rgba(255,255,255,0.98);
|
||||||
border-radius: 18px 18px 0 0;
|
border-radius: 18px 18px 0 0;
|
||||||
box-shadow: 0 -2px 16px rgba(0,0,0,0.18);
|
box-shadow: 0 -2px 16px rgba(0,0,0,0.18);
|
||||||
padding: 12px 8px 8px 8px;
|
padding: 12px 8px 8px 8px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
.nav-panel {
|
.nav-panel {
|
||||||
left: 24px;
|
left: 24px;
|
||||||
width: 370px;
|
width: 370px;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
top: 24px;
|
top: 24px;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
box-shadow: 0 2px 16px rgba(0,0,0,0.18);
|
box-shadow: 0 2px 16px rgba(0,0,0,0.18);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#routeInfoCard {
|
#routeInfoCard {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
#sourceSuggestions,
|
#sourceSuggestions,
|
||||||
#destSuggestions {
|
#destSuggestions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 220px;
|
max-height: 220px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-radius: 0 0 0.5rem 0.5rem;
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
}
|
}
|
||||||
.card-icon {
|
.card-icon {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.btn-lg, .form-control {
|
.btn-lg, .form-control {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
padding: 0.7rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
}
|
}
|
||||||
.form-check-label {
|
.form-check-label {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
.input-group-text svg,
|
.input-group-text svg,
|
||||||
.btn svg {
|
.btn svg {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div class="nav-panel shadow-lg">
|
<div class="nav-panel shadow-lg">
|
||||||
<div class="text-center mb-2">
|
<div class="text-center mb-2">
|
||||||
<span class="card-icon">🏍️</span>
|
<span class="card-icon">🏍️</span>
|
||||||
<span class="fs-4 fw-bold text-primary">FreeMoto</span>
|
<span class="fs-4 fw-bold text-primary">FreeMoto</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="routeInfoCard" class="alert alert-info d-none" role="alert"></div>
|
<div id="routeInfoCard" class="alert alert-info d-none" role="alert"></div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<div class="section-title">Route</div>
|
<div class="section-title">Route</div>
|
||||||
<div class="input-group mb-2 position-relative">
|
<div class="input-group mb-2 position-relative">
|
||||||
<span class="input-group-text" title="Start">
|
<span class="input-group-text" title="Start">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#0d6efd" class="bi bi-geo-alt" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 1 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#0d6efd" class="bi bi-geo-alt" viewBox="0 0 16 16"><path d="M8 16s6-5.686 6-10A6 6 0 1 0 2 6c0 4.314 6 10 6 10zm0-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" class="form-control" id="sourceInput" placeholder="Start address" autocomplete="off">
|
<input type="text" class="form-control" id="sourceInput" placeholder="Start address" autocomplete="off">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="useCurrentSource" title="Use current location">
|
<button class="btn btn-outline-secondary" type="button" id="useCurrentSource" title="Use current location">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="sourceSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
|
<div id="sourceSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group mb-2 position-relative">
|
<div class="input-group mb-2 position-relative">
|
||||||
<span class="input-group-text" title="Destination">
|
<span class="input-group-text" title="Destination">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#dc3545" class="bi bi-flag" viewBox="0 0 16 16"><path d="M14.778 2.222a.5.5 0 0 1 0 .707l-2.5 2.5a.5.5 0 0 1-.707 0l-2.5-2.5a.5.5 0 0 1 .707-.707L12 3.793l2.071-2.071a.5.5 0 0 1 .707 0z"/><path d="M2.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#dc3545" class="bi bi-flag" viewBox="0 0 16 16"><path d="M14.778 2.222a.5.5 0 0 1 0 .707l-2.5 2.5a.5.5 0 0 1-.707 0l-2.5-2.5a.5.5 0 0 1 .707-.707L12 3.793l2.071-2.071a.5.5 0 0 1 .707 0z"/><path d="M2.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5z"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" class="form-control" id="destInput" placeholder="Destination address" autocomplete="off">
|
<input type="text" class="form-control" id="destInput" placeholder="Destination address" autocomplete="off">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="useCurrentDest" title="Use current location">
|
<button class="btn btn-outline-secondary" type="button" id="useCurrentDest" title="Use current location">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#198754" class="bi bi-crosshair" viewBox="0 0 16 16"><path d="M8 15V1a7 7 0 1 1 0 14zm0-1a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M8 8.5a.5.5 0 0 1-.5-.5V2.707l-2.146 2.147a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V8a.5.5 0 0 1-.5.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="destSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
|
<div id="destSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 mb-2">
|
<div class="d-grid gap-2 mb-2">
|
||||||
<button type="button" id="plotRouteBtn" class="btn btn-success btn-lg">Plot Route</button>
|
<button type="button" id="plotRouteBtn" class="btn btn-success btn-lg">Plot Route</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-title">Route Options</div>
|
<div class="section-title">Route Options</div>
|
||||||
<form>
|
<form>
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="avoidHighways">
|
<input class="form-check-input" type="checkbox" id="avoidHighways">
|
||||||
<label class="form-check-label" for="avoidHighways">Avoid freeways</label>
|
<label class="form-check-label" for="avoidHighways">Avoid freeways</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="useShortest">
|
<input class="form-check-input" type="checkbox" id="useShortest">
|
||||||
<label class="form-check-label" for="useShortest">Shortest route</label>
|
<label class="form-check-label" for="useShortest">Shortest route</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="avoidTollRoads">
|
<input class="form-check-input" type="checkbox" id="avoidTollRoads">
|
||||||
<label class="form-check-label" for="avoidTollRoads">Avoid tolls</label>
|
<label class="form-check-label" for="avoidTollRoads">Avoid tolls</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="avoidFerries">
|
<input class="form-check-input" type="checkbox" id="avoidFerries">
|
||||||
<label class="form-check-label" for="avoidFerries">Avoid ferries</label>
|
<label class="form-check-label" for="avoidFerries">Avoid ferries</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="avoidUnpaved">
|
<input class="form-check-input" type="checkbox" id="avoidUnpaved">
|
||||||
<label class="form-check-label" for="avoidUnpaved">Avoid unpaved</label>
|
<label class="form-check-label" for="avoidUnpaved">Avoid unpaved</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div class="d-flex justify-content-between mt-3 mb-2">
|
<div class="d-flex justify-content-between mt-3 mb-2">
|
||||||
<button onclick="resetMarkers()" class="btn btn-primary btn-lg flex-fill me-2">Reset Points</button>
|
<button onclick="resetMarkers()" class="btn btn-primary btn-lg flex-fill me-2">Reset Points</button>
|
||||||
<div class="btn-group flex-fill" role="group" aria-label="Zoom controls">
|
<div class="btn-group flex-fill" role="group" aria-label="Zoom controls">
|
||||||
<button type="button" class="btn btn-outline-secondary btn-lg" id="zoomInBtn" title="Zoom in">+</button>
|
<button type="button" class="btn btn-outline-secondary btn-lg" id="zoomInBtn" title="Zoom in">+</button>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-lg" id="zoomOutBtn" title="Zoom out">−</button>
|
<button type="button" class="btn btn-outline-secondary btn-lg" id="zoomOutBtn" title="Zoom out">−</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 mb-2">
|
<div class="d-grid gap-2 mb-2">
|
||||||
<button type="button" id="exportGpxBtn" class="btn btn-warning btn-lg">Export GPX</button>
|
<button type="button" id="exportGpxBtn" class="btn btn-warning btn-lg">Export GPX</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
<script src="/main.js"></script>
|
<script src="/main.js"></script>
|
||||||
<script src="/route.js"></script>
|
<script src="/route.js"></script>
|
||||||
<script src="/geolocate.js"></script>
|
<script src="/geolocate.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
// Center on a default point
|
// Center on a default point
|
||||||
var map = L.map('map', { zoomControl: false }).setView([53.866237, 10.676289], 18);
|
var map = L.map('map', { zoomControl: false }).setView([53.866237, 10.676289], 18);
|
||||||
|
|
||||||
// Add OSM tiles
|
// Add OSM tiles
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
var userIcon = L.icon({
|
var userIcon = L.icon({
|
||||||
iconUrl: '/maps-arrow.svg', // Add a motorcycle icon to your static folder
|
iconUrl: '/maps-arrow.svg', // Add a motorcycle icon to your static folder
|
||||||
iconSize: [40, 40]
|
iconSize: [40, 40]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get users location
|
// Get users location
|
||||||
if (navigator.geolocation) {
|
if (navigator.geolocation) {
|
||||||
navigator.geolocation.getCurrentPosition(function(position) {
|
navigator.geolocation.getCurrentPosition(function(position) {
|
||||||
var lat = position.coords.latitude;
|
var lat = position.coords.latitude;
|
||||||
var lon = position.coords.longitude;
|
var lon = position.coords.longitude;
|
||||||
map.setView([lat, lon], 14);
|
map.setView([lat, lon], 14);
|
||||||
L.marker([lat, lon], {icon: userIcon}).addTo(map).bindPopup('You are here!');
|
L.marker([lat, lon], {icon: userIcon}).addTo(map).bindPopup('You are here!');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom Bootstrap zoom controls
|
// Custom Bootstrap zoom controls
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var zoomInBtn = document.getElementById('zoomInBtn');
|
var zoomInBtn = document.getElementById('zoomInBtn');
|
||||||
var zoomOutBtn = document.getElementById('zoomOutBtn');
|
var zoomOutBtn = document.getElementById('zoomOutBtn');
|
||||||
if (zoomInBtn && zoomOutBtn) {
|
if (zoomInBtn && zoomOutBtn) {
|
||||||
zoomInBtn.addEventListener('click', function() {
|
zoomInBtn.addEventListener('click', function() {
|
||||||
map.zoomIn();
|
map.zoomIn();
|
||||||
});
|
});
|
||||||
zoomOutBtn.addEventListener('click', function() {
|
zoomOutBtn.addEventListener('click', function() {
|
||||||
map.zoomOut();
|
map.zoomOut();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -10,26 +10,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
function calculateRoute() {
|
function calculateRoute() {
|
||||||
if (points.length === 2) {
|
if (points.length === 2) {
|
||||||
var options = {
|
var moto = {};
|
||||||
"exclude_restrictions": true
|
// Avoid highways -> lower highway usage weight
|
||||||
};
|
|
||||||
if (avoidHighwaysCheckbox && avoidHighwaysCheckbox.checked) {
|
if (avoidHighwaysCheckbox && avoidHighwaysCheckbox.checked) {
|
||||||
options.use_highways = 0;
|
moto.use_highways = 0.0; // 0..1 (0 avoids, 1 prefers)
|
||||||
}
|
|
||||||
if (useShortestCheckbox && useShortestCheckbox.checked) {
|
|
||||||
options.use_shortest = true;
|
|
||||||
}
|
|
||||||
if (avoidTollRoadsCheckbox && avoidTollRoadsCheckbox.checked) {
|
|
||||||
options.avoid_toll = true;
|
|
||||||
}
|
}
|
||||||
|
// Avoid ferries -> lower ferry usage weight
|
||||||
if (avoidFerriesCheckbox && avoidFerriesCheckbox.checked) {
|
if (avoidFerriesCheckbox && avoidFerriesCheckbox.checked) {
|
||||||
options.avoid_ferry = true;
|
moto.use_ferry = 0.0; // 0..1
|
||||||
}
|
}
|
||||||
|
// Avoid unpaved -> exclude unpaved roads entirely
|
||||||
if (avoidUnpavedCheckbox && avoidUnpavedCheckbox.checked) {
|
if (avoidUnpavedCheckbox && avoidUnpavedCheckbox.checked) {
|
||||||
options.avoid_unpaved = true;
|
moto.exclude_unpaved = true;
|
||||||
|
}
|
||||||
|
// Avoid tolls -> exclude tolls
|
||||||
|
if (avoidTollRoadsCheckbox && avoidTollRoadsCheckbox.checked) {
|
||||||
|
moto.exclude_tolls = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var costing_options = { motorcycle: options };
|
var costing_options = { motorcycle: moto };
|
||||||
|
|
||||||
var requestBody = {
|
var requestBody = {
|
||||||
locations: [
|
locations: [
|
||||||
@@ -37,8 +36,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
{ lat: points[1].lat, lon: points[1].lng }
|
{ lat: points[1].lat, lon: points[1].lng }
|
||||||
],
|
],
|
||||||
costing: "motorcycle",
|
costing: "motorcycle",
|
||||||
costing_options: costing_options
|
costing_options: costing_options,
|
||||||
|
units: "kilometers"
|
||||||
};
|
};
|
||||||
|
if (useShortestCheckbox && useShortestCheckbox.checked) {
|
||||||
|
requestBody.shortest = true; // top-level shortest flag
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/route', {
|
fetch('/route', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -50,7 +53,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
var leg = data.trip && data.trip.legs && data.trip.legs[0];
|
var leg = data.trip && data.trip.legs && data.trip.legs[0];
|
||||||
var infoCard = document.getElementById('routeInfoCard');
|
var infoCard = document.getElementById('routeInfoCard');
|
||||||
if (leg && leg.summary && typeof leg.summary.length === 'number' && typeof leg.summary.time === 'number') {
|
if (leg && leg.summary && typeof leg.summary.length === 'number' && typeof leg.summary.time === 'number') {
|
||||||
var distanceKm = (leg.summary.length / 1000).toFixed(1); // meters to km
|
var distanceKm = (leg.summary.length).toFixed(1); // already in km
|
||||||
var durationMin = Math.round(leg.summary.time / 60); // seconds to minutes
|
var durationMin = Math.round(leg.summary.time / 60); // seconds to minutes
|
||||||
var info = `<strong>Distance:</strong> ${distanceKm} km<br>
|
var info = `<strong>Distance:</strong> ${distanceKm} km<br>
|
||||||
<strong>Estimated Time:</strong> ${durationMin} min<br>
|
<strong>Estimated Time:</strong> ${durationMin} min<br>
|
||||||
@@ -201,7 +204,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function reverseGeocode(lat, lon, inputId) {
|
function reverseGeocode(lat, lon, inputId) {
|
||||||
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`)
|
fetch(`/reverse?format=json&lat=${lat}&lon=${lon}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
var input = document.getElementById(inputId);
|
var input = document.getElementById(inputId);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
valhalla-scripted:
|
valhalla-scripted:
|
||||||
tty: true
|
tty: true
|
||||||
container_name: valhalla
|
container_name: valhalla
|
||||||
ports:
|
ports:
|
||||||
- 8002:8002
|
- 8002:8002
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD/custom_files:/custom_files
|
- $PWD/custom_files:/custom_files
|
||||||
image: ghcr.io/valhalla/valhalla-scripted:latest
|
image: ghcr.io/valhalla/valhalla-scripted:latest
|
||||||
BIN
bin/freemoto-web
Normal file
BIN
bin/freemoto-web
Normal file
Binary file not shown.
Reference in New Issue
Block a user