'use strict'; (function exposeDeviceTree(globalScope) { const SPECIAL = Object.freeze({ ORPHAN_SITE: '__orphaned__', UNGROUPED: '__ungrouped__', UNKNOWN_GROUP_PREFIX: '__unknown_group__:', UNKNOWN_SITE_PREFIX: '__unknown_site__:', }); function text(value) { return value === null || value === undefined ? '' : String(value).trim(); } function normalized(value) { return text(value).normalize('NFKD').toLocaleLowerCase(); } function compareNodes(left, right) { const nameOrder = left.normalizedName === right.normalizedName ? 0 : (left.normalizedName < right.normalizedName ? -1 : 1); const idOrder = left.canonicalId === right.canonicalId ? 0 : (left.canonicalId < right.canonicalId ? -1 : 1); return nameOrder || idOrder || left.ordinal - right.ordinal; } function createIdentity(kind, item, ordinal, occurrences, diagnostics) { const rawId = text(item && item.id); const canonicalId = rawId || `__invalid_${kind}_${ordinal}`; const count = (occurrences.get(canonicalId) || 0) + 1; occurrences.set(canonicalId, count); if (!rawId) diagnostics.push({ code: 'malformed-id', kind, ordinal, id: rawId }); if (count > 1) diagnostics.push({ code: 'duplicate-id', kind, ordinal, id: canonicalId }); return { rawId, canonicalId, uniqueId: count === 1 ? canonicalId : `${canonicalId}~${count}`, }; } function makeSite(id, name, ordinal, extra) { return Object.assign({ kind: 'site', id, canonicalId: id, name, normalizedName: normalized(name), ordinal, key: `site:${id}`, pendingDeletion: false, groups: [], synthetic: false, }, extra); } function makeGroup(site, id, name, ordinal, extra) { return Object.assign({ kind: 'group', id, canonicalId: id, name, normalizedName: normalized(name), ordinal, key: `group:${site.id}:${id}`, parentKey: site.key, site, cameras: [], pendingDeletion: false, synthetic: false, }, extra); } function buildDeviceTree(payload = {}) { const diagnostics = []; const sourceSites = Array.isArray(payload.sites) ? payload.sites : []; const sourceGroups = Array.isArray(payload.groups) ? payload.groups : []; const sourceDevices = Array.isArray(payload.devices) ? payload.devices : []; const rowByKey = new Map(); const cameraByKey = new Map(); const sites = []; const siteByRawId = new Map(); const groupByRawId = new Map(); const groupOccurrences = new Map(); const siteOccurrences = new Map(); const cameraOccurrences = new Map(); let syntheticOrdinal = sourceSites.length + sourceGroups.length + sourceDevices.length; sourceSites.forEach((item, ordinal) => { const safe = item && typeof item === 'object' ? item : {}; const identity = createIdentity('site', safe, ordinal, siteOccurrences, diagnostics); const site = makeSite(identity.uniqueId, text(safe.name) || identity.canonicalId, ordinal, { canonicalId: identity.canonicalId, pendingDeletion: safe.pendingDeletion === true || Boolean(text(safe.pendingDeletionStart)), source: safe, }); sites.push(site); rowByKey.set(site.key, site); if (identity.rawId && !siteByRawId.has(identity.rawId)) siteByRawId.set(identity.rawId, site); }); function ensureUnknownSite(rawSiteId) { const labelId = text(rawSiteId); const id = labelId ? `${SPECIAL.UNKNOWN_SITE_PREFIX}${labelId}` : SPECIAL.ORPHAN_SITE; let site = rowByKey.get(`site:${id}`); if (!site) { const name = labelId ? `Unknown site: ${labelId}` : 'Orphaned cameras'; site = makeSite(id, name, syntheticOrdinal++, { synthetic: true, hierarchyStatus: labelId ? 'unknown-site' : 'orphan' }); sites.push(site); rowByKey.set(site.key, site); } return site; } sourceGroups.forEach((item, ordinal) => { const safe = item && typeof item === 'object' ? item : {}; const identity = createIdentity('group', safe, ordinal, groupOccurrences, diagnostics); const parentId = text(safe.parentId); const site = siteByRawId.get(parentId) || ensureUnknownSite(parentId); if (!siteByRawId.has(parentId)) diagnostics.push({ code: parentId ? 'unknown-parent-site' : 'malformed-parent-site', key: `group:${site.id}:${identity.uniqueId}`, id: parentId }); const group = makeGroup(site, identity.uniqueId, text(safe.name) || identity.canonicalId, ordinal, { canonicalId: identity.canonicalId, rawParentId: parentId, pendingDeletion: safe.pendingDeletion === true || Boolean(text(safe.pendingDeletionStart)), source: safe, }); site.groups.push(group); rowByKey.set(group.key, group); if (identity.rawId && !groupByRawId.has(identity.rawId)) groupByRawId.set(identity.rawId, group); }); function ensureGroup(site, id, name, status, template) { const key = `group:${site.id}:${id}`; let group = rowByKey.get(key); if (!group) { group = makeGroup(site, id, name, syntheticOrdinal++, { synthetic: true, hierarchyStatus: status, canonicalId: template ? template.canonicalId : id, pendingDeletion: template ? template.pendingDeletion : false, source: template ? template.source : undefined, }); site.groups.push(group); rowByKey.set(group.key, group); } return group; } sourceDevices.forEach((item, ordinal) => { const safe = item && typeof item === 'object' ? item : {}; const identity = createIdentity('camera', safe, ordinal, cameraOccurrences, diagnostics); const directSiteId = text(safe.siteId); const requestedGroupId = text(safe.deviceGroupId); const knownGroup = requestedGroupId ? groupByRawId.get(requestedGroupId) : undefined; let site; let hierarchyStatus = text(safe.hierarchyStatus) || 'assigned'; if (directSiteId) { site = siteByRawId.get(directSiteId) || ensureUnknownSite(directSiteId); if (!siteByRawId.has(directSiteId)) hierarchyStatus = 'unknown-site'; } else if (knownGroup) { site = knownGroup.site; hierarchyStatus = 'inferred-site'; } else { site = ensureUnknownSite(''); hierarchyStatus = requestedGroupId ? 'unknown-group' : 'orphan'; } let group; if (!requestedGroupId) { group = ensureGroup(site, SPECIAL.UNGROUPED, 'Ungrouped', 'ungrouped'); } else if (!knownGroup) { group = ensureGroup(site, `${SPECIAL.UNKNOWN_GROUP_PREFIX}${requestedGroupId}`, `Unknown group: ${requestedGroupId}`, 'unknown-group'); if (hierarchyStatus !== 'unknown-site') hierarchyStatus = 'unknown-group'; diagnostics.push({ code: 'unknown-group', key: `camera:${identity.uniqueId}`, id: requestedGroupId }); } else if (knownGroup.site !== site) { group = ensureGroup(site, knownGroup.id, knownGroup.name, 'conflict', knownGroup); hierarchyStatus = 'conflict'; diagnostics.push({ code: 'site-group-conflict', key: `camera:${identity.uniqueId}`, siteId: directSiteId, groupId: requestedGroupId, groupSiteId: knownGroup.site.canonicalId }); } else { group = knownGroup; } const name = text(safe.name) || identity.canonicalId; const camera = { kind: 'camera', id: identity.uniqueId, canonicalId: identity.canonicalId, key: `camera:${identity.uniqueId}`, name, normalizedName: normalized(name), ordinal, parentKey: group.key, site, group, hierarchyStatus, source: safe, }; camera.searchCorpus = normalized([ name, identity.canonicalId, safe.model, safe.type, safe.address, site.name, group.name, ].map(text).join(' ')); group.cameras.push(camera); cameraByKey.set(camera.key, camera); rowByKey.set(camera.key, camera); }); sites.forEach((site) => { site.groups.sort(compareNodes); site.groups.forEach((group) => group.cameras.sort(compareNodes)); }); sites.sort(compareNodes); return Object.freeze({ sites, rowByKey, cameraByKey, diagnostics, cameraCount: cameraByKey.size }); } function rowDescriptor(node, level, expanded, selectedKey, position, size) { const aria = { level, expanded: node.kind === 'camera' ? undefined : expanded, selected: node.key === selectedKey, posinset: position, setsize: size, }; return { key: node.key, parentKey: node.parentKey || null, kind: node.kind, id: node.id, name: node.name, pendingDeletion: node.pendingDeletion === true, hierarchyStatus: node.hierarchyStatus, aria, ariaLevel: aria.level, ariaExpanded: aria.expanded, ariaSelected: aria.selected, ariaPosInSet: aria.posinset, ariaSetSize: aria.setsize, }; } function flattenVisibleRows(model, options = {}) { const expandedKeys = options.expandedKeys instanceof Set ? options.expandedKeys : new Set(); const selectedKey = typeof options.selectedKey === 'string' ? options.selectedKey : null; const matches = options.matchCameraKeys instanceof Set ? options.matchCameraKeys : null; const rows = []; const includedGroups = (site) => site.groups.filter((group) => !matches || group.cameras.some((camera) => matches.has(camera.key))); const includedSites = model.sites.filter((site) => !matches || includedGroups(site).length > 0); includedSites.forEach((site, siteIndex) => { const siteExpanded = expandedKeys.has(site.key); rows.push(rowDescriptor(site, 1, siteExpanded, selectedKey, siteIndex + 1, includedSites.length)); if (!siteExpanded) return; const groups = includedGroups(site); groups.forEach((group, groupIndex) => { const groupExpanded = expandedKeys.has(group.key); rows.push(rowDescriptor(group, 2, groupExpanded, selectedKey, groupIndex + 1, groups.length)); if (!groupExpanded) return; const cameras = matches ? group.cameras.filter((camera) => matches.has(camera.key)) : group.cameras; cameras.forEach((camera, cameraIndex) => { rows.push(rowDescriptor(camera, 3, undefined, selectedKey, cameraIndex + 1, cameras.length)); }); }); }); Object.defineProperty(rows, 'hiddenSelected', { value: Boolean(selectedKey && model.rowByKey.has(selectedKey) && !rows.some((row) => row.key === selectedKey)), enumerable: true, }); return rows; } function calculateVirtualWindow(rows, options = {}) { const rowHeight = Number.isFinite(options.rowHeight) && options.rowHeight > 0 ? options.rowHeight : 28; const viewportHeight = Number.isFinite(options.viewportHeight) && options.viewportHeight >= 0 ? options.viewportHeight : 0; const scrollTop = Math.max(0, Number.isFinite(options.scrollTop) ? options.scrollTop : 0); const overscan = Math.max(0, Number.isInteger(options.overscan) ? options.overscan : 16); const visibleStart = Math.floor(scrollTop / rowHeight); const visibleEnd = Math.ceil((scrollTop + viewportHeight) / rowHeight); const startIndex = Math.max(0, Math.min(rows.length, visibleStart - overscan)); const endIndex = Math.max(startIndex, Math.min(rows.length, visibleEnd + overscan)); return { rows: rows.slice(startIndex, endIndex), startIndex, endIndex, offsetTop: startIndex * rowHeight, totalHeight: rows.length * rowHeight, }; } function createSearchRunner(options = {}) { const scheduler = typeof options.scheduler === 'function' ? options.scheduler : (work) => setTimeout(work, 0); const chunkSize = Number.isInteger(options.chunkSize) && options.chunkSize > 0 ? options.chunkSize : 250; let generation = 0; function search(model, query, searchOptions = {}) { const myGeneration = ++generation; const terms = normalized(query).split(/\s+/).filter(Boolean); const cameras = [...model.cameraByKey.values()]; const cameraKeys = new Set(); const expandedKeys = new Set(searchOptions.expandedKeys instanceof Set ? searchOptions.expandedKeys : []); let index = 0; return new Promise((resolve) => { function finish(cancelled) { resolve(Object.freeze({ generation: myGeneration, cancelled, count: cameraKeys.size, cameraKeys, expandedKeys })); } function work() { if (myGeneration !== generation) return finish(true); const end = Math.min(cameras.length, index + chunkSize); for (; index < end; index += 1) { const camera = cameras[index]; if (terms.every((term) => camera.searchCorpus.includes(term))) { cameraKeys.add(camera.key); expandedKeys.add(camera.site.key); expandedKeys.add(camera.group.key); } } if (index < cameras.length) scheduler(work); else finish(false); } scheduler(work); }); } return Object.freeze({ search, cancel() { generation += 1; }, get generation() { return generation; } }); } const api = Object.freeze({ SPECIAL, buildDeviceTree, buildDeviceTreeModel: buildDeviceTree, flattenVisibleRows, flattenRows: flattenVisibleRows, createSearchRunner, calculateVirtualWindow, }); if (typeof module !== 'undefined' && module.exports) module.exports = api; if (globalScope) globalScope.AptDeviceTree = api; }(typeof window !== 'undefined' ? window : undefined));