spb/ora-ka Public
Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)
Python 80%
TypeScript 12.9%
CSS 6.8%
1// -----------------------------------------------------------------------------2// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// map/pills.ts : pastilles de prix pour la carte — images « étirables »5// (stretchable images MapLibre) générées au canvas : la pastille s'adapte6// à la largeur du texte (icon-text-fit) sans aucun sprite externe.7// -----------------------------------------------------------------------------8import type { Map as MLMap, StyleImageMetadata } from "maplibre-gl";910const R = 13; // rayon des coins (px logiques)11const H = 30; // hauteur de la pastille12const W = 64; // largeur de base (étirée au besoin)13const DPR = 2; // netteté écrans Retina1415/** roundRect avec repli manuel (Safari < 16 ne l'a pas). */16function rr(g: CanvasRenderingContext2D, x: number, y: number,17 w: number, h: number, r: number): void {18 if (typeof (g as any).roundRect === "function") {19 (g as any).roundRect(x, y, w, h, r);20 return;21 }22 g.moveTo(x + r, y);23 g.arcTo(x + w, y, x + w, y + h, r);24 g.arcTo(x + w, y + h, x, y + h, r);25 g.arcTo(x, y + h, x, y, r);26 g.arcTo(x, y, x + w, y, r);27 g.closePath();28}2930function drawPill(fill: string, stroke: string, shadow = true): ImageData {31 const cv = document.createElement("canvas");32 cv.width = W * DPR;33 cv.height = (H + 6) * DPR; // marge pour l'ombre + la pointe34 const g = cv.getContext("2d")!;35 g.scale(DPR, DPR);36 if (shadow) {37 g.shadowColor = "rgba(26,18,20,0.28)";38 g.shadowBlur = 5;39 g.shadowOffsetY = 2;40 }41 // corps arrondi42 g.beginPath();43 rr(g, 1, 1, W - 2, H - 2, R);44 g.fillStyle = fill;45 g.fill();46 g.shadowColor = "transparent";47 // petite pointe sous la pastille (ancrage visuel sur la propriété)48 g.beginPath();49 g.moveTo(W / 2 - 5, H - 1.5);50 g.lineTo(W / 2, H + 4);51 g.lineTo(W / 2 + 5, H - 1.5);52 g.closePath();53 g.fillStyle = fill;54 g.fill();55 g.strokeStyle = stroke;56 g.lineWidth = 1.4;57 g.beginPath();58 rr(g, 1, 1, W - 2, H - 2, R);59 g.stroke();60 return g.getImageData(0, 0, cv.width, cv.height);61}6263/** Enregistre les variantes de pastille dans la carte. */64export function addPills(map: MLMap): void {65 const meta: Partial<StyleImageMetadata> = {66 pixelRatio: DPR,67 // zones étirables entre les coins arrondis — coordonnées PHYSIQUES (× DPR)68 // et non dégénérées (x1 < x2), sinon MapLibre rejette l'image en silence69 stretchX: [[(R + 1) * DPR, (W - R - 1) * DPR]],70 stretchY: [[(R + 1) * DPR, (H - R - 1) * DPR]],71 // zone où le texte peut vivre72 content: [8 * DPR, 5 * DPR, (W - 8) * DPR, (H - 5) * DPR],73 };74 if (!map.hasImage("pill"))75 map.addImage("pill", drawPill("#ffffff", "rgba(26,18,20,0.35)"), meta);76 if (!map.hasImage("pill-deal"))77 map.addImage("pill-deal", drawPill("#e23744", "#b3202b"), meta);78 if (!map.hasImage("pill-active"))79 map.addImage("pill-active", drawPill("#1a1214", "#1a1214"), meta);80}8182/** « 425 k$ », « 1,25 M$ » — format compact des pastilles. */83export function shortPrice(p: number | null | undefined): string {84 if (p == null) return "•";85 if (p >= 1_000_000)86 return `${(p / 1_000_000).toLocaleString("fr-CA", { maximumFractionDigits: 2 })} M$`;87 if (p >= 10_000) return `${Math.round(p / 1000)} k$`;88 return `${p.toLocaleString("fr-CA", { maximumFractionDigits: 0 })} $`;89}90