First commit
This commit is contained in:
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +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/
|
||||||
2
app/web/.env.example
Normal file
2
app/web/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VALHALLA_URL=http://10.200.0.15:8002/route
|
||||||
|
PORT=8080
|
||||||
31
app/web/.gitea/workflows/docker.yml
Normal file
31
app/web/.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
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: gitea.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
|
||||||
|
tags: git.ztsw.de/pedan/freemoto/freemoto-web:latest
|
||||||
11
app/web/Dockerfile
Normal file
11
app/web/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM golang:1.22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN go mod tidy
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["go", "run", "main.go"]
|
||||||
7
app/web/docker-compose.yml
Normal file
7
app/web/docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
freemoto-web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
5
app/web/go.mod
Normal file
5
app/web/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module pedan/freemoto
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require github.com/joho/godotenv v1.5.1
|
||||||
2
app/web/go.sum
Normal file
2
app/web/go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
53
app/web/main.go
Normal file
53
app/web/main.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
_ = godotenv.Load(".env") // Load .env file
|
||||||
|
|
||||||
|
valhallaURL := os.Getenv("VALHALLA_URL")
|
||||||
|
if valhallaURL == "" {
|
||||||
|
valhallaURL = "http://10.200.0.15:8002/route"
|
||||||
|
}
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func proxyToValhalla(w http.ResponseWriter, r *http.Request, valhallaURL string) {
|
||||||
|
req, _ := http.NewRequest("POST", valhallaURL, r.Body)
|
||||||
|
req.Header = r.Header
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
139
app/web/static/geolocate.js
Normal file
139
app/web/static/geolocate.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
function geocode(query, callback) {
|
||||||
|
fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(query))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
callback(data[0]);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInputToCurrentLocation(inputId) {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
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}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById(inputId).value = data.display_name || `${lat},${lon}`;
|
||||||
|
document.getElementById(inputId).dataset.lat = lat;
|
||||||
|
document.getElementById(inputId).dataset.lon = lon;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuggestions(inputId, suggestionsId, value) {
|
||||||
|
var suggestionsBox = document.getElementById(suggestionsId);
|
||||||
|
if (!value) {
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
suggestionsBox.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(value))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
data.slice(0, 5).forEach(function(item) {
|
||||||
|
var option = document.createElement('button');
|
||||||
|
option.type = 'button';
|
||||||
|
option.className = 'list-group-item list-group-item-action';
|
||||||
|
option.textContent = item.display_name;
|
||||||
|
option.onclick = function() {
|
||||||
|
var input = document.getElementById(inputId);
|
||||||
|
input.value = item.display_name;
|
||||||
|
input.dataset.lat = item.lat;
|
||||||
|
input.dataset.lon = item.lon;
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
suggestionsBox.style.display = 'none';
|
||||||
|
};
|
||||||
|
suggestionsBox.appendChild(option);
|
||||||
|
});
|
||||||
|
suggestionsBox.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
suggestionsBox.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce helper
|
||||||
|
function debounce(fn, delay) {
|
||||||
|
let timer = null;
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInput(e) {
|
||||||
|
var val = e.target.value;
|
||||||
|
if (val) {
|
||||||
|
geocode(val, function(result) {
|
||||||
|
if (result) {
|
||||||
|
e.target.dataset.lat = result.lat;
|
||||||
|
e.target.dataset.lon = result.lon;
|
||||||
|
} else {
|
||||||
|
delete e.target.dataset.lat;
|
||||||
|
delete e.target.dataset.lon;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
delete e.target.dataset.lat;
|
||||||
|
delete e.target.dataset.lon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('sourceInput').addEventListener('input', debounce(function(e) {
|
||||||
|
showSuggestions('sourceInput', 'sourceSuggestions', e.target.value);
|
||||||
|
}, 400));
|
||||||
|
document.getElementById('destInput').addEventListener('input', debounce(function(e) {
|
||||||
|
showSuggestions('destInput', 'destSuggestions', e.target.value);
|
||||||
|
}, 400));
|
||||||
|
|
||||||
|
// Hide suggestions when input loses focus (with a slight delay for click)
|
||||||
|
document.getElementById('sourceInput').addEventListener('blur', function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('sourceSuggestions').style.display = 'none';
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
document.getElementById('destInput').addEventListener('blur', function() {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('destSuggestions').style.display = 'none';
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('useCurrentSource').onclick = function() {
|
||||||
|
setInputToCurrentLocation('sourceInput');
|
||||||
|
};
|
||||||
|
document.getElementById('useCurrentDest').onclick = function() {
|
||||||
|
setInputToCurrentLocation('destInput');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('sourceInput').addEventListener('blur', function(e) {
|
||||||
|
var val = e.target.value;
|
||||||
|
if (val && !e.target.dataset.lat) {
|
||||||
|
geocode(val, function(result) {
|
||||||
|
if (result) {
|
||||||
|
e.target.dataset.lat = result.lat;
|
||||||
|
e.target.dataset.lon = result.lon;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('destInput').addEventListener('blur', function(e) {
|
||||||
|
var val = e.target.value;
|
||||||
|
if (val && !e.target.dataset.lat) {
|
||||||
|
geocode(val, function(result) {
|
||||||
|
if (result) {
|
||||||
|
e.target.dataset.lat = result.lat;
|
||||||
|
e.target.dataset.lon = result.lon;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
126
app/web/static/index.html
Normal file
126
app/web/static/index.html
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>FreeMoto Web</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<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" />
|
||||||
|
<style>
|
||||||
|
html, body, #map {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-width: 100vw;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.controls-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
left: 24px;
|
||||||
|
z-index: 1001;
|
||||||
|
background: rgba(255,255,255,0.98);
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0,0,0,0.18);
|
||||||
|
padding: 28px 32px 22px 32px;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 370px;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution {
|
||||||
|
z-index: 1002;
|
||||||
|
}
|
||||||
|
.input-group-text {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
margin: 1.2rem 0;
|
||||||
|
}
|
||||||
|
#sourceSuggestions,
|
||||||
|
#destSuggestions {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2000;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 0 0 0.5rem 0.5rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<div class="controls-container shadow-lg">
|
||||||
|
<h4 class="mb-3 text-primary">FreeMoto Route Planner</h4>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="section-title">Route</div>
|
||||||
|
<div class="input-group mb-2 position-relative">
|
||||||
|
<span class="input-group-text" title="Start"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||||
|
<input type="text" class="form-control" id="sourceInput" placeholder="Enter start address" autocomplete="off">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="useCurrentSource" title="Use current location">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||||
|
<div id="sourceSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-2 position-relative">
|
||||||
|
<span class="input-group-text" title="Destination"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||||
|
<input type="text" class="form-control" id="destInput" placeholder="Enter destination address" autocomplete="off">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" id="useCurrentDest" title="Use current location">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||||
|
<div id="destSuggestions" class="list-group position-absolute w-100" style="z-index: 2000;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 mb-3">
|
||||||
|
<button type="button" id="plotRouteBtn" class="btn btn-success btn-sm">Plot Route</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="section-title">Route Options</div>
|
||||||
|
<form>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="avoidHighways">
|
||||||
|
<label class="form-check-label" for="avoidHighways">Avoid freeways</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="useShortest">
|
||||||
|
<label class="form-check-label" for="useShortest">Use shortest route</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="avoidTollRoads">
|
||||||
|
<label class="form-check-label" for="avoidTollRoads">Avoid toll roads</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="avoidFerries">
|
||||||
|
<label class="form-check-label" for="avoidFerries">Avoid ferries</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="avoidUnpaved">
|
||||||
|
<label class="form-check-label" for="avoidUnpaved">Avoid unpaved roads</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="d-grid gap-2 mb-3">
|
||||||
|
<button onclick="resetMarkers()" class="btn btn-primary btn-sm">Reset Points</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group mb-2 w-100" role="group" aria-label="Zoom controls">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="zoomInBtn" title="Zoom in">+</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="zoomOutBtn" title="Zoom out">−</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
<script src="/main.js"></script>
|
||||||
|
<script src="/route.js"></script>
|
||||||
|
<script src="/geolocate.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
37
app/web/static/main.js
Normal file
37
app/web/static/main.js
Normal file
@@ -0,0 +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: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}).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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
1
app/web/static/maps-arrow.svg
Normal file
1
app/web/static/maps-arrow.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M3.68478 18.7826L11.5642 4.77473C11.7554 4.43491 12.2446 4.43491 12.4358 4.77473L20.3152 18.7826C20.5454 19.1918 20.1357 19.6639 19.6982 19.4937L12.1812 16.5705C12.0647 16.5251 11.9353 16.5251 11.8188 16.5705L4.30179 19.4937C3.86426 19.6639 3.45463 19.1918 3.68478 18.7826Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 552 B |
171
app/web/static/route.js
Normal file
171
app/web/static/route.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var avoidHighwaysCheckbox = document.getElementById('avoidHighways');
|
||||||
|
var useShortestCheckbox = document.getElementById('useShortest');
|
||||||
|
var avoidTollRoadsCheckbox = document.getElementById('avoidTollRoads');
|
||||||
|
var avoidFerriesCheckbox = document.getElementById('avoidFerries');
|
||||||
|
var avoidUnpavedCheckbox = document.getElementById('avoidUnpaved');
|
||||||
|
var points = [];
|
||||||
|
var markers = [];
|
||||||
|
var routePolyline = null;
|
||||||
|
|
||||||
|
function calculateRoute() {
|
||||||
|
if (points.length === 2) {
|
||||||
|
var options = {
|
||||||
|
"exclude_restrictions": true
|
||||||
|
};
|
||||||
|
if (avoidHighwaysCheckbox && avoidHighwaysCheckbox.checked) {
|
||||||
|
options.use_highways = 0;
|
||||||
|
}
|
||||||
|
if (useShortestCheckbox && useShortestCheckbox.checked) {
|
||||||
|
options.use_shortest = true;
|
||||||
|
}
|
||||||
|
if (avoidTollRoadsCheckbox && avoidTollRoadsCheckbox.checked) {
|
||||||
|
options.avoid_toll = true;
|
||||||
|
}
|
||||||
|
if (avoidFerriesCheckbox && avoidFerriesCheckbox.checked) {
|
||||||
|
options.avoid_ferry = true;
|
||||||
|
}
|
||||||
|
if (avoidUnpavedCheckbox && avoidUnpavedCheckbox.checked) {
|
||||||
|
options.avoid_unpaved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var costing_options = { motorcycle: options };
|
||||||
|
|
||||||
|
var requestBody = {
|
||||||
|
locations: [
|
||||||
|
{ lat: points[0].lat, lon: points[0].lng },
|
||||||
|
{ lat: points[1].lat, lon: points[1].lng }
|
||||||
|
],
|
||||||
|
costing: "motorcycle",
|
||||||
|
costing_options: costing_options
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/route', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
var latlngs = decodePolyline6(data.trip.legs[0].shape);
|
||||||
|
if (routePolyline) {
|
||||||
|
map.removeLayer(routePolyline);
|
||||||
|
}
|
||||||
|
routePolyline = L.polyline(latlngs, { color: 'red', weight: 5}).addTo(map);
|
||||||
|
map.fitBounds(routePolyline.getBounds());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on('click', function(e) {
|
||||||
|
if (points.length < 2) {
|
||||||
|
var marker = L.marker(e.latlng).addTo(map);
|
||||||
|
markers.push(marker);
|
||||||
|
points.push(e.latlng);
|
||||||
|
marker.bindPopup(points.length === 1 ? "Start" : "End").openPopup();
|
||||||
|
}
|
||||||
|
calculateRoute();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for changes on all checkboxes
|
||||||
|
[
|
||||||
|
avoidHighwaysCheckbox,
|
||||||
|
useShortestCheckbox,
|
||||||
|
avoidTollRoadsCheckbox,
|
||||||
|
avoidFerriesCheckbox,
|
||||||
|
avoidUnpavedCheckbox
|
||||||
|
].forEach(function(checkbox) {
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.addEventListener('change', calculateRoute);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable other checkboxes when "Use shortest route" is checked
|
||||||
|
useShortestCheckbox.addEventListener('change', function() {
|
||||||
|
var disable = useShortestCheckbox.checked;
|
||||||
|
[
|
||||||
|
avoidHighwaysCheckbox,
|
||||||
|
avoidTollRoadsCheckbox,
|
||||||
|
avoidFerriesCheckbox,
|
||||||
|
avoidUnpavedCheckbox
|
||||||
|
].forEach(function(cb) {
|
||||||
|
cb.disabled = disable;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adapted from Valhalla docs — polyline6 decoder for JS
|
||||||
|
function decodePolyline6(encoded) {
|
||||||
|
var coords = [];
|
||||||
|
var index = 0, lat = 0, lng = 0;
|
||||||
|
var shift = 0, result = 0, byte = null, latitude_change, longitude_change;
|
||||||
|
var factor = Math.pow(10, 6);
|
||||||
|
while (index < encoded.length) {
|
||||||
|
byte = shift = result = 0;
|
||||||
|
do {
|
||||||
|
byte = encoded.charCodeAt(index++) - 63;
|
||||||
|
result |= (byte & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (byte >= 0x20);
|
||||||
|
latitude_change = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||||
|
|
||||||
|
shift = result = 0;
|
||||||
|
do {
|
||||||
|
byte = encoded.charCodeAt(index++) - 63;
|
||||||
|
result |= (byte & 0x1f) << shift;
|
||||||
|
shift += 5;
|
||||||
|
} while (byte >= 0x20);
|
||||||
|
longitude_change = (result & 1) ? ~(result >> 1) : (result >> 1);
|
||||||
|
|
||||||
|
lat += latitude_change;
|
||||||
|
lng += longitude_change;
|
||||||
|
|
||||||
|
coords.push([lat / factor, lng / factor]);
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Markers
|
||||||
|
function resetMarkers() {
|
||||||
|
markers.forEach(function(marker) {
|
||||||
|
map.removeLayer(marker);
|
||||||
|
});
|
||||||
|
markers = [];
|
||||||
|
points = [];
|
||||||
|
if (routePolyline) {
|
||||||
|
map.removeLayer(routePolyline);
|
||||||
|
routePolyline = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make resetMarkers available globally
|
||||||
|
window.resetMarkers = resetMarkers;
|
||||||
|
|
||||||
|
// Plot Route button logic
|
||||||
|
document.getElementById('plotRouteBtn').addEventListener('click', function() {
|
||||||
|
var sourceInput = document.getElementById('sourceInput');
|
||||||
|
var destInput = document.getElementById('destInput');
|
||||||
|
var sourceLat = parseFloat(sourceInput.dataset.lat);
|
||||||
|
var sourceLon = parseFloat(sourceInput.dataset.lon);
|
||||||
|
var destLat = parseFloat(destInput.dataset.lat);
|
||||||
|
var destLon = parseFloat(destInput.dataset.lon);
|
||||||
|
|
||||||
|
if (!isNaN(sourceLat) && !isNaN(sourceLon) && !isNaN(destLat) && !isNaN(destLon)) {
|
||||||
|
// Remove old markers
|
||||||
|
markers.forEach(function(marker) {
|
||||||
|
map.removeLayer(marker);
|
||||||
|
});
|
||||||
|
markers = [];
|
||||||
|
points = [];
|
||||||
|
|
||||||
|
// Add new markers
|
||||||
|
var startMarker = L.marker([sourceLat, sourceLon]).addTo(map).bindPopup("Start").openPopup();
|
||||||
|
var endMarker = L.marker([destLat, destLon]).addTo(map).bindPopup("End").openPopup();
|
||||||
|
markers.push(startMarker, endMarker);
|
||||||
|
points.push({lat: sourceLat, lng: sourceLon}, {lat: destLat, lng: destLon});
|
||||||
|
|
||||||
|
calculateRoute();
|
||||||
|
} else {
|
||||||
|
alert("Please enter valid addresses for both start and destination.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
9
backend/compose.yml
Normal file
9
backend/compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
valhalla-scripted:
|
||||||
|
tty: true
|
||||||
|
container_name: valhalla
|
||||||
|
ports:
|
||||||
|
- 8002:8002
|
||||||
|
volumes:
|
||||||
|
- $PWD/custom_files:/custom_files
|
||||||
|
image: ghcr.io/valhalla/valhalla-scripted:latest
|
||||||
Reference in New Issue
Block a user