feat(tickets): teardrop pin icons; closed = one slate colour, status ignored
Render INC tickets as canvas-drawn teardrop map pins via MapLibre symbol layers (scales to thousands of closed features, unlike DOM markers): - Open pins coloured by SLA state; larger than the old circles for hierarchy. - Closed pins use a single muted slate colour irrespective of status (the only distinction that matters once closed), slightly smaller + under the open layer. - Legend/popup closed swatch aligned to the new closed colour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6504328e58
commit
0ed9b6a252
1 changed files with 47 additions and 18 deletions
|
|
@ -884,6 +884,7 @@ const EMPTY_FC = { type: 'FeatureCollection', features: [] };
|
|||
// SLA state → colour (open layer + legend); mirrors the warm-dark palette.
|
||||
const SLA_COLORS = { breached: '#ef5b5b', at_risk: '#f0a93b', ok: '#2dd4a7', unknown: '#6b7280' };
|
||||
const SLA_LABELS = { breached: 'Breached', at_risk: 'At risk', ok: 'OK', unknown: 'Unknown' };
|
||||
const CLOSED_COLOR = '#94a3b8'; // muted slate — closed tickets (status irrelevant)
|
||||
|
||||
const COST_CENTRE_COLORS = {
|
||||
'isp': '#3b82f6', 'osp': '#E8954A', 'osp patrol': '#f97316', 'fds': '#22c55e',
|
||||
|
|
@ -945,6 +946,32 @@ function incQs() {
|
|||
return p.toString();
|
||||
}
|
||||
|
||||
// Draw a teardrop map-pin (head circle + point) at 2x for crispness; returns
|
||||
// ImageData added to the map style. Recoloured per SLA state / closed.
|
||||
function pinImageData(fill) {
|
||||
const r = 22, sw = 4, cx = r + sw, cy = r + sw, tip = cy + r * 2.55;
|
||||
const w = 2 * (r + sw), h = Math.ceil(tip + sw);
|
||||
const cv = document.createElement('canvas'); cv.width = w; cv.height = h;
|
||||
const ctx = cv.getContext('2d');
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, Math.PI * 0.75, Math.PI * 0.25, false); // top ~270° of the head
|
||||
ctx.lineTo(cx, tip); // taper to the tip
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fill; ctx.fill();
|
||||
ctx.lineWidth = sw; ctx.strokeStyle = 'rgba(255,255,255,.95)'; ctx.lineJoin = 'round'; ctx.stroke();
|
||||
ctx.beginPath(); ctx.arc(cx, cy, r * 0.4, 0, 2 * Math.PI); // inner white hole
|
||||
ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.fill();
|
||||
return ctx.getImageData(0, 0, w, h);
|
||||
}
|
||||
function addPinImages() {
|
||||
const pins = {
|
||||
'pin-breached': SLA_COLORS.breached, 'pin-at_risk': SLA_COLORS.at_risk,
|
||||
'pin-ok': SLA_COLORS.ok, 'pin-unknown': SLA_COLORS.unknown, 'pin-closed': CLOSED_COLOR,
|
||||
};
|
||||
for (const [id, fill] of Object.entries(pins))
|
||||
if (!tkMap.hasImage(id)) tkMap.addImage(id, pinImageData(fill), { pixelRatio: 2 });
|
||||
}
|
||||
|
||||
function initIncMap() {
|
||||
if (tkMap) { tkMap.resize(); return; } // already built — just fix sizing
|
||||
|
||||
|
|
@ -966,28 +993,30 @@ function initIncMap() {
|
|||
tkPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 14 });
|
||||
|
||||
tkMap.on('load', () => {
|
||||
// Closed overlay (windowed) under the live open layer.
|
||||
addPinImages(); // teardrop pin icons (open by SLA, closed one slate colour)
|
||||
// Closed overlay (windowed) — drawn UNDER the live open layer; status irrelevant
|
||||
// so every closed ticket uses the single muted 'pin-closed', slightly smaller.
|
||||
tkMap.addSource('inc-closed', { type: 'geojson', data: EMPTY_FC });
|
||||
tkMap.addLayer({
|
||||
id: 'inc-closed', type: 'circle', source: 'inc-closed',
|
||||
layout: { visibility: tkLayerState.closed ? 'visible' : 'none' },
|
||||
paint: {
|
||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 6, 3, 11, 5, 16, 7],
|
||||
'circle-color': '#6b7280', 'circle-opacity': 0.45,
|
||||
'circle-stroke-color': '#9ca3af', 'circle-stroke-width': 1,
|
||||
id: 'inc-closed', type: 'symbol', source: 'inc-closed',
|
||||
layout: {
|
||||
visibility: tkLayerState.closed ? 'visible' : 'none',
|
||||
'icon-image': 'pin-closed',
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 6, 0.42, 11, 0.6, 16, 0.78],
|
||||
'icon-anchor': 'bottom', 'icon-allow-overlap': true, 'icon-ignore-placement': true,
|
||||
},
|
||||
paint: { 'icon-opacity': 0.85 },
|
||||
});
|
||||
// Open layer (live) — coloured by derived SLA state.
|
||||
// Open layer (live) — pin colour = derived SLA state; larger for hierarchy.
|
||||
tkMap.addSource('inc-open', { type: 'geojson', data: EMPTY_FC });
|
||||
tkMap.addLayer({
|
||||
id: 'inc-open', type: 'circle', source: 'inc-open',
|
||||
layout: { visibility: tkLayerState.open ? 'visible' : 'none' },
|
||||
paint: {
|
||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 6, 4, 11, 6, 16, 9],
|
||||
'circle-color': ['match', ['get', 'sla_state'],
|
||||
'breached', SLA_COLORS.breached, 'at_risk', SLA_COLORS.at_risk, 'ok', SLA_COLORS.ok,
|
||||
SLA_COLORS.unknown],
|
||||
'circle-stroke-color': '#fff', 'circle-stroke-width': 1.5, 'circle-opacity': 0.92,
|
||||
id: 'inc-open', type: 'symbol', source: 'inc-open',
|
||||
layout: {
|
||||
visibility: tkLayerState.open ? 'visible' : 'none',
|
||||
'icon-image': ['match', ['get', 'sla_state'],
|
||||
'breached', 'pin-breached', 'at_risk', 'pin-at_risk', 'ok', 'pin-ok', 'pin-unknown'],
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 6, 0.52, 11, 0.78, 16, 1],
|
||||
'icon-anchor': 'bottom', 'icon-allow-overlap': true, 'icon-ignore-placement': true,
|
||||
},
|
||||
});
|
||||
for (const id of ['inc-open', 'inc-closed']) {
|
||||
|
|
@ -1183,7 +1212,7 @@ function showIncPopup(f, closed) {
|
|||
}
|
||||
if (p.geo_source === 'cluster') lines.push('<div class="row muted" style="font-size:10px">approx — cluster location</div>');
|
||||
tkPopup.setLngLat(f.geometry.coordinates).setHTML(`<div class="pop">
|
||||
<b>${escapeHtml(p.ticket_id || '—')} <span class="badge" style="color:${closed ? '#9ca3af' : (SLA_COLORS[p.sla_state] || '#fff')}">${closed ? 'CLOSED' : 'OPEN'}</span></b>
|
||||
<b>${escapeHtml(p.ticket_id || '—')} <span class="badge" style="color:${closed ? CLOSED_COLOR : (SLA_COLORS[p.sla_state] || '#fff')}">${closed ? 'CLOSED' : 'OPEN'}</span></b>
|
||||
${lines.join('')}</div>`).addTo(tkMap);
|
||||
}
|
||||
|
||||
|
|
@ -1191,7 +1220,7 @@ function buildIncLayers() {
|
|||
const m = (incData && incData.metrics) || {};
|
||||
const rows = [
|
||||
{ id: 'open', label: 'Open INC', color: SLA_COLORS.breached, n: m.open_now ?? 0 },
|
||||
{ id: 'closed', label: 'Closed INC', color: '#9ca3af', n: m.closed_in_window ?? 0 },
|
||||
{ id: 'closed', label: 'Closed INC', color: CLOSED_COLOR, n: m.closed_in_window ?? 0 },
|
||||
{ id: 'vehicles', label: 'Vehicles', color: '#E8954A', n: vehCount },
|
||||
];
|
||||
let html = rows.map((r) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue