diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f297f0 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/app/web/.env.example b/app/web/.env.example new file mode 100644 index 0000000..fdb11d8 --- /dev/null +++ b/app/web/.env.example @@ -0,0 +1,2 @@ +VALHALLA_URL=http://10.200.0.15:8002/route +PORT=8080 \ No newline at end of file diff --git a/app/web/.gitea/workflows/docker.yml b/app/web/.gitea/workflows/docker.yml new file mode 100644 index 0000000..0077801 --- /dev/null +++ b/app/web/.gitea/workflows/docker.yml @@ -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 \ No newline at end of file diff --git a/app/web/Dockerfile b/app/web/Dockerfile new file mode 100644 index 0000000..81f9736 --- /dev/null +++ b/app/web/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.22-alpine + +WORKDIR /app + +COPY . . + +RUN go mod tidy + +EXPOSE 8080 + +CMD ["go", "run", "main.go"] \ No newline at end of file diff --git a/app/web/docker-compose.yml b/app/web/docker-compose.yml new file mode 100644 index 0000000..251850d --- /dev/null +++ b/app/web/docker-compose.yml @@ -0,0 +1,7 @@ +services: + freemoto-web: + build: . + ports: + - "8080:8080" + env_file: + - .env \ No newline at end of file diff --git a/app/web/go.mod b/app/web/go.mod new file mode 100644 index 0000000..d8348a3 --- /dev/null +++ b/app/web/go.mod @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/app/web/go.sum @@ -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= diff --git a/app/web/main.go b/app/web/main.go new file mode 100644 index 0000000..9781394 --- /dev/null +++ b/app/web/main.go @@ -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) +} \ No newline at end of file diff --git a/app/web/static/geolocate.js b/app/web/static/geolocate.js new file mode 100644 index 0000000..ebaad06 --- /dev/null +++ b/app/web/static/geolocate.js @@ -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; + } + }); + } + }); +}); \ No newline at end of file diff --git a/app/web/static/index.html b/app/web/static/index.html new file mode 100644 index 0000000..a0210a9 --- /dev/null +++ b/app/web/static/index.html @@ -0,0 +1,126 @@ + + +
+