diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..f3b1d03 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "go-devcontainer", + "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm" +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9496009 --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml index d1afbd4..f019983 100644 --- a/.gitea/workflows/docker.yml +++ b/.gitea/workflows/docker.yml @@ -1,36 +1,36 @@ -name: Build and Publish Docker Image - -on: [workflow_dispatch] - -jobs: - build-and-push: - strategy: - matrix: - arch: [amd64, arm64] - runs-on: ${{ matrix.arch }} - -#jobs: -# build-and-push: -# runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Gitea Container Registry - uses: docker/login-action@v3 - with: - registry: git.ztsw.de - username: ${{ secrets.CR_USERNAME }} - password: ${{ secrets.CR_PASSWORD }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/${{ matrix.arch }} +name: Build and Publish Docker Image + +on: [workflow_dispatch] + +jobs: + build-and-push: + strategy: + matrix: + arch: [amd64, arm64] + runs-on: ${{ matrix.arch }} + +#jobs: +# build-and-push: +# runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.ztsw.de + username: ${{ secrets.CR_USERNAME }} + password: ${{ secrets.CR_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/${{ matrix.arch }} tags: git.ztsw.de/pedan/nosori/nosori:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index 56f5d18..7b2f841 100644 --- a/.gitignore +++ b/.gitignore @@ -1,46 +1,46 @@ -# Go build artifacts -*.exe -*.exe~ -*.dll -*.so -*.dylib -*.test -*.out - -# Go modules cache -vendor/ - -# Dependency directories -# /go/ - -# IDE/editor files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -Thumbs.db - -# Environment files -.env - -# Docker -*.tar - -# Node/npm -node_modules/ -npm-debug.log -yarn-error.log - -# Custom tiles or generated map data -backend/custom_tiles - -# Gitea -#.gitea/workflows/*.log - -# Static build output -dist/ -build/ +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# Go modules cache +vendor/ + +# Dependency directories +# /go/ + +# IDE/editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +Thumbs.db + +# Environment files +.env + +# Docker +*.tar + +# Node/npm +node_modules/ +npm-debug.log +yarn-error.log + +# Custom tiles or generated map data +backend/custom_tiles + +# Gitea +#.gitea/workflows/*.log + +# Static build output +dist/ +build/ diff --git a/Dockerfile b/Dockerfile index 9e369ce..99f8069 100644 --- a/Dockerfile +++ b/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 -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 -CMD ["go", "run", "main.go"] \ No newline at end of file +# 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"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 3318a22..729032e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,18 @@ -MIT License - -Copyright (c) 2025 pedan - -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 -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 -following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. - -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 -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 -USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT License + +Copyright (c) 2025 pedan + +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 +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 +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +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 +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 +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index eeca776..47ee71d 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ Create a `.env` file in the project root: ``` VALHALLA_URL=http://valhalla:8002/route 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 @@ -59,6 +66,9 @@ services: #environment: # - VALHALLA_URL=http://10.200.0.15:8002/route # - PORT=8080 + # - NOMINATIM_URL=https://nominatim.openstreetmap.org + # - NOMINATIM_USER_AGENT=FreeMoto/1.0 (+https://fm.ztsw.de/) + # - LOG_LEVEL=debug valhalla-scripted: image: ghcr.io/valhalla/valhalla-scripted:latest ports: @@ -69,6 +79,17 @@ services: - 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 - **Map UI:** Edit `static/index.html` and `static/main.js` diff --git a/app/web/.env.example b/app/web/.env.example index fdb11d8..5f561d5 100644 --- a/app/web/.env.example +++ b/app/web/.env.example @@ -1,2 +1,2 @@ -VALHALLA_URL=http://10.200.0.15:8002/route +VALHALLA_URL=http://10.200.0.15:8002/route PORT=8080 \ No newline at end of file diff --git a/app/web/go.mod b/app/web/go.mod index d8348a3..ceee761 100644 --- a/app/web/go.mod +++ b/app/web/go.mod @@ -1,5 +1,5 @@ -module pedan/freemoto - -go 1.24.5 - -require github.com/joho/godotenv v1.5.1 +module pedan/freemoto + +go 1.24.5 + +require github.com/joho/godotenv v1.5.1 diff --git a/app/web/go.sum b/app/web/go.sum index d61b19e..cabd53b 100644 --- a/app/web/go.sum +++ b/app/web/go.sum @@ -1,2 +1,2 @@ -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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/app/web/main.go b/app/web/main.go index 9781394..f8391b3 100644 --- a/app/web/main.go +++ b/app/web/main.go @@ -1,53 +1,249 @@ package main import ( - "io" - "log" - "net/http" - "os" + "context" + "fmt" + "io" + "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() { - _ = godotenv.Load(".env") // Load .env file + _ = godotenv.Load(".env") // Load .env file - valhallaURL := os.Getenv("VALHALLA_URL") - if valhallaURL == "" { - valhallaURL = "http://10.200.0.15:8002/route" - } + valhallaURL := os.Getenv("VALHALLA_URL") + if valhallaURL == "" { + valhallaURL = "http://valhalla:8002/route" + } - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } + nominatimBase := os.Getenv("NOMINATIM_URL") + if nominatimBase == "" { + 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")))) - http.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { - proxyToValhalla(w, r, valhallaURL) - }) - http.HandleFunc("/", 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:]) - }) - log.Printf("Listening on :%s", port) - http.ListenAndServe(":"+port, nil) + currentLogLevel = parseLogLevel(os.Getenv("LOG_LEVEL")) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + 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) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + }))) + 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) { - req, _ := http.NewRequest("POST", valhallaURL, r.Body) - req.Header = r.Header + // Create outbound request with caller context and preserve headers + 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{} - resp, err := client.Do(req) - if err != nil { - http.Error(w, err.Error(), http.StatusBadGateway) - return - } - defer resp.Body.Close() - w.Header().Set("Content-Type", resp.Header.Get("Content-Type")) - io.Copy(w, resp.Body) + client := http.Client{Timeout: 15 * 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=valhalla error=%v", rid, err) + } else { + logf(levelError, "upstream=valhalla error=%v", err) + } + 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 } \ No newline at end of file diff --git a/app/web/static/geolocate.js b/app/web/static/geolocate.js index 6a24bce..c2f102f 100644 --- a/app/web/static/geolocate.js +++ b/app/web/static/geolocate.js @@ -1,6 +1,6 @@ document.addEventListener('DOMContentLoaded', function() { 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(data => { if (data && data.length > 0) { @@ -16,7 +16,7 @@ document.addEventListener('DOMContentLoaded', function() { navigator.geolocation.getCurrentPosition(function(position) { var lat = position.coords.latitude; 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(data => { document.getElementById(inputId).value = data.display_name || `${lat},${lon}`; @@ -54,7 +54,7 @@ document.addEventListener('DOMContentLoaded', function() { suggestionsBox.style.display = 'none'; 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(data => { suggestionsBox.innerHTML = ''; diff --git a/app/web/static/index.html b/app/web/static/index.html index 17a1c0a..2829463 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -1,171 +1,171 @@ - - - - FreeMoto Navigation - - - - - - - -
- - - - - - - + + + + FreeMoto Navigation + + + + + + + +
+ + + + + + + diff --git a/app/web/static/main.js b/app/web/static/main.js index e47126b..d56f1c7 100644 --- a/app/web/static/main.js +++ b/app/web/static/main.js @@ -1,37 +1,37 @@ -// Center on a default point -var map = L.map('map', { zoomControl: false }).setView([53.866237, 10.676289], 18); - -// Add OSM tiles -L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap' -}).addTo(map); - -var userIcon = L.icon({ - iconUrl: '/maps-arrow.svg', // Add a motorcycle icon to your static folder - iconSize: [40, 40] -}); - -// Get users location -if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(function(position) { - var lat = position.coords.latitude; - var lon = position.coords.longitude; - map.setView([lat, lon], 14); - L.marker([lat, lon], {icon: userIcon}).addTo(map).bindPopup('You are here!'); - }); -} - -// Custom Bootstrap zoom controls -document.addEventListener('DOMContentLoaded', function() { - var zoomInBtn = document.getElementById('zoomInBtn'); - var zoomOutBtn = document.getElementById('zoomOutBtn'); - if (zoomInBtn && zoomOutBtn) { - zoomInBtn.addEventListener('click', function() { - map.zoomIn(); - }); - zoomOutBtn.addEventListener('click', function() { - map.zoomOut(); - }); - } +// Center on a default point +var map = L.map('map', { zoomControl: false }).setView([53.866237, 10.676289], 18); + +// Add OSM tiles +L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap' +}).addTo(map); + +var userIcon = L.icon({ + iconUrl: '/maps-arrow.svg', // Add a motorcycle icon to your static folder + iconSize: [40, 40] +}); + +// Get users location +if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(position) { + var lat = position.coords.latitude; + var lon = position.coords.longitude; + map.setView([lat, lon], 14); + L.marker([lat, lon], {icon: userIcon}).addTo(map).bindPopup('You are here!'); + }); +} + +// Custom Bootstrap zoom controls +document.addEventListener('DOMContentLoaded', function() { + var zoomInBtn = document.getElementById('zoomInBtn'); + var zoomOutBtn = document.getElementById('zoomOutBtn'); + if (zoomInBtn && zoomOutBtn) { + zoomInBtn.addEventListener('click', function() { + map.zoomIn(); + }); + zoomOutBtn.addEventListener('click', function() { + map.zoomOut(); + }); + } }); \ No newline at end of file diff --git a/app/web/static/route.js b/app/web/static/route.js index 886dfea..8ceb9d6 100644 --- a/app/web/static/route.js +++ b/app/web/static/route.js @@ -10,26 +10,25 @@ document.addEventListener('DOMContentLoaded', function() { function calculateRoute() { if (points.length === 2) { - var options = { - "exclude_restrictions": true - }; + var moto = {}; + // Avoid highways -> lower highway usage weight if (avoidHighwaysCheckbox && avoidHighwaysCheckbox.checked) { - options.use_highways = 0; - } - if (useShortestCheckbox && useShortestCheckbox.checked) { - options.use_shortest = true; - } - if (avoidTollRoadsCheckbox && avoidTollRoadsCheckbox.checked) { - options.avoid_toll = true; + moto.use_highways = 0.0; // 0..1 (0 avoids, 1 prefers) } + // Avoid ferries -> lower ferry usage weight 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) { - 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 = { locations: [ @@ -37,8 +36,12 @@ document.addEventListener('DOMContentLoaded', function() { { lat: points[1].lat, lon: points[1].lng } ], 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', { method: 'POST', @@ -50,7 +53,7 @@ document.addEventListener('DOMContentLoaded', function() { var leg = data.trip && data.trip.legs && data.trip.legs[0]; var infoCard = document.getElementById('routeInfoCard'); 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 info = `Distance: ${distanceKm} km
Estimated Time: ${durationMin} min
@@ -201,7 +204,7 @@ document.addEventListener('DOMContentLoaded', function() { }); 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(data => { var input = document.getElementById(inputId); diff --git a/backend/compose.yml b/backend/compose.yml index 25d16ea..3660757 100644 --- a/backend/compose.yml +++ b/backend/compose.yml @@ -1,9 +1,9 @@ -services: - valhalla-scripted: - tty: true - container_name: valhalla - ports: - - 8002:8002 - volumes: - - $PWD/custom_files:/custom_files +services: + valhalla-scripted: + tty: true + container_name: valhalla + ports: + - 8002:8002 + volumes: + - $PWD/custom_files:/custom_files image: ghcr.io/valhalla/valhalla-scripted:latest \ No newline at end of file diff --git a/bin/freemoto-web b/bin/freemoto-web new file mode 100644 index 0000000..b2861e8 Binary files /dev/null and b/bin/freemoto-web differ