// ----------------------------------------------------------------------------- // Immo-Ka — Agrégateur de propriétés à vendre (province de Québec) // Auteur : Simon-Pierre Boucher — contact@spboucher.ai // map/pills.ts : pastilles de prix pour la carte — images « étirables » // (stretchable images MapLibre) générées au canvas : la pastille s'adapte // à la largeur du texte (icon-text-fit) sans aucun sprite externe. // ----------------------------------------------------------------------------- import type { Map as MLMap, StyleImageMetadata } from "maplibre-gl"; const R = 13; // rayon des coins (px logiques) const H = 30; // hauteur de la pastille const W = 64; // largeur de base (étirée au besoin) const DPR = 2; // netteté écrans Retina /** roundRect avec repli manuel (Safari < 16 ne l'a pas). */ function rr(g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void { if (typeof (g as any).roundRect === "function") { (g as any).roundRect(x, y, w, h, r); return; } g.moveTo(x + r, y); g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r); g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath(); } function drawPill(fill: string, stroke: string, shadow = true): ImageData { const cv = document.createElement("canvas"); cv.width = W * DPR; cv.height = (H + 6) * DPR; // marge pour l'ombre + la pointe const g = cv.getContext("2d")!; g.scale(DPR, DPR); if (shadow) { g.shadowColor = "rgba(26,18,20,0.28)"; g.shadowBlur = 5; g.shadowOffsetY = 2; } // corps arrondi g.beginPath(); rr(g, 1, 1, W - 2, H - 2, R); g.fillStyle = fill; g.fill(); g.shadowColor = "transparent"; // petite pointe sous la pastille (ancrage visuel sur la propriété) g.beginPath(); g.moveTo(W / 2 - 5, H - 1.5); g.lineTo(W / 2, H + 4); g.lineTo(W / 2 + 5, H - 1.5); g.closePath(); g.fillStyle = fill; g.fill(); g.strokeStyle = stroke; g.lineWidth = 1.4; g.beginPath(); rr(g, 1, 1, W - 2, H - 2, R); g.stroke(); return g.getImageData(0, 0, cv.width, cv.height); } /** Enregistre les variantes de pastille dans la carte. */ export function addPills(map: MLMap): void { const meta: Partial = { pixelRatio: DPR, // zones étirables entre les coins arrondis — coordonnées PHYSIQUES (× DPR) // et non dégénérées (x1 < x2), sinon MapLibre rejette l'image en silence stretchX: [[(R + 1) * DPR, (W - R - 1) * DPR]], stretchY: [[(R + 1) * DPR, (H - R - 1) * DPR]], // zone où le texte peut vivre content: [8 * DPR, 5 * DPR, (W - 8) * DPR, (H - 5) * DPR], }; if (!map.hasImage("pill")) map.addImage("pill", drawPill("#ffffff", "rgba(26,18,20,0.35)"), meta); if (!map.hasImage("pill-deal")) map.addImage("pill-deal", drawPill("#e23744", "#b3202b"), meta); if (!map.hasImage("pill-active")) map.addImage("pill-active", drawPill("#1a1214", "#1a1214"), meta); } /** « 425 k$ », « 1,25 M$ » — format compact des pastilles. */ export function shortPrice(p: number | null | undefined): string { if (p == null) return "•"; if (p >= 1_000_000) return `${(p / 1_000_000).toLocaleString("fr-CA", { maximumFractionDigits: 2 })} M$`; if (p >= 10_000) return `${Math.round(p / 1000)} k$`; return `${p.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $`; }