Compare commits
No commits in common. "9367ba5b6bbe398463103e32bb8cbc8717f5a261" and "b0fdcd4332aac4f9740a8f7375ffd4ca2eaa9c68" have entirely different histories.
9367ba5b6b
...
b0fdcd4332
9 changed files with 7 additions and 473 deletions
|
|
@ -1,35 +0,0 @@
|
||||||
name: Build & Push marker-api
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'services/marker-api/**'
|
|
||||||
- '.forgejo/workflows/build-marker-api.yml'
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: 192.168.178.6:8083
|
|
||||||
IMAGE: 192.168.178.6:8083/${{ github.repository_owner }}/rd13_tile_server-marker-api
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Login to Forgejo Container Registry
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.GITHUB_TOKEN }}" | \
|
|
||||||
docker login ${{ env.REGISTRY }} \
|
|
||||||
--username "${{ github.actor }}" \
|
|
||||||
--password-stdin
|
|
||||||
|
|
||||||
- name: Build & push marker-api
|
|
||||||
run: |
|
|
||||||
docker build \
|
|
||||||
--tag "${{ env.IMAGE }}:latest" \
|
|
||||||
--tag "${{ env.IMAGE }}:${{ github.sha }}" \
|
|
||||||
./services/marker-api
|
|
||||||
docker push "${{ env.IMAGE }}:latest"
|
|
||||||
docker push "${{ env.IMAGE }}:${{ github.sha }}"
|
|
||||||
|
|
@ -38,21 +38,3 @@ services:
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
depends_on:
|
depends_on:
|
||||||
- martin
|
- martin
|
||||||
|
|
||||||
marker-api:
|
|
||||||
# Image wird von Forgejo CI gebaut und in die Registry gepusht.
|
|
||||||
# Erstes lokales Build: docker compose build marker-api
|
|
||||||
image: 192.168.178.6:8083/cschulz/rd13_tile_server-marker-api:latest
|
|
||||||
container_name: rd13_marker_api
|
|
||||||
build:
|
|
||||||
context: ./services/marker-api
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- "9984:3000"
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 5s
|
|
||||||
|
|
|
||||||
|
|
@ -232,15 +232,6 @@ docker compose logs -f tileserver # Raster-Rendering
|
||||||
- Fonts: `data/fonts/`
|
- Fonts: `data/fonts/`
|
||||||
- Sprites: `data/sprites/`
|
- Sprites: `data/sprites/`
|
||||||
|
|
||||||
**marker-api (Marker-PNG-Generierung)**
|
|
||||||
- Code: `services/marker-api/` (Go, 0 externe Dependencies)
|
|
||||||
- Image: `192.168.178.6:8083/cschulz/rd13_tile_server-marker-api:latest` (Forgejo Registry)
|
|
||||||
- API Root: `http://localhost:9984`
|
|
||||||
- Endpunkt: `/styles/v4/marker/{type}+{hexcolor}(@{scale}x).png`
|
|
||||||
- Typen: `pin-s` (24px), `pin-m` (32px), `pin-l` (40px); Canvas quadratisch, 80% opak, schlanke Teardrop-Form (r/d≈0.35)
|
|
||||||
- Image neu bauen: `docker compose build marker-api && docker compose up -d marker-api`
|
|
||||||
- CI: Forgejo baut + pusht automatisch bei Änderungen in `services/marker-api/`
|
|
||||||
|
|
||||||
### Reverse Proxy (NPM) – Öffentliche URLs
|
### Reverse Proxy (NPM) – Öffentliche URLs
|
||||||
|
|
||||||
**Konfiguration (manuell im NPM WebUI):**
|
**Konfiguration (manuell im NPM WebUI):**
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,9 @@
|
||||||
```
|
```
|
||||||
Internet (HTTPS)
|
Internet (HTTPS)
|
||||||
↓
|
↓
|
||||||
Caddy Reverse Proxy (tiles.rd13server.de)
|
NPM Reverse Proxy (tiles.rd13server.de, Rate Limiting)
|
||||||
├─ /styles/v4/marker/* → marker-api :9984 (Go, Pin-PNG-Generator)
|
├─ /osm/{z}/{x}/{y}.pbf → Martin (Vektor-Tiles)
|
||||||
├─ /styles/* → tileserver-gl :9983 (Raster-PNG)
|
└─ /osm-intl/{z}/{x}/{y}.png → tileserver-gl (Raster-PNG)
|
||||||
└─ /* → Martin :9982 (Vektor-Tiles)
|
|
||||||
↓
|
↓
|
||||||
tileserver-gl renders Martin Vektor → PNG
|
tileserver-gl renders Martin Vektor → PNG
|
||||||
↓
|
↓
|
||||||
|
|
@ -28,8 +27,7 @@ Caddy Reverse Proxy (tiles.rd13server.de)
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Martin** | `osm.mbtiles` | `/{layer}/{z}/{x}/{y}.pbf` | Vektor-API, Tile-Distribution |
|
| **Martin** | `osm.mbtiles` | `/{layer}/{z}/{x}/{y}.pbf` | Vektor-API, Tile-Distribution |
|
||||||
| **tileserver-gl** | Martin PBF + `osm-intl.json` | `/{style}/{z}/{x}/{y}.png` | Rendering, PNG-Tile-Cache |
|
| **tileserver-gl** | Martin PBF + `osm-intl.json` | `/{style}/{z}/{x}/{y}.png` | Rendering, PNG-Tile-Cache |
|
||||||
| **marker-api** | URL-Parameter (type, color, scale) | `/styles/v4/marker/*.png` | Go-Service, Google-Maps-Pin-Generierung |
|
| **NPM** | HTTP Client Requests | HTTPS Responses | Proxy, SSL, Rate-Limit |
|
||||||
| **Caddy** | HTTP Client Requests | HTTPS Responses | Proxy, SSL, Routing |
|
|
||||||
|
|
||||||
**Verwandte ADRs:**
|
**Verwandte ADRs:**
|
||||||
- [ADR-001: Tile Rendering Stack – Martin + tileserver-gl](docs/adr/001-martin-tileserver-gl-stack.md)
|
- [ADR-001: Tile Rendering Stack – Martin + tileserver-gl](docs/adr/001-martin-tileserver-gl-stack.md)
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
# Session 2026-07-01 – Marker-API für MediaWiki Kartographer Visual Editor
|
|
||||||
|
|
||||||
## Kontext
|
|
||||||
MediaWiki Kartographer Visual Editor rief `https://tiles.rd13server.de/styles/v4/marker/pin-m+7e7e7e@2x.png` auf – der Endpunkt existierte nicht (HTTP 404). Ziel war es, einen Maptiler-kompatiblen Marker-Endpunkt bereitzustellen, ohne MediaWiki-Extensions anzupassen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 1
|
|
||||||
«Wenn ich im media wiki auf karten einen pin im visual editor setzen will https://tiles.rd13server.de/styles/v4/marker/pin-m+7e7e7e@2x.png und das hier aufgerufen wird, kommt nur ein ? im browser. Wie können wir das beheben?»
|
|
||||||
|
|
||||||
### Antwort 1
|
|
||||||
Diagnose ergab: URL lieferte HTTP 404. Die Maptiler Static Images Marker-API (`/styles/v4/marker/`) ist kein Standard-Endpunkt von tileserver-gl. Analysierte Lösungsoptionen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 2
|
|
||||||
«Ich würde gerne die media wiki extensions so unangetastet wie möglich lassen.»
|
|
||||||
|
|
||||||
### Antwort 2
|
|
||||||
Entscheidung für einen serverseitigen Marker-Generierungs-Service, der die Maptiler-kompatible URL-Schema bereitstellt. MediaWiki-Extensions bleiben vollständig unverändert.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 3
|
|
||||||
«Heißt das das am ende der caddy den marker bereit stellt? und wie wäre es wenn der tile server den maker mit liefern würde?»
|
|
||||||
|
|
||||||
### Antwort 3
|
|
||||||
Analyse zweier Ansätze: Custom tileserver-gl Image (Variante A) vs. separater Service. Variante A wurde versucht aber verworfen, da tileserver-gl kein Node.js Modul exportiert und der Original-Entrypoint komplex ist.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 4–6
|
|
||||||
«variante a implementieren» / «Prüfe noch mal welche ansätze es generell gibt» / «ok dann lass uns die option mit dem zusätzlichen service container implementieren!»
|
|
||||||
|
|
||||||
### Antwort 4–6
|
|
||||||
Vollständige Lösungsanalyse durchgeführt. Entscheidung für **Option D: minimaler separater Docker Container** mit Go (keine externen Dependencies, ~5–10 MB RAM, `FROM scratch`-artiges Alpine-Image).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 7
|
|
||||||
«ist node.js schon das schlangste was wir machen können ich würde gerne auf minimal ram verbauch optimieren?»
|
|
||||||
|
|
||||||
### Antwort 7
|
|
||||||
RAM-Vergleich: Go ~5–10 MB vs. Node.js ~25–40 MB vs. Python ~20–30 MB. Go als klarer Gewinner (stdlib reicht für PNG-Generierung).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 8
|
|
||||||
«dann bitte in go implementieren.»
|
|
||||||
|
|
||||||
### Antwort 8
|
|
||||||
Implementiert:
|
|
||||||
- `services/marker-api/main.go` – Go HTTP-Server mit Scanline-Algorithmus für Kreis+Dreieck Pin
|
|
||||||
- `services/marker-api/go.mod` – Go-Modulfile, 0 externe Dependencies
|
|
||||||
- `services/marker-api/Dockerfile` – Multi-Stage Build: `golang:1.21-alpine` → `alpine:3.19`
|
|
||||||
- `.forgejo/workflows/build-marker-api.yml` – CI Workflow
|
|
||||||
- `docker-compose.yml` – marker-api Service auf Port 9984
|
|
||||||
- `rd13_system_proxy/caddy/Caddyfile` – Route `/styles/v4/marker/*` → Port 9984
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose build marker-api
|
|
||||||
docker compose up -d marker-api
|
|
||||||
docker exec rd13_caddy_proxy caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
curl "https://tiles.rd13server.de/styles/v4/marker/pin-m+7e7e7e@2x.png"
|
|
||||||
# HTTP 200 | 545 bytes | image/png ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 9
|
|
||||||
«ok the maker is not very good looking can we make it to look more like the marker from google maps? I want to have all the container build happening in forgejo and have the images stored in the container registry.»
|
|
||||||
|
|
||||||
### Antwort 9
|
|
||||||
Marker-Optik komplett neu implementiert: Mathematisch korrekte Teardrop-Form mit echten Tangenten vom Tip-Punkt an den Kreis (Google-Maps-Look). Weißer Rand + weißer Innenpunkt.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose build marker-api && docker compose up -d marker-api
|
|
||||||
curl "https://tiles.rd13server.de/styles/v4/marker/pin-m+7e7e7e@2x.png"
|
|
||||||
# HTTP 200 | 568 bytes | image/png ✅
|
|
||||||
```
|
|
||||||
|
|
||||||
Forgejo CI Workflow: `.forgejo/workflows/build-marker-api.yml`
|
|
||||||
- Trigger: Push auf `main`, wenn `services/marker-api/**` geändert
|
|
||||||
- Registry: `192.168.178.6:8083/cschulz/rd13_tile_server-marker-api:latest`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 10
|
|
||||||
«alle container builds und code sollte gemeinsam im tile server repo auf forgejo liegen und es soll kein extra repo für marker-api geben.»
|
|
||||||
|
|
||||||
### Antwort 10
|
|
||||||
Aufgeräumt:
|
|
||||||
- `services/tileserver-gl-marker/` (gescheiterter Ansatz) gelöscht
|
|
||||||
- Workflow-Image-Name: `rd13_tile_server-marker-api` (klar zum Repo gehörig)
|
|
||||||
- `docker-compose.yml` Image-Referenz aktualisiert
|
|
||||||
- Session-History und PROJECT_CONTEXT erstellt/aktualisiert
|
|
||||||
|
|
||||||
### Prompt 11
|
|
||||||
«so sehen die marker aus im media wiki warum sind sie farblos und warum abgeschnitten?»
|
|
||||||
|
|
||||||
### Antwort 11
|
|
||||||
Zwei Probleme identifiziert:
|
|
||||||
1. **Abgeschnitten**: Canvas war quadratisch, Teardrop-Spitze wurde unten abgeschnitten
|
|
||||||
2. **Farblos**: `7e7e7e` ist das MediaWiki-Default wenn kein `marker-color` im GeoJSON gesetzt ist – korrektes Verhalten
|
|
||||||
|
|
||||||
Geometry-Fix: r=35.5% der Breite, tipY=97.5%, Spitze passt jetzt sauber in die Canvas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Prompt 12
|
|
||||||
«Aber ich will doch das es in media wiki gut aussieht also muss es in 32x32 passen und gut aussehen. wäre es auch möglich ihn etwas durchsichtig zu machen?»
|
|
||||||
|
|
||||||
### Prompt 13
|
|
||||||
«Das sieht aber immer noch komisch aus...»
|
|
||||||
|
|
||||||
### Antwort 13
|
|
||||||
Ursache identifiziert: `r/d = 0.59` (Kreisradius / Abstand zur Spitze) → Öffnungswinkel 36° → Tangenten laufen flach aus → Form sieht aus wie ein Oval.
|
|
||||||
|
|
||||||
Fix: `r = size * 0.24` (statt 0.355) → `r/d ≈ 0.35` → Öffnungswinkel 20° → schlanker Pin.
|
|
||||||
|
|
||||||
Verifiziert mit blauen Test-Pin (±4285F4): sieht jetzt aus wie Google-Maps-Pin.
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# PROJECT_CONTEXT – rd13_tile_server
|
# PROJECT_CONTEXT – rd13_tile_server
|
||||||
|
|
||||||
**Letzte Aktualisierung:** 2026-07-01 – ✅ **Marker-API**: Go-Service für Maptiler-kompatible Marker-Endpunkte (`/styles/v4/marker/*`). 32×32px Canvas, schlanker Teardrop-Pin (`r/d≈0.35`, Öffnungswinkel 20°), 80% Transparenz. Forgejo CI baut + pusht Image in lokale Container-Registry.
|
**Letzte Aktualisierung:** 2026-06-22 – ✅ **Integrationen geschärft**: Nextcloud- und Home-Assistant-Doku auf verifizierte Endpunkte umgestellt, produktive Domain `tiles.rd13server.de` explizit in Integrations-Snippets gesetzt, Bamberg z14/z16 Vergleichsexporte + Bash-Exportskript ergänzt.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Projektziel
|
## Projektziel
|
||||||
|
|
||||||
Selbst gehosteter Tile Server für MediaWiki (Kartographer), Nextcloud, Home Assistant.
|
Selbst gehosteter Tile Server für MediaWiki (Kartographer), Nextcloud, Home Assistant.
|
||||||
**Stack:** Martin (Vektor-API) + tileserver-gl (Raster PNG) + marker-api (Go) + Caddy (Reverse Proxy).
|
**Stack:** Martin (Vektor-API) + tileserver-gl (Raster PNG) + Caddy (Reverse Proxy).
|
||||||
Tile-Updates manuell 2–4×/Jahr; Styles anpassbar.
|
Tile-Updates manuell 2–4×/Jahr; Styles anpassbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -16,9 +16,8 @@ Tile-Updates manuell 2–4×/Jahr; Styles anpassbar.
|
||||||
|
|
||||||
- **Vektor-API:** `ghcr.io/maplibre/martin:latest` (Port 9982 → `/osm/{z}/{x}/{y}`)
|
- **Vektor-API:** `ghcr.io/maplibre/martin:latest` (Port 9982 → `/osm/{z}/{x}/{y}`)
|
||||||
- **Raster-Renderer:** `maptiler/tileserver-gl` (Port 9983 → `/styles/osm-intl/{z}/{x}/{y}.png`)
|
- **Raster-Renderer:** `maptiler/tileserver-gl` (Port 9983 → `/styles/osm-intl/{z}/{x}/{y}.png`)
|
||||||
- **Marker-API:** `192.168.178.6:8083/cschulz/rd13_tile_server-marker-api:latest` (Port 9984 → `/styles/v4/marker/*.png`)
|
|
||||||
- **Style:** OSM Bright GL (128 Layer, Wanderwege, POIs, vollständige Weltkarte)
|
- **Style:** OSM Bright GL (128 Layer, Wanderwege, POIs, vollständige Weltkarte)
|
||||||
- **Proxy:** Caddy (`rd13_system_proxy`) – SSL, Routing `/styles/v4/marker/*` → 9984, `/styles/` → 9983, rest → 9982
|
- **Proxy:** Caddy (`rd13_system_proxy`) – SSL, Routing `/styles/` → 9983, rest → 9982
|
||||||
- **Konfiguration:** `config/tileserver.json`, `config/styles/osm-intl.json`
|
- **Konfiguration:** `config/tileserver.json`, `config/styles/osm-intl.json`
|
||||||
- **Daten:** `data/osm.mbtiles` (95 GB, Planet z0–14), `data/sprites/`, `data/fonts/`
|
- **Daten:** `data/osm.mbtiles` (95 GB, Planet z0–14), `data/sprites/`, `data/fonts/`
|
||||||
- **Satellit:** `docker-compose.download.yml` → `data/satellite.mbtiles` (~38 GB, z0–11, noch nicht gestartet)
|
- **Satellit:** `docker-compose.download.yml` → `data/satellite.mbtiles` (~38 GB, z0–11, noch nicht gestartet)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# ── Stage 1: Build ──────────────────────────────────────────────────────────
|
|
||||||
FROM golang:1.21-alpine AS builder
|
|
||||||
WORKDIR /build
|
|
||||||
COPY go.mod main.go ./
|
|
||||||
# Statisch gelinkt, minimales Binary (~5 MB)
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o marker-api .
|
|
||||||
|
|
||||||
# ── Stage 2: Run ─────────────────────────────────────────────────────────────
|
|
||||||
# alpine:3.19 ≈ 8 MB Basis, wget für Healthcheck
|
|
||||||
FROM alpine:3.19
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
COPY --from=builder /build/marker-api /usr/local/bin/marker-api
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
ENV PORT=3000
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
||||||
CMD wget -qO- http://localhost:3000/health || exit 1
|
|
||||||
|
|
||||||
ENTRYPOINT ["marker-api"]
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module marker-api
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
@ -1,257 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"image/png"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// URL-Muster: /styles/v4/marker/{type}+{hexcolor}(@{scale}x).png
|
|
||||||
var markerPattern = regexp.MustCompile(
|
|
||||||
`^/styles/v4/marker/([a-z-]+)\+([0-9a-fA-F]{6})(?:@(\d+(?:\.\d+)?x))?\.png$`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Basis-Größen in logischen Pixeln (quadratische Canvas)
|
|
||||||
var baseSizes = map[string]float64{
|
|
||||||
"pin-s": 24,
|
|
||||||
"pin-m": 32,
|
|
||||||
"pin-l": 40,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marker-Cache (max 500 Einträge, reicht für alle Farb/Typ/Scale-Kombinationen)
|
|
||||||
var (
|
|
||||||
markerCache = make(map[string][]byte)
|
|
||||||
markerCacheMu sync.RWMutex
|
|
||||||
)
|
|
||||||
|
|
||||||
func hexToColor(h string) color.RGBA {
|
|
||||||
r, _ := strconv.ParseUint(h[0:2], 16, 8)
|
|
||||||
g, _ := strconv.ParseUint(h[2:4], 16, 8)
|
|
||||||
b, _ := strconv.ParseUint(h[4:6], 16, 8)
|
|
||||||
return color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fillPolygon füllt ein Polygon mit dem Scanline-Algorithmus
|
|
||||||
func fillPolygon(img *image.RGBA, pts [][2]float64, c color.RGBA) {
|
|
||||||
if len(pts) < 3 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bounds := img.Bounds()
|
|
||||||
minY, maxY := pts[0][1], pts[0][1]
|
|
||||||
for _, p := range pts {
|
|
||||||
if p[1] < minY {
|
|
||||||
minY = p[1]
|
|
||||||
}
|
|
||||||
if p[1] > maxY {
|
|
||||||
maxY = p[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n := len(pts)
|
|
||||||
for scanY := int(math.Floor(minY)); scanY <= int(math.Ceil(maxY)); scanY++ {
|
|
||||||
if scanY < bounds.Min.Y || scanY >= bounds.Max.Y {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
y := float64(scanY) + 0.5
|
|
||||||
var xs []float64
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
p1, p2 := pts[i], pts[(i+1)%n]
|
|
||||||
if (p1[1] <= y && p2[1] > y) || (p2[1] <= y && p1[1] > y) {
|
|
||||||
x := p1[0] + (y-p1[1])/(p2[1]-p1[1])*(p2[0]-p1[0])
|
|
||||||
xs = append(xs, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Float64s(xs)
|
|
||||||
for i := 0; i+1 < len(xs); i += 2 {
|
|
||||||
x0 := int(math.Round(xs[i]))
|
|
||||||
x1 := int(math.Round(xs[i+1]))
|
|
||||||
for x := x0; x <= x1; x++ {
|
|
||||||
if x >= bounds.Min.X && x < bounds.Max.X {
|
|
||||||
img.SetRGBA(x, scanY, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawCircle zeichnet einen gefüllten Kreis
|
|
||||||
func drawCircle(img *image.RGBA, cx, cy, r float64, c color.RGBA) {
|
|
||||||
bounds := img.Bounds()
|
|
||||||
for y := int(math.Floor(cy - r)); y <= int(math.Ceil(cy+r)); y++ {
|
|
||||||
for x := int(math.Floor(cx - r)); x <= int(math.Ceil(cx+r)); x++ {
|
|
||||||
if !image.Pt(x, y).In(bounds) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dx := float64(x) + 0.5 - cx
|
|
||||||
dy := float64(y) + 0.5 - cy
|
|
||||||
if dx*dx+dy*dy <= r*r {
|
|
||||||
img.SetRGBA(x, y, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pinOutline erzeugt den Umriss eines Google-Maps-artigen Pins:
|
|
||||||
// Kreis oben, gerade Seiten (tangential) laufen in einer Spitze zusammen.
|
|
||||||
// Mathematisch korrekt: die Seiten sind exakt die Tangenten vom Tip-Punkt an den Kreis.
|
|
||||||
func pinOutline(cx, cy, r, tipY float64, numArcPts int) [][2]float64 {
|
|
||||||
d := tipY - cy // vertikaler Abstand: Kreismittelpunkt → Spitze
|
|
||||||
if d <= r {
|
|
||||||
// Fallback: Spitze zu nah am Kreis → einfaches Dreieck
|
|
||||||
return [][2]float64{{cx - r, cy}, {cx + r, cy}, {cx, tipY}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Winkel, um den die Tangente von der Senkrechten abweicht
|
|
||||||
alpha := math.Asin(r / d)
|
|
||||||
|
|
||||||
// Berührpunkte auf dem Kreis (im Bild-Koordinatensystem: 0°=rechts, 90°=unten)
|
|
||||||
rightAngle := math.Pi/2 - alpha // rechter Berührpunkt (~63°)
|
|
||||||
leftAngle := math.Pi/2 + alpha // linker Berührpunkt (~117°)
|
|
||||||
|
|
||||||
var pts [][2]float64
|
|
||||||
|
|
||||||
// Bogen vom rechten Berührpunkt, gegen den Uhrzeigersinn durch den Scheitel (270°),
|
|
||||||
// zum linken Berührpunkt.
|
|
||||||
// arcSpan = rightAngle + 2π - leftAngle (entgegen dem Uhrzeigersinn)
|
|
||||||
arcSpan := rightAngle + 2*math.Pi - leftAngle
|
|
||||||
for i := 0; i <= numArcPts; i++ {
|
|
||||||
t := float64(i) / float64(numArcPts)
|
|
||||||
angle := rightAngle - t*arcSpan
|
|
||||||
pts = append(pts, [2]float64{
|
|
||||||
cx + r*math.Cos(angle),
|
|
||||||
cy + r*math.Sin(angle),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Abschlusspunkt: die Spitze des Pins
|
|
||||||
pts = append(pts, [2]float64{cx, tipY})
|
|
||||||
return pts
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateMarker erzeugt einen Google-Maps-artigen Pin als PNG
|
|
||||||
func generateMarker(pinType, hexColor string, scale float64) ([]byte, error) {
|
|
||||||
cacheKey := pinType + "-" + hexColor + "-" + strconv.FormatFloat(scale, 'f', 2, 64)
|
|
||||||
|
|
||||||
markerCacheMu.RLock()
|
|
||||||
if cached, ok := markerCache[cacheKey]; ok {
|
|
||||||
markerCacheMu.RUnlock()
|
|
||||||
return cached, nil
|
|
||||||
}
|
|
||||||
markerCacheMu.RUnlock()
|
|
||||||
|
|
||||||
bs, ok := baseSizes[pinType]
|
|
||||||
if !ok {
|
|
||||||
bs = baseSizes["pin-m"]
|
|
||||||
}
|
|
||||||
size := math.Round(bs * scale)
|
|
||||||
if size < 12 {
|
|
||||||
size = 12
|
|
||||||
}
|
|
||||||
|
|
||||||
// Geometrie: schlanker Google-Maps-Pin
|
|
||||||
// r/d ≈ 0.35 → Öffnungswinkel ~20° → Pin sieht spitz aus, nicht oval
|
|
||||||
cx := size / 2
|
|
||||||
r := size * 0.24 // kleiner Kreis → langer, schlanker Schwanz
|
|
||||||
cy := r + size*0.02 // Kreismittelpunkt oben
|
|
||||||
tipY := size * 0.975 // Spitze am unteren Rand
|
|
||||||
borderW := math.Max(1.5, size*0.05) // Randbreite
|
|
||||||
innerR := r * 0.40 // weißer Innenpunkt (etwas größer für Sichtbarkeit)
|
|
||||||
|
|
||||||
// Transparenz: 80% opak (Alpha 204)
|
|
||||||
fillBase := hexToColor(hexColor)
|
|
||||||
fill := color.RGBA{R: fillBase.R, G: fillBase.G, B: fillBase.B, A: 204}
|
|
||||||
white := color.RGBA{R: 255, G: 255, B: 255, A: 220}
|
|
||||||
whiteDot := color.RGBA{R: 255, G: 255, B: 255, A: 204}
|
|
||||||
|
|
||||||
iSize := int(math.Ceil(size))
|
|
||||||
img := image.NewRGBA(image.Rect(0, 0, iSize, iSize))
|
|
||||||
|
|
||||||
// 1. Weißer Rand
|
|
||||||
borderPts := pinOutline(cx, cy, r+borderW, tipY, 64)
|
|
||||||
fillPolygon(img, borderPts, white)
|
|
||||||
|
|
||||||
// 2. Farbige Pin-Form
|
|
||||||
fillPts := pinOutline(cx, cy, r, tipY, 64)
|
|
||||||
fillPolygon(img, fillPts, fill)
|
|
||||||
|
|
||||||
// 3. Weißer Innenpunkt
|
|
||||||
drawCircle(img, cx, cy, innerR, whiteDot)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := png.Encode(&buf, img); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result := buf.Bytes()
|
|
||||||
|
|
||||||
markerCacheMu.Lock()
|
|
||||||
if len(markerCache) < 500 {
|
|
||||||
markerCache[cacheKey] = result
|
|
||||||
}
|
|
||||||
markerCacheMu.Unlock()
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleMarker(w http.ResponseWriter, r *http.Request) {
|
|
||||||
matches := markerPattern.FindStringSubmatch(r.URL.Path)
|
|
||||||
if matches == nil {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pinType := matches[1]
|
|
||||||
hexColor := matches[2]
|
|
||||||
scaleStr := matches[3]
|
|
||||||
|
|
||||||
scale := 1.0
|
|
||||||
if scaleStr != "" {
|
|
||||||
if s, err := strconv.ParseFloat(strings.TrimSuffix(scaleStr, "x"), 64); err == nil && s > 0 && s <= 4 {
|
|
||||||
scale = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("marker type=%s color=#%s scale=%.1f", pinType, hexColor, scale)
|
|
||||||
|
|
||||||
data, err := generateMarker(pinType, hexColor, scale)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error generating marker: %v", err)
|
|
||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "image/png")
|
|
||||||
w.Header().Set("Cache-Control", "public, max-age=2592000, immutable")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleHealth(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_, _ = w.Write([]byte(`{"status":"ok","service":"marker-api"}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
port := os.Getenv("PORT")
|
|
||||||
if port == "" {
|
|
||||||
port = "3000"
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/health", handleHealth)
|
|
||||||
mux.HandleFunc("/styles/v4/marker/", handleMarker)
|
|
||||||
|
|
||||||
log.Printf("marker-api listening on :%s", port)
|
|
||||||
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue