feat(dynamics): Dataverse FS layer + WW-DEMO sample GeoJSON
Sample-first Cesium overlay via createLocalGeoJsonLayer; normalize unit tests; docs pointer. No secrets.
This commit is contained in:
parent
4d13854d0d
commit
4ab771ac65
|
|
@ -30,4 +30,10 @@ This is not a closed ISV black box. It is a community-shaped bridge from OSINT-g
|
|||
|
||||
Early public versioning. Demo data only until security pass. No customer PHI. No live tech GPS in public builds until consent model is locked.
|
||||
|
||||
Repo: https://github.com/NukaSoft/gods-eye-view
|
||||
Repo: https://github.com/NukaSoft/gods-eye-view
|
||||
|
||||
## Layer module (eggbot 2026-09-09)
|
||||
- src/data/dataverseFs.js — Cesium overlay via local GeoJSON loader
|
||||
- Sample: public/samples/dataverse-fs-ww-demo.geojson`r
|
||||
- Env: VITE_DATAVERSE_FS_GEOJSON_URL (adapter URL); default = sample
|
||||
- Toggle appears with other local data layers (Dynamics Field Service)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"type": "FeatureCollection",
|
||||
"name": "dataverse-fs-ww-demo",
|
||||
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
|
||||
"properties": {
|
||||
"source": "sample",
|
||||
"org": "nukasoft.crm.dynamics.com",
|
||||
"theme": "WeatherWyze",
|
||||
"disclaimer": "Synthetic demo coordinates for GEV layer spike. Not live GPS. No real customer PHI. Scrub before any public PR."
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6681, 42.9634] },
|
||||
"properties": {
|
||||
"kind": "customer",
|
||||
"entity": "account",
|
||||
"id": "00000000-0000-0000-0000-000000000101",
|
||||
"name": "WW Demo Municipal Utilities",
|
||||
"status": "active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6702, 42.9610] },
|
||||
"properties": {
|
||||
"kind": "site",
|
||||
"entity": "msdyn_functionallocation",
|
||||
"id": "00000000-0000-0000-0000-000000000201",
|
||||
"name": "WW Site — River Pump Station",
|
||||
"status": "active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6655, 42.9662] },
|
||||
"properties": {
|
||||
"kind": "site",
|
||||
"entity": "msdyn_functionallocation",
|
||||
"id": "00000000-0000-0000-0000-000000000202",
|
||||
"name": "WW Site — North Substation",
|
||||
"status": "active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6702, 42.9611] },
|
||||
"properties": {
|
||||
"kind": "asset",
|
||||
"entity": "msdyn_customerasset",
|
||||
"id": "00000000-0000-0000-0000-000000000301",
|
||||
"name": "Pump P-12",
|
||||
"status": "active",
|
||||
"locationSource": "site"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6700, 42.9608] },
|
||||
"properties": {
|
||||
"kind": "workorder",
|
||||
"entity": "msdyn_workorder",
|
||||
"id": "00000000-0000-0000-0000-000000000401",
|
||||
"name": "WW-DEMO-001",
|
||||
"status": "open",
|
||||
"priority": "high"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6658, 42.9660] },
|
||||
"properties": {
|
||||
"kind": "workorder",
|
||||
"entity": "msdyn_workorder",
|
||||
"id": "00000000-0000-0000-0000-000000000402",
|
||||
"name": "WW-DEMO-002",
|
||||
"status": "open",
|
||||
"priority": "normal"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": { "type": "Point", "coordinates": [-85.6670, 42.9645] },
|
||||
"properties": {
|
||||
"kind": "resource",
|
||||
"entity": "bookableresource",
|
||||
"id": "00000000-0000-0000-0000-000000000501",
|
||||
"name": "WW Tech A (demo)",
|
||||
"status": "available",
|
||||
"locationSource": "start",
|
||||
"updated": "2026-09-09T18:00:00Z",
|
||||
"staleSec": 3600
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* @module dataverseFs
|
||||
* Dynamics 365 Field Service / Dataverse dispatch layer for God's Eye View.
|
||||
* Default path: GeoJSON sample or VITE_DATAVERSE_FS_GEOJSON_URL (adapter).
|
||||
* Browser does not call Dataverse OData by default — secrets stay in the adapter.
|
||||
*/
|
||||
|
||||
import { createLocalGeoJsonLayer } from './localGeojson.js';
|
||||
|
||||
export const DATAVERSE_FS_OVERLAY_SOURCE_ID = 'dataverse-fs';
|
||||
|
||||
const ALLOWED_KINDS = new Set(['customer', 'site', 'asset', 'workorder', 'resource', 'contact']);
|
||||
|
||||
/**
|
||||
* Normalize a GeoJSON FeatureCollection for the FS layer (unit-testable, no Cesium).
|
||||
* Drops features without finite lon/lat; keeps kind/id/name when present.
|
||||
* @param {object} collection
|
||||
* @returns {{type:string, features:object[]}}
|
||||
*/
|
||||
export function normalizeDataverseFsCollection(collection) {
|
||||
if (!collection || collection.type !== 'FeatureCollection' || !Array.isArray(collection.features)) {
|
||||
return { type: 'FeatureCollection', features: [] };
|
||||
}
|
||||
const features = [];
|
||||
for (const f of collection.features) {
|
||||
const coords = f?.geometry?.coordinates;
|
||||
if (!Array.isArray(coords) || coords.length < 2) continue;
|
||||
if (coords[0] == null || coords[1] == null) continue;
|
||||
const lon = Number(coords[0]);
|
||||
const lat = Number(coords[1]);
|
||||
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
||||
const props = f.properties && typeof f.properties === 'object' ? { ...f.properties } : {};
|
||||
if (props.kind && !ALLOWED_KINDS.has(String(props.kind))) {
|
||||
// keep unknown kinds out of semantic success counts, but still allow render if coords valid
|
||||
// for v1 we skip unknown kinds to avoid false greens
|
||||
continue;
|
||||
}
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [lon, lat] },
|
||||
properties: {
|
||||
kind: props.kind ? String(props.kind) : 'site',
|
||||
entity: props.entity ? String(props.entity) : 'unknown',
|
||||
id: props.id != null ? String(props.id) : undefined,
|
||||
name: props.name != null ? String(props.name) : 'Untitled',
|
||||
status: props.status,
|
||||
priority: props.priority,
|
||||
href: props.href,
|
||||
updated: props.updated,
|
||||
staleSec: props.staleSec,
|
||||
},
|
||||
});
|
||||
}
|
||||
return { type: 'FeatureCollection', features };
|
||||
}
|
||||
|
||||
function resolveGeoJsonUrl() {
|
||||
const fromEnv = typeof import.meta !== 'undefined' && import.meta.env
|
||||
? import.meta.env.VITE_DATAVERSE_FS_GEOJSON_URL
|
||||
: undefined;
|
||||
if (fromEnv && String(fromEnv).trim()) return String(fromEnv).trim();
|
||||
return '/samples/dataverse-fs-ww-demo.geojson';
|
||||
}
|
||||
|
||||
const dataverseFsLayer = createLocalGeoJsonLayer({
|
||||
id: DATAVERSE_FS_OVERLAY_SOURCE_ID,
|
||||
url: resolveGeoJsonUrl(),
|
||||
name: 'Dynamics Field Service',
|
||||
color: '#742774',
|
||||
icon: 'DV',
|
||||
source: 'Dataverse FS',
|
||||
labels: true,
|
||||
labelMax: 800,
|
||||
labelGridPx: 140,
|
||||
});
|
||||
|
||||
export function getDataverseFsLayer() {
|
||||
return dataverseFsLayer;
|
||||
}
|
||||
|
||||
export async function setDataverseFsEnabled(viewer, enabled) {
|
||||
if (!viewer) return;
|
||||
if (enabled) await dataverseFsLayer.enable(viewer);
|
||||
else await dataverseFsLayer.disable(viewer);
|
||||
}
|
||||
|
||||
export async function refreshDataverseFs(viewer) {
|
||||
if (!viewer) return;
|
||||
await dataverseFsLayer.disable(viewer);
|
||||
await dataverseFsLayer.enable(viewer);
|
||||
}
|
||||
|
||||
export default dataverseFsLayer;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeDataverseFsCollection } from './dataverseFs.js';
|
||||
|
||||
test('normalize drops null coords and empty input', () => {
|
||||
assert.deepEqual(normalizeDataverseFsCollection(null).features, []);
|
||||
assert.deepEqual(normalizeDataverseFsCollection({ type: 'FeatureCollection', features: [] }).features, []);
|
||||
const bad = {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{ type: 'Feature', geometry: { type: 'Point', coordinates: [null, 1] }, properties: { kind: 'site', id: '1', name: 'x' } },
|
||||
{ type: 'Feature', geometry: { type: 'Point', coordinates: [-85.67, 42.96] }, properties: { kind: 'site', id: '2', name: 'ok' } },
|
||||
],
|
||||
};
|
||||
const out = normalizeDataverseFsCollection(bad);
|
||||
assert.equal(out.features.length, 1);
|
||||
assert.equal(out.features[0].properties.id, '2');
|
||||
assert.equal(out.features[0].properties.name, 'ok');
|
||||
assert.equal(out.features[0].properties.kind, 'site');
|
||||
});
|
||||
|
||||
test('normalize skips unknown kinds', () => {
|
||||
const fc = {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{ type: 'Feature', geometry: { type: 'Point', coordinates: [1, 2] }, properties: { kind: 'spaceship', id: '9', name: 'nope' } },
|
||||
{ type: 'Feature', geometry: { type: 'Point', coordinates: [3, 4] }, properties: { kind: 'workorder', id: '8', name: 'WO' } },
|
||||
],
|
||||
};
|
||||
const out = normalizeDataverseFsCollection(fc);
|
||||
assert.equal(out.features.length, 1);
|
||||
assert.equal(out.features[0].properties.kind, 'workorder');
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { createLocalGeoJsonLayer } from './localGeojson.js';
|
||||
import dataverseFsLayer from './dataverseFs.js';
|
||||
import { createFirmsHeatmapLayer } from './firmsHeatmap.js';
|
||||
import submarineCablesLayer from './telegeographySubmarineCables.js';
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ const fires = createFirmsHeatmapLayer({
|
|||
});
|
||||
|
||||
export default [
|
||||
dataverseFsLayer,
|
||||
datacenters,
|
||||
dams,
|
||||
submarineCablesLayer,
|
||||
|
|
|
|||
Loading…
Reference in New Issue