feat(map): refactor con 6 variables toggleables (listings/entire/revenue/licencia/hut_plazas/hut_registros)
This commit is contained in:
+297
-151
@@ -11,6 +11,22 @@ const filterOptions = [
|
||||
{ value: "Formentera", label: "Formentera" },
|
||||
];
|
||||
|
||||
interface VariableOption {
|
||||
value: string;
|
||||
labelKey: string;
|
||||
descKey: string;
|
||||
scope: "airbnb" | "hut";
|
||||
}
|
||||
|
||||
const variableOptions: VariableOption[] = [
|
||||
{ value: "airbnb_listings", labelKey: "map.variable_airbnb_listings", descKey: "map.variable_airbnb_listings_desc", scope: "airbnb" },
|
||||
{ value: "airbnb_entire_homes", labelKey: "map.variable_airbnb_entire", descKey: "map.variable_airbnb_entire_desc", scope: "airbnb" },
|
||||
{ value: "airbnb_revenue_total", labelKey: "map.variable_airbnb_revenue", descKey: "map.variable_airbnb_revenue_desc", scope: "airbnb" },
|
||||
{ value: "airbnb_con_licencia", labelKey: "map.variable_airbnb_con_licencia", descKey: "map.variable_airbnb_con_licencia_desc", scope: "airbnb" },
|
||||
{ value: "hut_plazas", labelKey: "map.variable_hut_plazas", descKey: "map.variable_hut_plazas_desc", scope: "hut" },
|
||||
{ value: "hut_registros", labelKey: "map.variable_hut_registros", descKey: "map.variable_hut_registros_desc", scope: "hut" },
|
||||
];
|
||||
|
||||
const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
---
|
||||
|
||||
@@ -40,7 +56,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute top-3 left-3 z-20 bg-white/95 backdrop-blur border border-stone-200 rounded-lg shadow-sm p-3 max-w-xs"
|
||||
class="absolute top-3 left-3 z-20 bg-white/95 backdrop-blur border border-stone-200 rounded-lg shadow-sm p-3 w-64 max-w-[calc(100vw-1.5rem)]"
|
||||
>
|
||||
<div class="mb-2">
|
||||
<label for="isla-filter" class="block text-xs font-medium text-stone-700 mb-1">
|
||||
@@ -55,6 +71,22 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-stone-200 pt-2 mb-2">
|
||||
<label for="variable-select" class="block text-xs font-medium text-stone-700 mb-1">
|
||||
{t(lang, "map.variable_label")}
|
||||
</label>
|
||||
<select
|
||||
id="variable-select"
|
||||
class="w-full text-sm border border-stone-300 rounded px-2 py-1 bg-white"
|
||||
>
|
||||
{variableOptions.map((o) => (
|
||||
<option value={o.value} data-scope={o.scope}>{t(lang, o.labelKey)}</option>
|
||||
))}
|
||||
</select>
|
||||
<p id="variable-desc" class="text-[10px] text-stone-500 mt-1 leading-tight"></p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-stone-200 pt-2">
|
||||
<p class="text-xs font-medium text-stone-700 mb-1">
|
||||
{t(lang, "map.layer_label")}
|
||||
@@ -76,45 +108,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
<p class="font-medium text-stone-800 mb-2">
|
||||
{t(lang, "map.legend_title")}
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
<p class="font-semibold text-stone-700">
|
||||
{t(lang, "map.legend_airbnb_title")}
|
||||
</p>
|
||||
<p class="text-stone-500 mb-1">
|
||||
{t(lang, "map.legend_airbnb_desc")}
|
||||
</p>
|
||||
<div class="flex items-center gap-0.5 h-2 rounded overflow-hidden">
|
||||
<span class="flex-1" style="background:#fef3c7"></span>
|
||||
<span class="flex-1" style="background:#fde68a"></span>
|
||||
<span class="flex-1" style="background:#fbbf24"></span>
|
||||
<span class="flex-1" style="background:#f97316"></span>
|
||||
<span class="flex-1" style="background:#c2410c"></span>
|
||||
<span class="flex-1" style="background:#7c2d12"></span>
|
||||
</div>
|
||||
<div class="flex justify-between text-[10px] text-stone-500 mt-0.5">
|
||||
<span>0</span>
|
||||
<span>100+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-stone-700">
|
||||
{t(lang, "map.legend_hut_title")}
|
||||
</p>
|
||||
<p class="text-stone-500 mb-1">
|
||||
{t(lang, "map.legend_hut_desc")}
|
||||
</p>
|
||||
<div class="flex items-center gap-0.5 h-2 rounded overflow-hidden">
|
||||
<span class="flex-1" style="background:#e0f2fe"></span>
|
||||
<span class="flex-1" style="background:#7dd3fc"></span>
|
||||
<span class="flex-1" style="background:#0ea5e9"></span>
|
||||
<span class="flex-1" style="background:#0369a1"></span>
|
||||
<span class="flex-1" style="background:#0c4a6e"></span>
|
||||
</div>
|
||||
<div class="flex justify-between text-[10px] text-stone-500 mt-0.5">
|
||||
<span>0</span>
|
||||
<span>7K+</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="legend-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,35 +124,154 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
const lang = (shell.dataset.lang as "ca" | "es") ?? "ca";
|
||||
const popupTitleTmpl = shell.dataset.popupTitleTmpl ?? "Secció {cusec}";
|
||||
|
||||
const t_ca: Record<string, string> = {
|
||||
municipi: "Municipi",
|
||||
isla: "Illa",
|
||||
const T: Record<string, string> = {
|
||||
municipi: lang === "es" ? "Municipio" : "Municipi",
|
||||
isla: lang === "es" ? "Isla" : "Illa",
|
||||
listings: "Listings Airbnb",
|
||||
hut_registros: "Registres HUT",
|
||||
hut_plazas: "Places HUT",
|
||||
granularitat: "Granularitat",
|
||||
sense_dades: "Sense dades",
|
||||
seccion_censal: "secció censal",
|
||||
entire_homes: lang === "es" ? "Pisos enteros" : "Pisos sencers",
|
||||
revenue: lang === "es" ? "Ingresos L365d" : "Ingressos L365d",
|
||||
con_licencia: lang === "es" ? "Con licencia Airbnb" : "Amb llicència Airbnb",
|
||||
hut_registros: lang === "es" ? "Registros HUT" : "Registres HUT",
|
||||
hut_plazas: lang === "es" ? "Plazas HUT" : "Places HUT",
|
||||
granularitat: lang === "es" ? "Granularidad" : "Granularitat",
|
||||
seccion_censal: lang === "es" ? "sección censal" : "secció censal",
|
||||
municipio: "municipi",
|
||||
illa: "illa",
|
||||
list: "Llista",
|
||||
empty: lang === "es" ? "Sin datos" : "Sense dades",
|
||||
legend_applies_to: lang === "es" ? "Aplica a" : "Aplica a",
|
||||
legend_islands_mm: lang === "es" ? "Mallorca / Menorca (sección censal)" : "Mallorca / Menorca (secció censal)",
|
||||
legend_islands_ef: lang === "es" ? "Ibiza / Formentera (municipio)" : "Eivissa / Formentera (municipi)",
|
||||
};
|
||||
const t_es: Record<string, string> = {
|
||||
municipi: "Municipio",
|
||||
isla: "Isla",
|
||||
listings: "Listings Airbnb",
|
||||
hut_registros: "Registros HUT",
|
||||
hut_plazas: "Plazas HUT",
|
||||
granularitat: "Granularidad",
|
||||
sense_dades: "Sin datos",
|
||||
seccion_censal: "sección censal",
|
||||
municipio: "municipio",
|
||||
illa: "isla",
|
||||
list: "Lista",
|
||||
|
||||
// Descriptions per variable (used in selector helper text and legend).
|
||||
const DESC: Record<string, string> = {
|
||||
airbnb_listings:
|
||||
lang === "es"
|
||||
? "Total de listings activos por sección censal"
|
||||
: "Total de listings actius per secció censal",
|
||||
airbnb_entire_homes:
|
||||
lang === "es"
|
||||
? "Listings que son pisos completos — proxy de conversión residencial → turístico"
|
||||
: "Listings que són pisos complets — proxy de conversió residencial → turístic",
|
||||
airbnb_revenue_total:
|
||||
lang === "es"
|
||||
? "Suma estimada de ingresos de los últimos 365 días por sección"
|
||||
: "Suma estimada dels ingressos dels darrers 365 dies per secció",
|
||||
airbnb_con_licencia:
|
||||
lang === "es"
|
||||
? "Listings con campo `license` rellenado (autorreportado, no validado)"
|
||||
: "Listings amb camp `license` emplenat (autorreportat, no validat)",
|
||||
hut_plazas:
|
||||
lang === "es"
|
||||
? "Plazas registradas en HUT del Consell d'Eivissa (municipio)"
|
||||
: "Places registrades al HUT del Consell d'Eivissa (municipi)",
|
||||
hut_registros:
|
||||
lang === "es"
|
||||
? "Número de registros HUT activos (municipio)"
|
||||
: "Nombre de registres HUT actius (municipi)",
|
||||
};
|
||||
const T = lang === "es" ? t_es : t_ca;
|
||||
|
||||
// Variable → paint + legend. Airbnb colors are warm (orange),
|
||||
// HUT colors are cool (blue). Each entry's stops double as legend ticks.
|
||||
const LAYERS: Record<string, {
|
||||
scope: "airbnb" | "hut";
|
||||
field: string;
|
||||
colorExpr: any;
|
||||
stops: number[];
|
||||
colors: string[];
|
||||
label: string;
|
||||
desc: string;
|
||||
appliesTo: string;
|
||||
}> = {
|
||||
airbnb_listings: {
|
||||
scope: "airbnb",
|
||||
field: "airbnb_listings",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "airbnb_listings"], 0],
|
||||
"#fef3c7", 1, "#fde68a", 10, "#fbbf24", 50, "#f97316", 150, "#c2410c", 400, "#7c2d12",
|
||||
],
|
||||
stops: [0, 1, 10, 50, 150, 400],
|
||||
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
|
||||
label: T.listings,
|
||||
desc: DESC.airbnb_listings,
|
||||
appliesTo: T.legend_islands_mm,
|
||||
},
|
||||
airbnb_entire_homes: {
|
||||
scope: "airbnb",
|
||||
field: "airbnb_entire_homes",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "airbnb_entire_homes"], 0],
|
||||
"#fef3c7", 1, "#fde68a", 5, "#fbbf24", 30, "#f97316", 100, "#c2410c", 250, "#7c2d12",
|
||||
],
|
||||
stops: [0, 1, 5, 30, 100, 250],
|
||||
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
|
||||
label: T.entire_homes,
|
||||
desc: DESC.airbnb_entire_homes,
|
||||
appliesTo: T.legend_islands_mm,
|
||||
},
|
||||
airbnb_revenue_total: {
|
||||
scope: "airbnb",
|
||||
field: "airbnb_revenue_total",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "airbnb_revenue_total"], 0],
|
||||
"#fef3c7", 100000, "#fde68a", 500000, "#fbbf24",
|
||||
2000000, "#f97316", 5000000, "#c2410c", 10000000, "#7c2d12",
|
||||
],
|
||||
stops: [0, 100000, 500000, 2000000, 5000000, 10000000],
|
||||
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
|
||||
label: T.revenue,
|
||||
desc: DESC.airbnb_revenue_total,
|
||||
appliesTo: T.legend_islands_mm,
|
||||
},
|
||||
airbnb_con_licencia: {
|
||||
scope: "airbnb",
|
||||
field: "airbnb_con_licencia",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "airbnb_con_licencia"], 0],
|
||||
"#fef3c7", 1, "#fde68a", 10, "#fbbf24", 50, "#f97316", 150, "#c2410c", 400, "#7c2d12",
|
||||
],
|
||||
stops: [0, 1, 10, 50, 150, 400],
|
||||
colors: ["#fef3c7", "#fde68a", "#fbbf24", "#f97316", "#c2410c", "#7c2d12"],
|
||||
label: T.con_licencia,
|
||||
desc: DESC.airbnb_con_licencia,
|
||||
appliesTo: T.legend_islands_mm,
|
||||
},
|
||||
hut_plazas: {
|
||||
scope: "hut",
|
||||
field: "hut_plazas",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "hut_plazas"], 0],
|
||||
"#e0f2fe", 1, "#7dd3fc", 1000, "#0ea5e9", 3000, "#0369a1", 7000, "#0c4a6e",
|
||||
],
|
||||
stops: [0, 1, 1000, 3000, 7000],
|
||||
colors: ["#e0f2fe", "#7dd3fc", "#0ea5e9", "#0369a1", "#0c4a6e", "#0c4a6e"],
|
||||
label: T.hut_plazas,
|
||||
desc: DESC.hut_plazas,
|
||||
appliesTo: T.legend_islands_ef,
|
||||
},
|
||||
hut_registros: {
|
||||
scope: "hut",
|
||||
field: "hut_registros",
|
||||
colorExpr: [
|
||||
"step", ["coalesce", ["get", "hut_registros"], 0],
|
||||
"#e0f2fe", 1, "#7dd3fc", 200, "#0ea5e9", 1000, "#0369a1", 2000, "#0c4a6e",
|
||||
],
|
||||
stops: [0, 1, 200, 1000, 2000],
|
||||
colors: ["#e0f2fe", "#7dd3fc", "#0ea5e9", "#0369a1", "#0c4a6e", "#0c4a6e"],
|
||||
label: T.hut_registros,
|
||||
desc: DESC.hut_registros,
|
||||
appliesTo: T.legend_islands_ef,
|
||||
},
|
||||
};
|
||||
|
||||
const fmt = (n: number) =>
|
||||
new Intl.NumberFormat(lang === "ca" ? "ca-ES" : "es-ES").format(n);
|
||||
const fmtShort = (n: number) => {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`;
|
||||
return fmt(n);
|
||||
};
|
||||
const fmtMoney = (n: number) => `$${fmtShort(n)}`;
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: "map",
|
||||
@@ -179,11 +292,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: "background",
|
||||
type: "background",
|
||||
paint: { "background-color": "#f5f5f4" },
|
||||
},
|
||||
{ id: "background", type: "background", paint: { "background-color": "#f5f5f4" } },
|
||||
{
|
||||
id: "secciones-line",
|
||||
type: "line",
|
||||
@@ -191,10 +300,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
"source-layer": "cases",
|
||||
paint: {
|
||||
"line-color": "#a8a29e",
|
||||
"line-width": [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
6, 0.1, 10, 0.3, 13, 0.6,
|
||||
],
|
||||
"line-width": ["interpolate", ["linear"], ["zoom"], 6, 0.1, 10, 0.3, 13, 0.6],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -204,12 +310,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
"source-layer": "cases",
|
||||
filter: ["match", ["get", "isla"], ["Mallorca", "Menorca"], true, false],
|
||||
paint: {
|
||||
"fill-color": [
|
||||
"step",
|
||||
["coalesce", ["get", "airbnb_listings"], 0],
|
||||
"#fef3c7", 1, "#fde68a", 10, "#fbbf24",
|
||||
50, "#f97316", 150, "#c2410c", 400, "#7c2d12",
|
||||
],
|
||||
"fill-color": LAYERS.airbnb_listings.colorExpr,
|
||||
"fill-opacity": 0.7,
|
||||
},
|
||||
},
|
||||
@@ -220,12 +321,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
"source-layer": "cases",
|
||||
filter: ["match", ["get", "isla"], ["Eivissa", "Formentera"], true, false],
|
||||
paint: {
|
||||
"fill-color": [
|
||||
"step",
|
||||
["coalesce", ["get", "hut_plazas"], 0],
|
||||
"#e0f2fe", 1, "#7dd3fc", 1000, "#0ea5e9",
|
||||
3000, "#0369a1", 7000, "#0c4a6e",
|
||||
],
|
||||
"fill-color": LAYERS.hut_plazas.colorExpr,
|
||||
"fill-opacity": 0.7,
|
||||
},
|
||||
},
|
||||
@@ -237,8 +333,7 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
paint: {
|
||||
"circle-color": [
|
||||
"step", ["get", "point_count"],
|
||||
"#fde68a", 10, "#fbbf24", 50, "#f97316",
|
||||
200, "#c2410c", 1000, "#7c2d12",
|
||||
"#fde68a", 10, "#fbbf24", 50, "#f97316", 200, "#c2410c", 1000, "#7c2d12",
|
||||
],
|
||||
"circle-radius": [
|
||||
"step", ["get", "point_count"],
|
||||
@@ -285,10 +380,6 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");
|
||||
map.addControl(new maplibregl.ScaleControl({ unit: "metric" }), "bottom-left");
|
||||
|
||||
// En Astro 7 los <script> se cargan como modulo defer, asi que el
|
||||
// evento "load" del map puede dispararse ANTES de que registremos
|
||||
// el handler. Patron seguro: chequear isStyleLoaded() y si ya paso,
|
||||
// ejecutar el callback inmediatamente.
|
||||
let readyDone = false;
|
||||
const onReady = (cb: () => void) => {
|
||||
if (readyDone) return;
|
||||
@@ -300,7 +391,6 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
} else {
|
||||
map.once("load", () => onReady(hideLoading));
|
||||
}
|
||||
// Safety: si el source pmtiles rompe, isStyleLoaded nunca sera true.
|
||||
setTimeout(() => onReady(hideLoading), 3000);
|
||||
|
||||
function hideLoading() {
|
||||
@@ -308,72 +398,119 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
if (loading) loading.style.display = "none";
|
||||
}
|
||||
|
||||
// Active variable starts as the default paint already set in style.
|
||||
let activeVariable = "airbnb_listings";
|
||||
|
||||
function applyVariable(variable: string) {
|
||||
const cfg = LAYERS[variable];
|
||||
if (!cfg) return;
|
||||
activeVariable = variable;
|
||||
|
||||
if (cfg.scope === "airbnb") {
|
||||
map.setPaintProperty("secciones-airbnb-fill", "fill-color", cfg.colorExpr);
|
||||
map.setPaintProperty("secciones-hut-fill", "fill-color", ["literal", "rgba(0,0,0,0)"]);
|
||||
map.setLayoutProperty("secciones-airbnb-fill", "visibility", "visible");
|
||||
map.setLayoutProperty("secciones-hut-fill", "visibility", "none");
|
||||
} else {
|
||||
map.setPaintProperty("secciones-hut-fill", "fill-color", cfg.colorExpr);
|
||||
map.setPaintProperty("secciones-airbnb-fill", "fill-color", ["literal", "rgba(0,0,0,0)"]);
|
||||
map.setLayoutProperty("secciones-hut-fill", "visibility", "visible");
|
||||
map.setLayoutProperty("secciones-airbnb-fill", "visibility", "none");
|
||||
}
|
||||
|
||||
updateLegend(cfg);
|
||||
const desc = document.getElementById("variable-desc");
|
||||
if (desc) desc.textContent = cfg.desc;
|
||||
}
|
||||
|
||||
function formatTick(n: number): string {
|
||||
if (n === 0) return "0";
|
||||
if (activeVariable === "airbnb_revenue_total") return fmtMoney(n);
|
||||
return fmtShort(n);
|
||||
}
|
||||
|
||||
function updateLegend(cfg: typeof LAYERS[string]) {
|
||||
const el = document.getElementById("legend-content");
|
||||
if (!el) return;
|
||||
const swatches = cfg.colors
|
||||
.map((c) => `<span class="flex-1" style="background:${c}"></span>`)
|
||||
.join("");
|
||||
const labels = cfg.stops
|
||||
.map((s, i) =>
|
||||
i === 0 || i === cfg.stops.length - 1
|
||||
? `<span>${formatTick(s)}${i === cfg.stops.length - 1 ? "+" : ""}</span>`
|
||||
: `<span></span>`,
|
||||
)
|
||||
.join("");
|
||||
el.innerHTML = `
|
||||
<p class="font-semibold text-stone-700 mb-1">${escape(cfg.label)}</p>
|
||||
<p class="text-stone-500 mb-1 leading-tight">${escape(cfg.desc)}</p>
|
||||
<div class="flex items-center gap-0.5 h-2 rounded overflow-hidden mb-0.5">${swatches}</div>
|
||||
<div class="flex justify-between text-[10px] text-stone-500 mb-2">${labels}</div>
|
||||
<p class="text-[10px] text-stone-500 italic">${T.legend_applies_to}: ${escape(cfg.appliesTo)}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
map.on("load", () => {
|
||||
const applyFilter = (isla: string) => {
|
||||
const filterExpr =
|
||||
isla === "all"
|
||||
? null
|
||||
: (["==", ["get", "isla"], isla] as unknown as maplibregl.FilterSpecification);
|
||||
["secciones-airbnb-fill", "secciones-hut-fill", "secciones-line"].forEach(
|
||||
(id) => {
|
||||
const filterExpr =
|
||||
isla === "all"
|
||||
? null
|
||||
: (["==", ["get", "isla"], isla] as unknown as maplibregl.FilterSpecification);
|
||||
["secciones-airbnb-fill", "secciones-hut-fill", "secciones-line"].forEach((id) => {
|
||||
if (map.getLayer(id)) {
|
||||
map.setFilter(id, filterExpr);
|
||||
}
|
||||
},
|
||||
);
|
||||
if (isla === "all") {
|
||||
map.setFilter("airbnb-clusters", null);
|
||||
map.setFilter("airbnb-cluster-count", null);
|
||||
map.setFilter("airbnb-unclustered", null);
|
||||
} else if (isla === "Mallorca" || isla === "Menorca") {
|
||||
const f: maplibregl.FilterSpecification = ["==", ["get", "isla"], isla];
|
||||
map.setFilter("airbnb-clusters", ["all", ["has", "point_count"], f]);
|
||||
map.setFilter("airbnb-cluster-count", ["all", ["has", "point_count"], f]);
|
||||
map.setFilter("airbnb-unclustered", ["all", ["!", ["has", "point_count"]], f]);
|
||||
} else {
|
||||
map.setFilter("airbnb-clusters", ["==", ["literal", "__none__"], "__none__"]);
|
||||
map.setFilter("airbnb-cluster-count", ["==", ["literal", "__none__"], "__none__"]);
|
||||
map.setFilter("airbnb-unclustered", ["==", ["literal", "__none__"], "__none__"]);
|
||||
}
|
||||
};
|
||||
});
|
||||
if (isla === "all") {
|
||||
map.setFilter("airbnb-clusters", null);
|
||||
map.setFilter("airbnb-cluster-count", null);
|
||||
map.setFilter("airbnb-unclustered", null);
|
||||
} else if (isla === "Mallorca" || isla === "Menorca") {
|
||||
const f: maplibregl.FilterSpecification = ["==", ["get", "isla"], isla];
|
||||
map.setFilter("airbnb-clusters", ["all", ["has", "point_count"], f]);
|
||||
map.setFilter("airbnb-cluster-count", ["all", ["has", "point_count"], f]);
|
||||
map.setFilter("airbnb-unclustered", ["all", ["!", ["has", "point_count"]], f]);
|
||||
} else {
|
||||
const none: maplibregl.FilterSpecification = ["==", ["literal", "__none__"], "__none__"];
|
||||
map.setFilter("airbnb-clusters", none);
|
||||
map.setFilter("airbnb-cluster-count", none);
|
||||
map.setFilter("airbnb-unclustered", none);
|
||||
}
|
||||
};
|
||||
|
||||
const select = document.getElementById("isla-filter") as HTMLSelectElement;
|
||||
select?.addEventListener("change", () => applyFilter(select.value));
|
||||
if (select) applyFilter(select.value);
|
||||
|
||||
const variableSelect = document.getElementById("variable-select") as HTMLSelectElement;
|
||||
variableSelect?.addEventListener("change", () => applyVariable(variableSelect.value));
|
||||
if (variableSelect) applyVariable(variableSelect.value);
|
||||
|
||||
const toggleChoro = document.getElementById("toggle-choropleth") as HTMLInputElement;
|
||||
const toggleClu = document.getElementById("toggle-clusters") as HTMLInputElement;
|
||||
toggleChoro?.addEventListener("change", () => {
|
||||
const v = toggleChoro.checked ? "visible" : "none";
|
||||
["secciones-airbnb-fill", "secciones-hut-fill", "secciones-line"].forEach(
|
||||
(id) => {
|
||||
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", v);
|
||||
},
|
||||
);
|
||||
const activeFillId = LAYERS[activeVariable].scope === "airbnb"
|
||||
? "secciones-airbnb-fill"
|
||||
: "secciones-hut-fill";
|
||||
[activeFillId, "secciones-line"].forEach((id) => {
|
||||
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", v);
|
||||
});
|
||||
});
|
||||
toggleClu?.addEventListener("change", () => {
|
||||
const v = toggleClu.checked ? "visible" : "none";
|
||||
["airbnb-clusters", "airbnb-cluster-count", "airbnb-unclustered"].forEach(
|
||||
(id) => {
|
||||
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", v);
|
||||
},
|
||||
);
|
||||
["airbnb-clusters", "airbnb-cluster-count", "airbnb-unclustered"].forEach((id) => {
|
||||
if (map.getLayer(id)) map.setLayoutProperty(id, "visibility", v);
|
||||
});
|
||||
});
|
||||
|
||||
map.on("click", "secciones-airbnb-fill", (e) => openPopup(e, "airbnb"));
|
||||
map.on("click", "secciones-hut-fill", (e) => openPopup(e, "hut"));
|
||||
map.on("mouseenter", "secciones-airbnb-fill", () => {
|
||||
map.getCanvas().style.cursor = "pointer";
|
||||
});
|
||||
map.on("mouseleave", "secciones-airbnb-fill", () => {
|
||||
map.getCanvas().style.cursor = "";
|
||||
});
|
||||
map.on("mouseenter", "secciones-hut-fill", () => {
|
||||
map.getCanvas().style.cursor = "pointer";
|
||||
});
|
||||
map.on("mouseleave", "secciones-hut-fill", () => {
|
||||
map.getCanvas().style.cursor = "";
|
||||
});
|
||||
map.on("mouseenter", "secciones-airbnb-fill", () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", "secciones-airbnb-fill", () => { map.getCanvas().style.cursor = ""; });
|
||||
map.on("mouseenter", "secciones-hut-fill", () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", "secciones-hut-fill", () => { map.getCanvas().style.cursor = ""; });
|
||||
map.on("click", "airbnb-clusters", async (e) => {
|
||||
const features = map.queryRenderedFeatures(e.point, { layers: ["airbnb-clusters"] });
|
||||
const clusterId = features[0]?.properties?.cluster_id;
|
||||
@@ -383,12 +520,8 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
const coords = (features[0].geometry as { coordinates: [number, number] }).coordinates;
|
||||
map.easeTo({ center: coords, zoom });
|
||||
});
|
||||
map.on("mouseenter", "airbnb-clusters", () => {
|
||||
map.getCanvas().style.cursor = "pointer";
|
||||
});
|
||||
map.on("mouseleave", "airbnb-clusters", () => {
|
||||
map.getCanvas().style.cursor = "";
|
||||
});
|
||||
map.on("mouseenter", "airbnb-clusters", () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", "airbnb-clusters", () => { map.getCanvas().style.cursor = ""; });
|
||||
});
|
||||
|
||||
function openPopup(e: maplibregl.MapLayerMouseEvent, kind: "airbnb" | "hut") {
|
||||
@@ -398,21 +531,34 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
const cusec = String(p.CUSEC ?? "—");
|
||||
const title = popupTitleTmpl.replace("{cusec}", cusec);
|
||||
const gran = String(p.granularidad ?? "—");
|
||||
const granLabel = T[gran] ?? gran;
|
||||
const granLabel =
|
||||
gran === "seccion_censal" ? T.seccion_censal : gran === "municipio" ? T.municipio : gran;
|
||||
const listings = Number(p.airbnb_listings ?? 0);
|
||||
const entire = Number(p.airbnb_entire_homes ?? 0);
|
||||
const conLic = Number(p.airbnb_con_licencia ?? 0);
|
||||
const plazas = Number(p.airbnb_accommodates_total ?? 0);
|
||||
const revenue = Number(p.airbnb_revenue_total ?? 0);
|
||||
const regs = Number(p.hut_registros ?? 0);
|
||||
const hPlazas = Number(p.hut_plazas ?? 0);
|
||||
const lines: string[] = [];
|
||||
lines.push(`<div class="font-bold text-sm mb-1">${escape(title)}</div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.municipi}:</span> ${escape(String(p.NMUN ?? "—"))}</div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.isla}:</span> ${escape(String(p.isla ?? "—"))}</div>`);
|
||||
|
||||
const sectionTitle = (label: string) =>
|
||||
`<div class="text-[10px] uppercase tracking-wide text-stone-500 mt-2 mb-0.5 border-t border-stone-200 pt-1">${label}</div>`;
|
||||
|
||||
if (kind === "airbnb") {
|
||||
const listings = Number(p.airbnb_listings ?? 0);
|
||||
const plazas = Number(p.airbnb_accommodates_total ?? 0);
|
||||
lines.push(`<div class="text-xs text-stone-700 mt-1"><span class="text-stone-500">${T.listings}:</span> <b>${fmt(listings)}</b></div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(plazas)}</b></div>`);
|
||||
lines.push(sectionTitle("Airbnb"));
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings}:</span> <b>${fmt(listings)}</b></div>`);
|
||||
if (entire) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.entire_homes}:</span> <b>${fmt(entire)}</b></div>`);
|
||||
if (conLic) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.con_licencia}:</span> <b>${fmt(conLic)}</b></div>`);
|
||||
if (plazas) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.listings} (plazas):</span> <b>${fmt(plazas)}</b></div>`);
|
||||
if (revenue) lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.revenue}:</span> <b>${fmtMoney(revenue)}</b></div>`);
|
||||
} else {
|
||||
const regs = Number(p.hut_registros ?? 0);
|
||||
const plazas = Number(p.hut_plazas ?? 0);
|
||||
lines.push(`<div class="text-xs text-stone-700 mt-1"><span class="text-stone-500">${T.hut_registros}:</span> <b>${fmt(regs)}</b></div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(plazas)}</b></div>`);
|
||||
lines.push(sectionTitle("HUT"));
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_registros}:</span> <b>${fmt(regs)}</b></div>`);
|
||||
lines.push(`<div class="text-xs text-stone-700"><span class="text-stone-500">${T.hut_plazas}:</span> <b>${fmt(hPlazas)}</b></div>`);
|
||||
}
|
||||
lines.push(`<div class="text-[10px] text-stone-500 mt-1 italic">${T.granularitat}: ${granLabel}</div>`);
|
||||
new Popup({ closeButton: true })
|
||||
@@ -428,4 +574,4 @@ const popupTitleTmpl = t(lang, "map.popup_title");
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -67,7 +67,25 @@
|
||||
"layer_label": "Capa",
|
||||
"layer_choropleth": "Plànol coroplètic",
|
||||
"layer_clusters": "Punts Airbnb (clústers)",
|
||||
"variable_label": "Variable",
|
||||
"variable_airbnb_listings": "Listings Airbnb",
|
||||
"variable_airbnb_listings_desc": "Total de listings actius per secció censal",
|
||||
"variable_airbnb_entire": "Pisos sencers (entire home/apt)",
|
||||
"variable_airbnb_entire_desc": "Listings que són pisos complets — proxy de conversió residencial → turístic",
|
||||
"variable_airbnb_revenue": "Ingressos Airbnb L365d",
|
||||
"variable_airbnb_revenue_desc": "Suma estimada dels ingressos dels darrers 365 dies per secció",
|
||||
"variable_airbnb_con_licencia": "Listings amb llicència Airbnb",
|
||||
"variable_airbnb_con_licencia_desc": "Listings amb camp `license` emplenat (autorreportat, no validat pel Govern)",
|
||||
"variable_hut_plazas": "Places HUT legals",
|
||||
"variable_hut_plazas_desc": "Places registrades al registre HUT del Consell d'Eivissa (municipi)",
|
||||
"variable_hut_registros": "Registres HUT",
|
||||
"variable_hut_registros_desc": "Nombre de registres HUT actius (municipi)",
|
||||
"legend_title": "Llegenda",
|
||||
"legend_scale_label": "Escala",
|
||||
"legend_applies_to": "Aplica a",
|
||||
"legend_islands_mallorca_menorca": "Mallorca / Menorca (secció censal)",
|
||||
"legend_islands_eivissa_formentera": "Eivissa / Formentera (municipi)",
|
||||
"legend_empty": "Sense dades per a aquesta variable en aquesta illa",
|
||||
"legend_airbnb_title": "Mallorca / Menorca",
|
||||
"legend_airbnb_desc": "Listings actius a Airbnb per secció censal",
|
||||
"legend_hut_title": "Eivissa / Formentera",
|
||||
|
||||
@@ -67,7 +67,25 @@
|
||||
"layer_label": "Capa",
|
||||
"layer_choropleth": "Plano coroplético",
|
||||
"layer_clusters": "Puntos Airbnb (clústeres)",
|
||||
"variable_label": "Variable",
|
||||
"variable_airbnb_listings": "Listings Airbnb",
|
||||
"variable_airbnb_listings_desc": "Total de listings activos por sección censal",
|
||||
"variable_airbnb_entire": "Pisos enteros (entire home/apt)",
|
||||
"variable_airbnb_entire_desc": "Listings que son pisos completos — proxy de conversión residencial → turístico",
|
||||
"variable_airbnb_revenue": "Ingresos Airbnb L365d",
|
||||
"variable_airbnb_revenue_desc": "Suma estimada de ingresos de los últimos 365 días por sección",
|
||||
"variable_airbnb_con_licencia": "Listings con licencia Airbnb",
|
||||
"variable_airbnb_con_licencia_desc": "Listings con campo `license` rellenado (autorreportado, no validado por Govern)",
|
||||
"variable_hut_plazas": "Plazas HUT legales",
|
||||
"variable_hut_plazas_desc": "Plazas registradas en el registro HUT del Consell d'Eivissa (municipio)",
|
||||
"variable_hut_registros": "Registros HUT",
|
||||
"variable_hut_registros_desc": "Número de registros HUT activos (municipio)",
|
||||
"legend_title": "Leyenda",
|
||||
"legend_scale_label": "Escala",
|
||||
"legend_applies_to": "Aplica a",
|
||||
"legend_islands_mallorca_menorca": "Mallorca / Menorca (sección censal)",
|
||||
"legend_islands_eivissa_formentera": "Ibiza / Formentera (municipio)",
|
||||
"legend_empty": "Sin datos para esta variable en esta isla",
|
||||
"legend_airbnb_title": "Mallorca / Menorca",
|
||||
"legend_airbnb_desc": "Listings activos en Airbnb por sección censal",
|
||||
"legend_hut_title": "Ibiza / Formentera",
|
||||
|
||||
Reference in New Issue
Block a user