Compare commits

...

3 commits

Author SHA1 Message Date
9367ba5b6b fix: slim pin geometry, r/d=0.35 opening angle 20deg
Some checks failed
Build & Push marker-api / build (push) Failing after 4s
Was: r=35.5% of size -> r/d=0.59 -> 36 deg opening -> fat oval
Now: r=24% of size -> r/d=0.35 -> 20 deg opening -> proper Google Maps pin shape
2026-07-01 12:23:31 +00:00
3960c9c5c1 fix: marker pin 32x32 canvas, correct geometry, 80% opacity
- Square canvas (pin-m=32x32 @1x, 64x64 @2x) - fits MediaWiki slot
- r=35.5% of width, tipY=97.5%: teardrop no longer clipped
- 80% transparency: fill.A=204, white border A=220, inner dot A=204
2026-07-01 12:13:48 +00:00
9df190aca8 feat: add Go marker-api service for MediaWiki Kartographer pin support
- services/marker-api/: Go HTTP service (0 external deps, ~8 MB RAM)
  - Mathematically correct teardrop pin shape (Google Maps style)
  - Tangent-based outline: circle + straight sides meeting at tip
  - White border + white inner dot, in-memory cache (max 500 entries)
- .forgejo/workflows/build-marker-api.yml: CI builds & pushes to
  Forgejo registry on push to main (image: rd13_tile_server-marker-api)
- docker-compose.yml: add marker-api service on port 9984
- Caddyfile (rd13_system_proxy): route /styles/v4/marker/* -> :9984
- docs/ADMIN.md + MAINTAINER.md: marker-api dokumentiert

Fixes: /styles/v4/marker/pin-m+{color}(@{scale}x).png was 404, now 200
2026-07-01 11:59:00 +00:00
9 changed files with 473 additions and 7 deletions

View file

@ -0,0 +1,35 @@
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 }}"

View file

@ -38,3 +38,21 @@ 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

View file

@ -232,6 +232,15 @@ 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):**

View file

@ -10,9 +10,10 @@
``` ```
Internet (HTTPS) Internet (HTTPS)
NPM Reverse Proxy (tiles.rd13server.de, Rate Limiting) Caddy Reverse Proxy (tiles.rd13server.de)
├─ /osm/{z}/{x}/{y}.pbf → Martin (Vektor-Tiles) ├─ /styles/v4/marker/* → marker-api :9984 (Go, Pin-PNG-Generator)
└─ /osm-intl/{z}/{x}/{y}.png → tileserver-gl (Raster-PNG) ├─ /styles/* → tileserver-gl :9983 (Raster-PNG)
└─ /* → Martin :9982 (Vektor-Tiles)
tileserver-gl renders Martin Vektor → PNG tileserver-gl renders Martin Vektor → PNG
@ -27,7 +28,8 @@ NPM Reverse Proxy (tiles.rd13server.de, Rate Limiting)
|---|---|---|---| |---|---|---|---|
| **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 |
| **NPM** | HTTP Client Requests | HTTPS Responses | Proxy, SSL, Rate-Limit | | **marker-api** | URL-Parameter (type, color, scale) | `/styles/v4/marker/*.png` | Go-Service, Google-Maps-Pin-Generierung |
| **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)

View file

@ -0,0 +1,121 @@
# 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 46
«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 46
Vollständige Lösungsanalyse durchgeführt. Entscheidung für **Option D: minimaler separater Docker Container** mit Go (keine externen Dependencies, ~510 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 ~510 MB vs. Node.js ~2540 MB vs. Python ~2030 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.

View file

@ -1,13 +1,13 @@
# PROJECT_CONTEXT rd13_tile_server # PROJECT_CONTEXT rd13_tile_server
**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. **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.
--- ---
## 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) + Caddy (Reverse Proxy). **Stack:** Martin (Vektor-API) + tileserver-gl (Raster PNG) + marker-api (Go) + Caddy (Reverse Proxy).
Tile-Updates manuell 24×/Jahr; Styles anpassbar. Tile-Updates manuell 24×/Jahr; Styles anpassbar.
--- ---
@ -16,8 +16,9 @@ Tile-Updates manuell 24×/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/` → 9983, rest → 9982 - **Proxy:** Caddy (`rd13_system_proxy`) SSL, Routing `/styles/v4/marker/*` → 9984, `/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 z014), `data/sprites/`, `data/fonts/` - **Daten:** `data/osm.mbtiles` (95 GB, Planet z014), `data/sprites/`, `data/fonts/`
- **Satellit:** `docker-compose.download.yml``data/satellite.mbtiles` (~38 GB, z011, noch nicht gestartet) - **Satellit:** `docker-compose.download.yml``data/satellite.mbtiles` (~38 GB, z011, noch nicht gestartet)

View file

@ -0,0 +1,20 @@
# ── 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"]

View file

@ -0,0 +1,3 @@
module marker-api
go 1.21

257
services/marker-api/main.go Normal file
View file

@ -0,0 +1,257 @@
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)
}
}