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)
- 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)
- 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`
- 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
«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.
### Prompt 14
«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
**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$`,
)
// Basis-Größen in logischen Pixeln (quadratische Canvas)
var baseSizes = map[string]float64{
"pin-s": 24,
"pin-m": 32,
"pin-l": 40,
// Basis-Abmessungen (Breite × Höhe) entspricht Maptiler-Standard.
// Der Pin ankert an der Spitze (bottom-center), nicht am Bildmittelpunkt.
type pinDims struct{ w, h float64 }
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)
@ -151,30 +154,33 @@ func generateMarker(pinType, hexColor string, scale float64) ([]byte, error) {
if !ok {
bs = baseSizes["pin-m"]
}
size := math.Round(bs * scale)
if size < 12 {
size = 12
w := math.Round(bs.w * scale)
h := math.Round(bs.h * scale)
if w < 10 {
w = 10
}
if h < 15 {
h = 15
}
// 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)
// Geometrie: Kreis füllt ~74% der Breite, r/d ≈ 0.37 → Öffnungswinkel 22°
// Entspricht exakt Maptiler pin-m Proportionen (27×41px @1x)
cx := w / 2
r := w * 0.37 // Kreisradius: Kreis fast so breit wie Canvas
cy := r + h*0.04 // Kreismittelpunkt Y: kleiner oberer Rand
tipY := h - h*0.03 // Spitze Y: 3% Abstand vom unteren Rand
// 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)
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}
fill := color.RGBA{R: fillBase.R, G: fillBase.G, B: fillBase.B, A: 217}
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
iSize := int(math.Ceil(size))
img := image.NewRGBA(image.Rect(0, 0, iSize, iSize))
img := image.NewRGBA(image.Rect(0, 0, int(math.Ceil(w)), int(math.Ceil(h))))
// 1. Weißer Rand
// 1. Weißer Rand (Pin mit borderW größerem Radius)
borderPts := pinOutline(cx, cy, r+borderW, tipY, 64)
fillPolygon(img, borderPts, white)
@ -182,8 +188,8 @@ func generateMarker(pinType, hexColor string, scale float64) ([]byte, error) {
fillPts := pinOutline(cx, cy, r, tipY, 64)
fillPolygon(img, fillPts, fill)
// 3. Weißer Innenpunkt
drawCircle(img, cx, cy, innerR, whiteDot)
// 3. Weißer Innenpunkt vollständig opak für gute Sichtbarkeit
drawCircle(img, cx, cy, innerR, white)
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {