mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
* feat(region-routing): P1 tagging + region_routes primitive + read/write API + preview launcher - config.py: add Coverage.region_tagging (bool=False); add RegionRouteMatrix dataclass (enabled, cells) above NotificationsConfig; add region_routes field to NotificationsConfig; add explicit hydration branch for region_routes in _dict_to_dataclass mirroring destinations pattern. - coverage_area.py: add MonitoringArea.name (str|None=None, frozen); update areas_from_config to preserve name; refactor inline geom extraction from classify_event_areas into shared _event_geom_json helper; add matching_area_names(geom_json, areas)->list[str] (additive, all named matches, config-order, deduped; gate unchanged); add event_region_names convenience wrapper. - coverage_filter.py: add region_tagging ctor kwarg; stamp event.region/ regions before the gate when region_tagging=True and areas non-empty and not event.regions (never clobbers satpass preset). - pipeline/__init__.py: wire region_tagging into CoverageFilter construction. - notification_routes.py: add GET /notifications/regions (named coverage area names, config-order, deduped); GET /notifications/region-routing (matrix as JSON); POST /notifications/region-routing (explicit RMW — only region_routes changes, toggles/rules/destinations survive). - scripts/preview_dashboard.py: mesh-free launcher — dashboard API only, no mesh connector, no broadcast loop; vite runs separately. All 87 coverage tests pass; 300 total pass; 6 pre-existing failures unchanged (adapter config count mismatch + MeshCore EventType.NEW_CONTACT). * feat(region-routing): manual region x family matrix editor page Adds RegionRoutingMatrix.tsx — a plain editor over the region_routes config primitive. Rows = families (via useFamilies()), cols = regions (from GET /api/notifications/regions). Each cell exposes MT channel (ChannelPicker single + includeDisabled), MC channel name (text input), min_severity select (routine/priority/critical/immediate), and an enabled checkbox. Only cells where MT or MC is set are included in the sparse POST payload. Master enable toggle maps to top-level enabled. MT budget guard warns when more than 7 distinct MT indices are in use. Sticky family column; horizontal scroll for wide region sets. Registers route /region-routing in App.tsx and adds "Region Routing" nav entry (Map icon) under the Meshtastic section in Layout.tsx, immediately after Routing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(region-routing): regions endpoint reads saved (disk) coverage so routing columns are dynamic without a bot restart; preview reloads config after writes * feat(routing): unify MT/MC routing into per-family cards; region routing as an in-card expand; remove rules/destinations UI + standalone page * refactor(routing): move Meshtastic Routing from /notifications to /meshtastic/routing (mirror /meshcore/routing); redirect legacy path * feat(region-routing): dispatcher honors region_routes matrix (authoritative-on-match, per-region cooldown, per-channel dedup); non-matrix path unchanged Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(region-routing): matrix dedup key must match boot-restore 2-tuple form (prevents restart re-broadcast flood); regression test --------- Co-authored-by: Matt Johnson <mj@k7zvx.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
4.1 KiB
JavaScript
149 lines
4.1 KiB
JavaScript
import {blur2, max, ticks} from "d3-array";
|
|
import {slice} from "./array.js";
|
|
import constant from "./constant.js";
|
|
import Contours from "./contours.js";
|
|
|
|
function defaultX(d) {
|
|
return d[0];
|
|
}
|
|
|
|
function defaultY(d) {
|
|
return d[1];
|
|
}
|
|
|
|
function defaultWeight() {
|
|
return 1;
|
|
}
|
|
|
|
export default function() {
|
|
var x = defaultX,
|
|
y = defaultY,
|
|
weight = defaultWeight,
|
|
dx = 960,
|
|
dy = 500,
|
|
r = 20, // blur radius
|
|
k = 2, // log2(grid cell size)
|
|
o = r * 3, // grid offset, to pad for blur
|
|
n = (dx + o * 2) >> k, // grid width
|
|
m = (dy + o * 2) >> k, // grid height
|
|
threshold = constant(20);
|
|
|
|
function grid(data) {
|
|
var values = new Float32Array(n * m),
|
|
pow2k = Math.pow(2, -k),
|
|
i = -1;
|
|
|
|
for (const d of data) {
|
|
var xi = (x(d, ++i, data) + o) * pow2k,
|
|
yi = (y(d, i, data) + o) * pow2k,
|
|
wi = +weight(d, i, data);
|
|
if (wi && xi >= 0 && xi < n && yi >= 0 && yi < m) {
|
|
var x0 = Math.floor(xi),
|
|
y0 = Math.floor(yi),
|
|
xt = xi - x0 - 0.5,
|
|
yt = yi - y0 - 0.5;
|
|
values[x0 + y0 * n] += (1 - xt) * (1 - yt) * wi;
|
|
values[x0 + 1 + y0 * n] += xt * (1 - yt) * wi;
|
|
values[x0 + 1 + (y0 + 1) * n] += xt * yt * wi;
|
|
values[x0 + (y0 + 1) * n] += (1 - xt) * yt * wi;
|
|
}
|
|
}
|
|
|
|
blur2({data: values, width: n, height: m}, r * pow2k);
|
|
return values;
|
|
}
|
|
|
|
function density(data) {
|
|
var values = grid(data),
|
|
tz = threshold(values),
|
|
pow4k = Math.pow(2, 2 * k);
|
|
|
|
// Convert number of thresholds into uniform thresholds.
|
|
if (!Array.isArray(tz)) {
|
|
tz = ticks(Number.MIN_VALUE, max(values) / pow4k, tz);
|
|
}
|
|
|
|
return Contours()
|
|
.size([n, m])
|
|
.thresholds(tz.map(d => d * pow4k))
|
|
(values)
|
|
.map((c, i) => (c.value = +tz[i], transform(c)));
|
|
}
|
|
|
|
density.contours = function(data) {
|
|
var values = grid(data),
|
|
contours = Contours().size([n, m]),
|
|
pow4k = Math.pow(2, 2 * k),
|
|
contour = value => {
|
|
value = +value;
|
|
var c = transform(contours.contour(values, value * pow4k));
|
|
c.value = value; // preserve exact threshold value
|
|
return c;
|
|
};
|
|
Object.defineProperty(contour, "max", {get: () => max(values) / pow4k});
|
|
return contour;
|
|
};
|
|
|
|
function transform(geometry) {
|
|
geometry.coordinates.forEach(transformPolygon);
|
|
return geometry;
|
|
}
|
|
|
|
function transformPolygon(coordinates) {
|
|
coordinates.forEach(transformRing);
|
|
}
|
|
|
|
function transformRing(coordinates) {
|
|
coordinates.forEach(transformPoint);
|
|
}
|
|
|
|
// TODO Optimize.
|
|
function transformPoint(coordinates) {
|
|
coordinates[0] = coordinates[0] * Math.pow(2, k) - o;
|
|
coordinates[1] = coordinates[1] * Math.pow(2, k) - o;
|
|
}
|
|
|
|
function resize() {
|
|
o = r * 3;
|
|
n = (dx + o * 2) >> k;
|
|
m = (dy + o * 2) >> k;
|
|
return density;
|
|
}
|
|
|
|
density.x = function(_) {
|
|
return arguments.length ? (x = typeof _ === "function" ? _ : constant(+_), density) : x;
|
|
};
|
|
|
|
density.y = function(_) {
|
|
return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), density) : y;
|
|
};
|
|
|
|
density.weight = function(_) {
|
|
return arguments.length ? (weight = typeof _ === "function" ? _ : constant(+_), density) : weight;
|
|
};
|
|
|
|
density.size = function(_) {
|
|
if (!arguments.length) return [dx, dy];
|
|
var _0 = +_[0], _1 = +_[1];
|
|
if (!(_0 >= 0 && _1 >= 0)) throw new Error("invalid size");
|
|
return dx = _0, dy = _1, resize();
|
|
};
|
|
|
|
density.cellSize = function(_) {
|
|
if (!arguments.length) return 1 << k;
|
|
if (!((_ = +_) >= 1)) throw new Error("invalid cell size");
|
|
return k = Math.floor(Math.log(_) / Math.LN2), resize();
|
|
};
|
|
|
|
density.thresholds = function(_) {
|
|
return arguments.length ? (threshold = typeof _ === "function" ? _ : Array.isArray(_) ? constant(slice.call(_)) : constant(_), density) : threshold;
|
|
};
|
|
|
|
density.bandwidth = function(_) {
|
|
if (!arguments.length) return Math.sqrt(r * (r + 1));
|
|
if (!((_ = +_) >= 0)) throw new Error("invalid bandwidth");
|
|
return r = (Math.sqrt(4 * _ * _ + 1) - 1) / 2, resize();
|
|
};
|
|
|
|
return density;
|
|
}
|