1(function initMissionAtomicTimeline(global, factory) { 2 const api = factory(global); 3 if (typeof module !== "undefined" && module.exports) module.exports = api; 4 if (global) global.DSCMissionAtomicTimeline = api; 5})(typeof window !== "undefined" ? window : globalThis, function buildMissionAtomicTimelineCore(global) { 6 "use strict"; 7 8 const MAX_DURATION_MS = 168 * 60 * 60 * 1000; 9 const MAX_WEATHER_SLOTS = 169; 10 const MAX_ATOMICS = 512; 11 const LIGHT_STATES = new Set(["DAYLIGHT", "NIGHT", "UNKNOWN"]); 12 13 class MissionAtomicTimelineInputError extends TypeError { 14 constructor(code, path, message = code) { 15 super(message); 16 this.name = "MissionAtomicTimelineInputError"; 17 this.code = code; 18 this.path = path; 19 } 20 } 21 22 class MissionAtomicTimelineLimitError extends RangeError { 23 constructor(code, path, message = code) { 24 super(message); 25 this.name = "MissionAtomicTimelineLimitError"; 26 this.code = code; 27 this.path = path; 28 } 29 } 30 31 function fail(code, path, message) { throw new MissionAtomicTimelineInputError(code, path, message); } 32 function limit(code, path) { throw new MissionAtomicTimelineLimitError(code, path); } 33 function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } 34 function exactKeys(value, keys, path) { 35 if (!isObject(value)) fail("MALFORMED_INPUT", path); 36 const actual = Object.keys(value).sort(); 37 const expected = [...keys].sort(); 38 if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) fail("MALFORMED_INPUT", path); 39 } 40 function assertPlain(value, path = "input", seen = new Set()) { 41 if (value === null || ["string", "boolean"].includes(typeof value)) return; 42 if (typeof value === "number") { if (!Number.isFinite(value)) fail("MALFORMED_INPUT", path); return; } 43 if (typeof value !== "object" || seen.has(value)) fail("MALFORMED_INPUT", path); 44 const prototype = Object.getPrototypeOf(value); 45 if (prototype !== Object.prototype && prototype !== Array.prototype && prototype !== null) fail("MALFORMED_INPUT", path); 46 seen.add(value); 47 if (Array.isArray(value)) value.forEach((item, index) => assertPlain(item, `${path}[${index}]`, seen)); 48 else Object.entries(value).forEach(([key, item]) => assertPlain(item, `${path}.${key}`, seen)); 49 seen.delete(value); 50 } 51 function iso(value, path) { 52 if (typeof value !== "string") fail("MALFORMED_INPUT", path); 53 const ms = Date.parse(value); 54 if (!Number.isFinite(ms) || new Date(ms).toISOString() !== value) fail("MALFORMED_INPUT", path); 55 return { value, ms }; 56 } 57 function validateInterval(value, path) { 58 exactKeys(value, ["fromUtc", "toUtc"], path); 59 const from = iso(value.fromUtc, `${path}.fromUtc`); 60 const to = iso(value.toUtc, `${path}.toUtc`); 61 if (to.ms <= from.ms || to.ms - from.ms > MAX_DURATION_MS) fail("MALFORMED_INPUT", path); 62 return { from, to }; 63 } 64 function validateSegments(values, interval, path, withStatus = false) { 65 if (!Array.isArray(values) || !values.length) fail("MALFORMED_INPUT", path); 66 const segments = values.map((value, index) => { 67 exactKeys(value, withStatus ? ["fromUtc", "toUtc", "status"] : ["fromUtc", "toUtc"], `${path}[${index}]`); 68 const from = iso(value.fromUtc, `${path}[${index}].fromUtc`); 69 const to = iso(value.toUtc, `${path}[${index}].toUtc`); 70 if (to.ms <= from.ms || from.ms < interval.from.ms || to.ms > interval.to.ms) fail("MALFORMED_INPUT", path); 71 if (withStatus && !LIGHT_STATES.has(value.status)) fail("MALFORMED_INPUT", `${path}[${index}].status`); 72 return { from, to, status: withStatus ? value.status : undefined }; 73 }); 74 if (segments[0].from.ms !== interval.from.ms || segments[segments.length - 1].to.ms !== interval.to.ms) fail("MALFORMED_INPUT", path); 75 for (let index = 1; index < segments.length; index += 1) { 76 if (segments[index - 1].to.ms !== segments[index].from.ms) fail("MALFORMED_INPUT", path); 77 } 78 return segments; 79 } 80 81 function buildMissionAtomicTimeline(input) { 82 assertPlain(input); 83 exactKeys(input, ["schemaVersion", "interval", "legalSegments", "weatherSlots", "lightSegments"], "input"); 84 if (input.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.schemaVersion"); 85 const interval = validateInterval(input.interval, "input.interval"); 86 const legal = validateSegments(input.legalSegments, interval, "input.legalSegments"); 87 if (!Array.isArray(input.weatherSlots) || input.weatherSlots.length > MAX_WEATHER_SLOTS) { 88 if (Array.isArray(input.weatherSlots) && input.weatherSlots.length > MAX_WEATHER_SLOTS) limit("WEATHER_SLOT_LIMIT_EXCEEDED", "input.weatherSlots"); 89 fail("MALFORMED_INPUT", "input.weatherSlots"); 90 } 91 const weather = validateSegments(input.weatherSlots, interval, "input.weatherSlots"); 92 const light = validateSegments(input.lightSegments, interval, "input.lightSegments", true); 93 const boundaries = new Set([interval.from.ms, interval.to.ms]); 94 for (const segments of [legal, weather, light]) segments.forEac
94h(segment => { boundaries.add(segment.from.ms); boundaries.add(segment.to.ms); }); 95 const points = [...boundaries].sort((a, b) => a - b); 96 if (points.length - 1 > MAX_ATOMICS) limit("ATOMIC_INTERVAL_LIMIT_EXCEEDED", "output.atomicIntervals"); 97 const atomicIntervals = []; 98 let legalIndex = 0; let weatherIndex = 0; let lightIndex = 0; 99 for (let index = 0; index < points.length - 1; index += 1) { 100 const fromMs = points[index]; const toMs = points[index + 1]; 101 while (legalIndex < legal.length - 1 && fromMs >= legal[legalIndex].to.ms) legalIndex += 1; 102 while (weatherIndex < weather.length - 1 && fromMs >= weather[weatherIndex].to.ms) weatherIndex += 1; 103 while (lightIndex < light.length - 1 && fromMs >= light[lightIndex].to.ms) lightIndex += 1; 104 const legalSegment = legal[legalIndex]; const weatherSlot = weather[weatherIndex]; const lightSegment = light[lightIndex]; 105 if (!legalSegment || fromMs < legalSegment.from.ms || toMs > legalSegment.to.ms || 106 !weatherSlot || fromMs < weatherSlot.from.ms || toMs > weatherSlot.to.ms || 107 !lightSegment || fromMs < lightSegment.from.ms || toMs > lightSegment.to.ms) fail("MALFORMED_INPUT", "input.timeline"); 108 atomicIntervals.push({ 109 index: atomicIntervals.length, 110 fromUtc: new Date(fromMs).toISOString(), 111 toUtc: new Date(toMs).toISOString(), 112 durationMinutes: (toMs - fromMs) / 60000, 113 legalSegmentIndex: legalIndex, 114 weatherSlotIndex: weatherIndex, 115 lightSegmentIndex: lightIndex 116 }); 117 } 118 return { 119 schemaVersion: 1, 120 interval: { fromUtc: interval.from.value, toUtc: interval.to.value }, 121 atomicIntervals 122 }; 123 } 124 125 const romeFormatter = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }); 126 function parts(ms) { 127 return Object.fromEntries(romeFormatter.formatToParts(new Date(ms)).filter(item => item.type !== "literal").map(item => [item.type, Number(item.value)])); 128 } 129 function zonedMs(year, month, day, hour = 0) { 130 let guess = Date.UTC(year, month - 1, day, hour, 0, 0, 0); 131 for (let index = 0; index < 3; index += 1) { 132 const actual = parts(guess); 133 const delta = Date.UTC(year, month - 1, day, hour) - Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second); 134 guess += delta; 135 } 136 return guess; 137 } 138 function addUtcDate(date, days) { 139 const next = new Date(Date.UTC(date.year, date.month - 1, date.day + days)); 140 return { year: next.getUTCFullYear(), month: next.getUTCMonth() + 1, day: next.getUTCDate() }; 141 } 142 function geometryContract() { 143 const value = global?.DSCMissionGeometryContract; 144 if (!value?.representativePoint) fail("UNSUPPORTED_CONTRACT", "input.operationalGeometry", "REPRESENTATIVE_POINT_UNAVAILABLE"); 145 return value; 146 } 147 148 function buildMissionLightSegments(input) { 149 assertPlain(input); 150 exactKeys(input, ["schemaVersion", "interval", "operationalGeometry"], "input"); 151 if (input.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.schemaVersion"); 152 const interval = validateInterval(input.interval, "input.interval"); 153 let representative; 154 try { 155 representative = geometryContract().representativePoint(input.operationalGeometry, feature => global?.turf?.pointOnFeature(feature)); 156 } catch (error) { 157 if (error instanceof MissionAtomicTimelineInputError) throw error; 158 fail("UNSUPPORTED_CONTRACT", "input.operationalGeometry", "REPRESENTATIVE_POINT_UNAVAILABLE"); 159 } 160 const coordinates = representative?.point?.coordinates; 161 if (!Array.isArray(coordinates) || !Number.isFinite(coordinates[0]) || !Number.isFinite(coordinates[1])) fail("MALFORMED_INPUT", "input.operationalGeometry"); 162 const first = parts(interval.from.ms); const last = parts(interval.to.ms - 1); 163 let date = { year: first.year, month: first.month, day: first.day }; 164 const endKey = `${last.year}-${last.month}-${last.day}
164`; 165 const lightSegments = []; 166 for (let guard = 0; guard < 10; guard += 1) { 167 const startMs = zonedMs(date.year, date.month, date.day, 0); 168 const next = addUtcDate(date, 1); const endMs = zonedMs(next.year, next.month, next.day, 0); 169 const anchor = new Date(zonedMs(date.year, date.month, date.day, 12)); 170 let sunriseMs = null; let sunsetMs = null; 171 try { 172 const times = global?.SunCalc?.getTimes?.(anchor, coordinates[1], coordinates[0]); 173 const rise = times?.sunrise?.getTime?.(); const set = times?.sunset?.getTime?.(); 174 if (Number.isFinite(rise) && Number.isFinite(set) && rise < set) { sunriseMs = rise; sunsetMs = set; } 175 } catch { /* UNKNOWN for this local day */ } 176 const clippedFrom = Math.max(startMs, interval.from.ms); const clippedTo = Math.min(endMs, interval.to.ms); 177 if (clippedTo > clippedFrom) { 178 const points = [clippedFrom, clippedTo]; 179 if (sunriseMs !== null && sunriseMs > clippedFrom && sunriseMs < clippedTo) points.push(sunriseMs); 180 if (sunsetMs !== null && sunsetMs > clippedFrom && sunsetMs < clippedTo) points.push(sunsetMs); 181 points.sort((a, b) => a - b); 182 for (let index = 0; index < points.length - 1; index += 1) { 183 const fromMs = points[index]; const toMs = points[index + 1]; 184 const status = sunriseMs === null || sunsetMs === null ? "UNKNOWN" : fromMs >= sunriseMs && fromMs < sunsetMs ? "DAYLIGHT" : "NIGHT"; 185 lightSegments.push({ fromUtc: new Date(fromMs).toISOString(), toUtc: new Date(toMs).toISOString(), status }); 186 } 187 } 188 if (`${date.year}-${date.month}-${date.day}` === endKey) break; 189 date = next; 190 } 191 if (!lightSegments.length || lightSegments[0].fromUtc !== interval.from.value || lightSegments[lightSegments.length - 1].toUtc !== interval.to.value) fail("MALFORMED_INPUT", "input.lightSegments"); 192 return lightSegments; 193 } 194 195 return Object.freeze({ 196 buildMissionAtomicTimeline, 197 buildMissionLightSegments, 198 MissionAtomicTimelineInputError, 199 MissionAtomicTimelineLimitError 200 }); 201});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.