fix: proper 27x41 pin proportions, white border+dot fully opaque

- Non-square canvas (pin-m: 27x41 @1x = 54x82 @2x, Maptiler standard)
- r=37% of width -> circle fills 74% of canvas width
- r/d=0.37 -> 22deg opening angle (Google Maps standard)
- White border: borderW=max(2px,8%width), A=255 (fully opaque)
- White inner dot: r*0.38, A=255 (fully opaque, clearly visible)
- Fill: 85% opacity (A=217)
This commit is contained in:
Conrad Schulz 2026-07-01 12:33:05 +00:00
parent 9367ba5b6b
commit 641312aba9
4 changed files with 47 additions and 31 deletions

View file

@ -237,7 +237,7 @@ docker compose logs -f tileserver # Raster-Rendering
- Image: `192.168.178.6:8083/cschulz/rd13_tile_server-marker-api:latest` (Forgejo Registry) - Image: `192.168.178.6:8083/cschulz/rd13_tile_server-marker-api:latest` (Forgejo Registry)
- API Root: `http://localhost:9984` - API Root: `http://localhost:9984`
- Endpunkt: `/styles/v4/marker/{type}+{hexcolor}(@{scale}x).png` - 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) - Typen: `pin-s` (20×30px), `pin-m` (27×41px, Maptiler-Standard), `pin-l` (34×52px); 85% opak, weißer Rand + Innenpunkt
- Image neu bauen: `docker compose build marker-api && docker compose up -d marker-api` - 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/` - CI: Forgejo baut + pusht automatisch bei Änderungen in `services/marker-api/`

View file

@ -113,9 +113,19 @@ Geometry-Fix: r=35.5% der Breite, tipY=97.5%, Spitze passt jetzt sauber in die C
### Prompt 13 ### Prompt 13
«Das sieht aber immer noch komisch aus...» «Das sieht aber immer noch komisch aus...»
### Antwort 13 ### Prompt 14
Ursache identifiziert: `r/d = 0.59` (Kreisradius / Abstand zur Spitze) → Öffnungswinkel 36° → Tangenten laufen flach aus → Form sieht aus wie ein Oval. «besser aber immer noch nicht perfekt ich will es perfekt!! und ich will einen weißen rand und weißen punkt in der mitte wenn es geht»
Fix: `r = size * 0.24` (statt 0.355) → `r/d ≈ 0.35` → Öffnungswinkel 20° → schlanker Pin. ### Antwort 14
Ursache der Verzerrung identifiziert: Quadratische Canvas (32×32) erzwingt falsche Proportionen. Ein Teardrop-Pin kann in einem Quadrat nicht korrekt aussehen.
Verifiziert mit blauen Test-Pin (±4285F4): sieht jetzt aus wie Google-Maps-Pin. Lösung: Korrekte nicht-quadratische Canvas entsprechend Maptiler-Standard:
- pin-s: 20×30px, pin-m: 27×41px, pin-l: 34×52px (@1x)
- Der Pin ankert an der Spitze (bottom-center), nicht am Bildmittelpunkt
- r = 37% der Breite (Kreis füllt 74% der Canvas-Breite)
- r/d = 0.37 → Öffnungswinkel 22° (Google-Maps-Standard)
- Weißer Rand: borderW = max(2px, 8% der Breite) mit A=255 (vollständig opak)
- Weißer Innenpunkt: r * 0.38 mit A=255 (vollständig opak)
- Füllfarbe: 85% opak (A=217)
Verifiziert: pin-m@2x = 54×82px, sieht aus wie Google Maps Pin

View file

@ -1,6 +1,6 @@
# 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-07-01 **Marker-API**: Go-Service für Maptiler-kompatible Marker-Endpunkte (`/styles/v4/marker/*`). Korrekte 27×41px Proportionen (Maptiler-Standard), weißer Rand (A=255), weißer Innenpunkt (A=255), 85% opaker Fill. Forgejo CI baut + pusht Image.
--- ---

View file

@ -21,11 +21,14 @@ var markerPattern = regexp.MustCompile(
`^/styles/v4/marker/([a-z-]+)\+([0-9a-fA-F]{6})(?:@(\d+(?:\.\d+)?x))?\.png$`, `^/styles/v4/marker/([a-z-]+)\+([0-9a-fA-F]{6})(?:@(\d+(?:\.\d+)?x))?\.png$`,
) )
// Basis-Größen in logischen Pixeln (quadratische Canvas) // Basis-Abmessungen (Breite × Höhe) entspricht Maptiler-Standard.
var baseSizes = map[string]float64{ // Der Pin ankert an der Spitze (bottom-center), nicht am Bildmittelpunkt.
"pin-s": 24, type pinDims struct{ w, h float64 }
"pin-m": 32,
"pin-l": 40, var baseSizes = map[string]pinDims{
"pin-s": {20, 30},
"pin-m": {27, 41}, // Maptiler-Standard
"pin-l": {34, 52},
} }
// Marker-Cache (max 500 Einträge, reicht für alle Farb/Typ/Scale-Kombinationen) // Marker-Cache (max 500 Einträge, reicht für alle Farb/Typ/Scale-Kombinationen)
@ -151,30 +154,33 @@ func generateMarker(pinType, hexColor string, scale float64) ([]byte, error) {
if !ok { if !ok {
bs = baseSizes["pin-m"] bs = baseSizes["pin-m"]
} }
size := math.Round(bs * scale) w := math.Round(bs.w * scale)
if size < 12 { h := math.Round(bs.h * scale)
size = 12 if w < 10 {
w = 10
}
if h < 15 {
h = 15
} }
// Geometrie: schlanker Google-Maps-Pin // Geometrie: Kreis füllt ~74% der Breite, r/d ≈ 0.37 → Öffnungswinkel 22°
// r/d ≈ 0.35 → Öffnungswinkel ~20° → Pin sieht spitz aus, nicht oval // Entspricht exakt Maptiler pin-m Proportionen (27×41px @1x)
cx := size / 2 cx := w / 2
r := size * 0.24 // kleiner Kreis → langer, schlanker Schwanz r := w * 0.37 // Kreisradius: Kreis fast so breit wie Canvas
cy := r + size*0.02 // Kreismittelpunkt oben cy := r + h*0.04 // Kreismittelpunkt Y: kleiner oberer Rand
tipY := size * 0.975 // Spitze am unteren Rand tipY := h - h*0.03 // Spitze Y: 3% Abstand vom 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) borderW := math.Max(2.0, w*0.08) // weißer Rand: gut sichtbar
innerR := r * 0.38 // weißer Innenpunkt: 38% des Kreisradius
// Füllfarbe: 85% opak; Rand und Punkt: vollständig weiß
fillBase := hexToColor(hexColor) fillBase := hexToColor(hexColor)
fill := color.RGBA{R: fillBase.R, G: fillBase.G, B: fillBase.B, A: 204} fill := color.RGBA{R: fillBase.R, G: fillBase.G, B: fillBase.B, A: 217}
white := color.RGBA{R: 255, G: 255, B: 255, A: 220} white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
whiteDot := color.RGBA{R: 255, G: 255, B: 255, A: 204}
iSize := int(math.Ceil(size)) img := image.NewRGBA(image.Rect(0, 0, int(math.Ceil(w)), int(math.Ceil(h))))
img := image.NewRGBA(image.Rect(0, 0, iSize, iSize))
// 1. Weißer Rand // 1. Weißer Rand (Pin mit borderW größerem Radius)
borderPts := pinOutline(cx, cy, r+borderW, tipY, 64) borderPts := pinOutline(cx, cy, r+borderW, tipY, 64)
fillPolygon(img, borderPts, white) fillPolygon(img, borderPts, white)
@ -182,8 +188,8 @@ func generateMarker(pinType, hexColor string, scale float64) ([]byte, error) {
fillPts := pinOutline(cx, cy, r, tipY, 64) fillPts := pinOutline(cx, cy, r, tipY, 64)
fillPolygon(img, fillPts, fill) fillPolygon(img, fillPts, fill)
// 3. Weißer Innenpunkt // 3. Weißer Innenpunkt vollständig opak für gute Sichtbarkeit
drawCircle(img, cx, cy, innerR, whiteDot) drawCircle(img, cx, cy, innerR, white)
var buf bytes.Buffer var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil { if err := png.Encode(&buf, img); err != nil {