From e9052143d7fca389adc5a7fc3cef5517754e2b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EC=84=B1=EC=9D=BC?= Date: Mon, 29 Jun 2026 14:56:27 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8A=A4=ED=85=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EB=B0=94=20=EC=B8=A1=EC=A0=90=20=EC=A0=95=ED=99=95=EB=8F=84=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20+=20=EC=98=81=EC=83=81=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=20UI=20=ED=86=B5=ED=95=A9=20+=20=EC=A2=85?= =?UTF-8?q?=ED=95=A9=20=EA=B8=B0=EC=88=A0=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스테이션바/측점(체이니지) - 역 재진입 마커 누락 수정: 측점기준 탐지에 좌표근접(pxPassesTo) 합집합 보강(중복 24px 제거) - kmExists 판정: 좌표 반경만으로 잡힌 통과는 측점값 라벨/검색에서 제외(마커 동그라미는 유지) - 측점값 라벨 겹침 숨김(좌측 우선), 동일항목 브래킷 클러스터 - 측점 검색 순환(통과방향 다음→끝에서 처음), 마커 기준 후보 산출, 입력값 유지/blur 삭제 - 연속 체이니지 수직투영 유틸 분리(client/src/utils/chainage.ts), Minimap 컴포넌트 추가 영상 컨트롤 UI - 선형/드론궤적/좌측패널 토글을 재생바로 통합(settingsStore 공유·persist) - 드론 GPS·고도 HUD 상시 표시, 경로표고·투명도 항상 노출, 겹침제외 버튼 숨김 - 재생바 요소 재배치(좌측패널→선형→드론궤적→드론위치정보→시설등급), 활성색 amber 통일 문서/도구 - 종합 기술명세서(인계+특허 기초) md/html/pdf 3종, UI개선 보고서 3종 - md→html→pdf 변환 스크립트(scripts/md2docs.sh) + 스타일(docs/assets/report.css) - 작업 히스토리 다수 추가(docs/history/) Co-Authored-By: Claude Opus 4.8 (1M context) --- client/src/components/overlay/Minimap.tsx | 69 ++ client/src/components/overlay/RoutePanel.tsx | 25 +- .../src/components/overlay/StationOverlay.tsx | 134 +-- client/src/components/player/VideoPlayer.tsx | 229 +++-- client/src/stationbar/StationBar.module.scss | 6 +- client/src/stationbar/StationBar.tsx | 384 +++++--- .../PlaybackControls/PlaybackControls.tsx | 10 +- .../components/Timeline/Timeline.tsx | 164 +++- client/src/store/settingsStore.ts | 20 + client/src/utils/chainage.ts | 64 ++ docs/assets/report.css | 77 ++ .../2026-06-26_0913_동일항목-묶음-브래킷.md | 36 + .../2026-06-26_1411_스테이션바-거리축-전환.md | 35 + .../2026-06-26_1520_스테이션바-측점축-전환.md | 43 + ...2026-06-29_1029_역사-통과-좌표근접-보강.md | 40 + .../2026-06-29_1057_측점값-라벨-겹침-숨김.md | 23 + ...26-06-29_1106_선형-드론궤적-바버튼-이동.md | 26 + ...1_경로표고-투명도-항상표시+겹침제외숨김.md | 22 + ...9_1115_좌측패널-바이동+측점진단버튼삭제.md | 28 + ...29_1119_재생바-드론정보-시설등급-재배치.md | 19 + .../2026-06-29_1123_활성색-amber-통일.md | 25 + ...2026-06-29_1146_보고서-md-html-pdf-생성.md | 27 + ...6-06-29_1212_측점검색-순환점프+입력유지.md | 27 + .../2026-06-29_1219_측점검색-마커기준-전환.md | 27 + ...026-06-29_1228_좌표반경-마커-측점값숨김.md | 30 + ...26-06-29_1338_55000-외부접속-포트포워딩.md | 28 + ..._드론자세평활-오버레이렌더링-기술문서화.md | 302 +++++++ .../2026-06-29_1447_종합기술명세서-작성.md | 32 + docs/기술명세_GhiVideo_종합기술문서.html | 834 ++++++++++++++++++ docs/기술명세_GhiVideo_종합기술문서.md | 541 ++++++++++++ docs/기술명세_GhiVideo_종합기술문서.pdf | Bin 0 -> 235691 bytes docs/보고서_2026-06-29_스테이션바_UI개선.html | 158 ++++ docs/보고서_2026-06-29_스테이션바_UI개선.md | 71 ++ docs/보고서_2026-06-29_스테이션바_UI개선.pdf | Bin 0 -> 51416 bytes docs/특허_스테이션기반_주행영상_플레이어.html | 2 +- scripts/md2docs.sh | 35 + 36 files changed, 3308 insertions(+), 285 deletions(-) create mode 100644 client/src/components/overlay/Minimap.tsx create mode 100644 client/src/utils/chainage.ts create mode 100644 docs/assets/report.css create mode 100644 docs/history/2026-06-26_0913_동일항목-묶음-브래킷.md create mode 100644 docs/history/2026-06-26_1411_스테이션바-거리축-전환.md create mode 100644 docs/history/2026-06-26_1520_스테이션바-측점축-전환.md create mode 100644 docs/history/2026-06-29_1029_역사-통과-좌표근접-보강.md create mode 100644 docs/history/2026-06-29_1057_측점값-라벨-겹침-숨김.md create mode 100644 docs/history/2026-06-29_1106_선형-드론궤적-바버튼-이동.md create mode 100644 docs/history/2026-06-29_1111_경로표고-투명도-항상표시+겹침제외숨김.md create mode 100644 docs/history/2026-06-29_1115_좌측패널-바이동+측점진단버튼삭제.md create mode 100644 docs/history/2026-06-29_1119_재생바-드론정보-시설등급-재배치.md create mode 100644 docs/history/2026-06-29_1123_활성색-amber-통일.md create mode 100644 docs/history/2026-06-29_1146_보고서-md-html-pdf-생성.md create mode 100644 docs/history/2026-06-29_1212_측점검색-순환점프+입력유지.md create mode 100644 docs/history/2026-06-29_1219_측점검색-마커기준-전환.md create mode 100644 docs/history/2026-06-29_1228_좌표반경-마커-측점값숨김.md create mode 100644 docs/history/2026-06-29_1338_55000-외부접속-포트포워딩.md create mode 100644 docs/history/2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md create mode 100644 docs/history/2026-06-29_1447_종합기술명세서-작성.md create mode 100644 docs/기술명세_GhiVideo_종합기술문서.html create mode 100644 docs/기술명세_GhiVideo_종합기술문서.md create mode 100644 docs/기술명세_GhiVideo_종합기술문서.pdf create mode 100644 docs/보고서_2026-06-29_스테이션바_UI개선.html create mode 100644 docs/보고서_2026-06-29_스테이션바_UI개선.md create mode 100644 docs/보고서_2026-06-29_스테이션바_UI개선.pdf create mode 100755 scripts/md2docs.sh diff --git a/client/src/components/overlay/Minimap.tsx b/client/src/components/overlay/Minimap.tsx new file mode 100644 index 0000000..76e7102 --- /dev/null +++ b/client/src/components/overlay/Minimap.tsx @@ -0,0 +1,69 @@ +import { forwardRef } from 'react'; + +/** + * 나침반 (heading-up, 아날로그 눈금형) — 눈금 링 + N + 빨간 바늘이 드론 방위(--rot = -yaw)로 회전. + * - 회전 카드(눈금/N/빨간 바늘/빨간 눈금): 부모 RAF 가 `--rot`(누적 언랩) 갱신 → 부드럽게 회전. + * - 상단 흰 삼각형 인덱스 + 외곽 링은 고정(위 = 드론 진행방향 = 헤딩). + * ref 는 root div — `style.setProperty('--rot', `${-yaw}deg`)`. + */ +const D = 150; // 나침반 한 변(px) + +// 눈금: 6° 간격(60개). 0/90/180/270=장, 30°배수=중, 그 외=단. +const TICKS = Array.from({ length: 60 }, (_, i) => { + const deg = i * 6; + const long = deg % 90 === 0; + const mid = deg % 30 === 0; + const a = (deg * Math.PI) / 180; + const r1 = long ? 37 : mid ? 40 : 42; // 안쪽 끝 + const sin = Math.sin(a), cos = Math.cos(a); + return { + deg, + x1: 50 + r1 * sin, y1: 50 - r1 * cos, + x2: 50 + 46 * sin, y2: 50 - 46 * cos, + w: long ? 1.7 : mid ? 1.2 : 0.8, + color: long ? '#fff' : 'rgba(255,255,255,0.6)', + }; +}); + +export const Minimap = forwardRef(function Minimap(_props, ref) { + return ( +
+ {/* 고정 — 배경 원 + 외곽 링(두껍게). 프레임만 고정, 그 외 전부 회전. */} + + + + + {/* 회전 카드 — 눈금 + N + 빨간 바늘(N) + 흰 삼각형(S) + 빨간 눈금. 전체가 함께 회전. */} + + {TICKS.map((t) => ( + + ))} + {/* 빨간 화살표 — N(정북). 끝(tip)이 테두리에 맞닿고, 기단은 중심 쪽. */} + + {/* N 문자 — 빨간 화살표 '안쪽'(중심 방향). */} + + N + + {/* 흰 삼각형 — S방향, 빨간 화살표와 '정반대'(아래로). 끝이 하단 테두리에 맞닿음. */} + + {/* 중심 점 */} + + +
+ ); +}); diff --git a/client/src/components/overlay/RoutePanel.tsx b/client/src/components/overlay/RoutePanel.tsx index 0afb8f4..74a8df6 100644 --- a/client/src/components/overlay/RoutePanel.tsx +++ b/client/src/components/overlay/RoutePanel.tsx @@ -66,6 +66,7 @@ export default function RoutePanel({ currentTime, visible, onSeek, topPx = 90, b const pois = useGeoStore(s => s.pois); const structures = useGeoStore(s => s.structures); const gradeFilter = useSettingsStore(s => s.gradeFilter); + const poiOverlapExclude = useSettingsStore(s => s.poiOverlapExclude); const droneFrames = useGeoStore(s => s.frames); const routeMeta = useGeoStore(s => s.routeMeta); const [currentKm, setCurrentKm] = useState(0); @@ -250,9 +251,21 @@ export default function RoutePanel({ currentTime, visible, onSeek, topPx = 90, b const kmToY = (km: number) => (1 - (km - minKm) / (maxKm - minKm)) * 100; // 교량/터널만 표시 + 시설종별 필터(설정 스토어, 재생바와 동일 규칙). + // \uACB9\uCE68\uC81C\uC678 ON \uC2DC \uAC19\uC740 \uC704\uCE58 \uD615\uC81C \uC911 \uC9C4\uD589\uBC29\uD5A5((\uD558)/(\uC0C1))\uC744 \uC6B0\uC120 \uB0A8\uAE30\uB3C4\uB85D dir-\uC6B0\uC120 \uC815\uB82C. + const routeDir = routeMeta?.routeInfo?.direction?.includes('\uD558') ? '\uD558' + : routeMeta?.routeInfo?.direction?.includes('\uC0C1') ? '\uC0C1' : null; + const dirTag = routeDir === '\uC0C1' ? '(\uC0C1)' : routeDir === '\uD558' ? '(\uD558)' : ''; + // \uACBD\uB85C\uC0C1 \uC2DC\uC124\uBB3C\uB9CC: \uBC29\uD5A5\uD45C\uAE30((\uC0C1\u2026/(\uD558\u2026)\uAC00 \uC788\uC73C\uBA74 \uC601\uC0C1 \uBC29\uD5A5 \uC77C\uCE58\uD558\uB294 \uAC83\uB9CC(\uBC18\uB300 \uBC29\uD5A5=\uB2E4\uB978 \uC120\uB85C \uC81C\uC678). + const oppCh = routeDir === '\uD558' ? '\uC0C1' : routeDir === '\uC0C1' ? '\uD558' : ''; + const isOppositeDir = (name: string): boolean => !!oppCh && new RegExp(`[(\uFF08]${oppCh}`).test(name); + const baseStruct = (t: string): string => t.replace(/\s*[(\uFF08].*$/, '').trim(); + // \uAC19\uC740 \uC88C\uD45C(\uAC19\uC740 base \uBCC0\uD615)\uB294 \uACB9\uCE68\uC81C\uC678 \uC635\uC158\uACFC \uBB34\uAD00\uD558\uAC8C \uC9C4\uD589\uBC29\uD5A5 1\uAC1C\uB9CC(dir-\uC6B0\uC120 \uC815\uB82C \uD6C4 base\uBCC4 \uCCAB\uC9F8). + const seenBase = new Set(); const filteredPois = structures - .filter(s => (s.type === 'bridge' || s.type === 'tunnel') && typeof s.lat === 'number' && typeof s.lon === 'number' && isGradeVisible(s.grade, gradeFilter)) - .map(s => ({ title: s.name, category: s.type === 'tunnel' ? '\uD130\uB110' : '\uAD50\uB7C9', lat: s.lat as number, lon: s.lon as number, z: 0, type: 'poi' as const })); + .filter(s => (s.type === 'bridge' || s.type === 'tunnel') && typeof s.lat === 'number' && typeof s.lon === 'number' && isGradeVisible(s.grade, gradeFilter) && !isOppositeDir(s.name)) + .map(s => ({ title: s.name, category: s.type === 'tunnel' ? '\uD130\uB110' : '\uAD50\uB7C9', lat: s.lat as number, lon: s.lon as number, z: 0, type: 'poi' as const })) + .sort((a, b) => (dirTag && a.title.includes(dirTag) ? 0 : 1) - (dirTag && b.title.includes(dirTag) ? 0 : 1)) + .filter(p => { const b = baseStruct(p.title); if (seenBase.has(b)) return false; seenBase.add(b); return true; }); return (
95) return null; - if (placed.some(py => Math.abs(py - y) < MIN_GAP)) return null; - placed.push(y); + // 겹침제외 ON 일 때만 겹친 것(같은 위치 형제 포함) 제외 → 방향 우선(위 dir-정렬)으로 1개만. + // OFF 면 모두 표시(겹쳐도). 영상 오버레이 토글과 동일 규칙. + if (poiOverlapExclude) { + if (placed.some(py => Math.abs(py - y) < MIN_GAP)) return null; + placed.push(y); + } return (
; @@ -107,10 +110,10 @@ const DISPLAY_DEFAULTS = { emaAlpha: 1.0, smoothMinAlpha: 0.12, // 정지/떨림 시 최소 추종(작을수록 강한 평활) smoothSpeedRef: 0.010, // 즉시추종 기준 속도(클수록 더 강하게 평활) - maxPoiRange: 300, + maxPoiRange: 1000, // false = 지면고도 기준(POI를 지면고도 z에 고정 → gap=드론고도−지면고도 자동변동, 드론 오르내려도 지물에 붙음). - // true = 드론−이격거리 고정(gap 상수). 드론 고도 변동 시 부정확 → 기본 off. - poiDroneHeight: false, + // true = 드론−이격거리 고정(gap 상수). 드론 고도 변동 시 부정확. + poiDroneHeight: true, droneHeightDrop: 24, }; @@ -229,7 +232,7 @@ function ParamRow({ label, value, min, max, step, unit, decimals = 1, onChange, // ── 메인 컴포넌트 ───────────────────────────────────────────────────────────── -export default function StationOverlay({ currentFrame, currentTime, fps, visible, videoReady = true, videoWidth = 0, videoHeight = 0, onTogglePlay, showPanel = true, topPx = 72, timeRef }: Props) { +export default function StationOverlay({ currentFrame, currentTime, fps, visible, videoReady = true, videoWidth = 0, videoHeight = 0, onTogglePlay, showPanel = true, topPx = 72, barHeight = 0, timeRef }: Props) { const canvasRef = useRef(null); const canvasSizeRef = useRef({ w: 0, h: 0 }); @@ -262,6 +265,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const renderCacheRef = useRef(null); // 나침반 전용(측점선 visible 무관 항상 갱신·표시) const compassRef = useRef<{ effectiveYaw: number; hFovRad: number } | null>(null); + // 나침반 미니맵(DOM) root — RAF 에서 --rot(=-yaw) 갱신. minimapRotRef=누적 회전(360° 언랩). + const minimapRef = useRef(null); + const minimapRotRef = useRef(0); // UI state const [params, setParams] = useState(DEFAULT_CAMERA_PARAMS); @@ -279,24 +285,28 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // POI 표시 범위(m) — 드론 위치와의 수평 직선거리(distH, 고도차 무시). 이 범위 안이면 표시, // 밖이면 제외. (이전: 진행방향 앞/옆 비등방 → 단일 반경으로 통합.) 패널에서 실시간 조절. const [maxPoiRange, setMaxPoiRange] = useState(DISPLAY_DEFAULTS.maxPoiRange); - const maxPoiRangeRef = useRef(300); + const maxPoiRangeRef = useRef(DISPLAY_DEFAULTS.maxPoiRange); // 비교 테스트: POI 높이를 선로표고(false) 대신 '드론 고도 − N미터'(true)로. N=droneHeightDrop. const [poiDroneHeight, setPoiDroneHeight] = useState(DISPLAY_DEFAULTS.poiDroneHeight); const [droneHeightDrop, setDroneHeightDrop] = useState(DISPLAY_DEFAULTS.droneHeightDrop); // 드론 경로 가시화 — 드론의 GPS 경로(lat/lon)를 지정 표고 z 에 투영해 선으로 표시. - const [showDronePath, setShowDronePath] = useState(DISPLAY_DEFAULTS.showDronePath); + // 선형/드론궤적 토글은 설정 스토어로 공유 → VideoPlayer 하단 바 버튼에서도 제어. + const showDronePath = useSettingsStore(s => s.showDronePath); + const setShowDronePath = useSettingsStore(s => s.setShowDronePath); const [dronePathZ, setDronePathZ] = useState(DISPLAY_DEFAULTS.dronePathZ); // 기본 경로 표고 const showDronePathRef = useRef(true); const dronePathZRef = useRef(78); useEffect(() => { showDronePathRef.current = showDronePath; }, [showDronePath]); useEffect(() => { dronePathZRef.current = dronePathZ; }, [dronePathZ]); - // 선형(중심선) 독립 토글 + 드론경로 투명도(0~1) - const [showCenterline, setShowCenterline] = useState(DISPLAY_DEFAULTS.showCenterline); + // 선형(중심선) 독립 토글 + 드론경로 투명도(0~1) — 토글은 설정 스토어 공유(바 버튼 제어). + const showCenterline = useSettingsStore(s => s.showCenterline); + const setShowCenterline = useSettingsStore(s => s.setShowCenterline); const showCenterlineRef = useRef(true); useEffect(() => { showCenterlineRef.current = showCenterline; }, [showCenterline]); const [dronePathAlpha, setDronePathAlpha] = useState(DISPLAY_DEFAULTS.dronePathAlpha); const dronePathAlphaRef = useRef(0.75); useEffect(() => { dronePathAlphaRef.current = dronePathAlpha; }, [dronePathAlpha]); + // 측점 진단 토글 버튼은 삭제됨. (HUD 표시는 VideoPlayer 에서 showStationDiag 구독) // POI 위치 편집(드래그) 모드 const [editMode, setEditMode] = useState(false); const editModeRef = useRef(false); @@ -442,6 +452,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const poiOverlapExclude = useSettingsStore(s => s.poiOverlapExclude); const overlapExcludeRef = useRef(true); useEffect(() => { overlapExcludeRef.current = poiOverlapExclude; }, [poiOverlapExclude]); + // 좌측 노선 패널(RoutePanel) 표시 토글은 하단 재생바(VideoPlayer)로 이동. const storeBaseName = useGeoStore(s => s.baseName); const storePoiOverrides = useGeoStore(s => s.poiOverrides); const setPoiOverride = useGeoStore(s => s.setPoiOverride); @@ -798,7 +809,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible } }; - requestIdleCallback(step, { timeout: 200 }); + // 첫 청크(현재 프레임 포함)는 동기 실행 → 토글/재계산이 '정지 중에도' 즉시 반영. + // (idle 대기 시 정지 상태에서 갱신이 지연/누락되던 문제 해결.) 나머지는 step 내부에서 idle 로 이어감. + step(); }, [nearestCL]); // 모든 데이터 로드 완료 시 사전 계산 시작 @@ -936,6 +949,27 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible : currentDroneFrameRef.current!); const lines = buildLines(dronePose); + // 나침반 미니맵(heading-up) 회전 — 영상과 즉시 동기. dronePose.yaw 는 ±smoothHalf(기본 60fr ≈2s) + // 평활이라 회전 시 지연 → 가벼운 평활(±3fr)의 yaw 를 따로 보간해 사용. 360° 누적 언랩(점프 방지). + if (minimapRef.current) { + const arr = allDroneFramesRef.current; + let rawYaw = dronePose.yaw; + if (arr.length) { + let lo = 0, hi = arr.length - 1; + while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].frame < estFrame) lo = m + 1; else hi = m; } + const i2 = lo, i1 = Math.max(0, lo - 1); + const f1 = arr[i1].frame, f2 = arr[i2].frame; + const frac = f2 > f1 ? Math.min(1, Math.max(0, (estFrame - f1) / (f2 - f1))) : 0; + const a = smoothFrame(arr, i1, 3), b = smoothFrame(arr, i2, 3); // ±3fr 가벼운 평활 + let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180; + rawYaw = a.yaw + dy * frac; + } + const target = -(rawYaw + paramsRef.current.yawOffset); + const delta = (((target - minimapRotRef.current) % 360) + 540) % 360 - 180; + minimapRotRef.current += delta; + minimapRef.current.style.setProperty('--rot', `${minimapRotRef.current}deg`); + } + // 선로 중심선 (선형) — 독립 토글 if (showCenterlineRef.current && lines.centerlineSegs.length > 0) { ctx.strokeStyle = 'rgba(255,50,50,0.85)'; @@ -996,7 +1030,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const seenSt = new Set(); const seenPoi = new Set(); - // 측점 라벨 — 연속 포즈로 직접 투영 → 이상치 거부 → EMA + // 측점 라벨 — 연속 포즈로 직접 투영 → 이상치 거부 → EMA. + // 선형(중심선) 토글과 함께 ON/OFF (빨간 스테이션 라벨 = 선형의 일부). + if (showCenterlineRef.current) { ctx.font = 'bold 18px monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; @@ -1028,6 +1064,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const tw = ctx.measureText(cleanTitle(stA.title)).width; hitBoxes.push({ kind: 'station', title: stA.title, x0: x - 8, y0: y - 12, x1: lx + tw + 2, y1: y + 12 }); }); + } // POI 마커 — 연속 포즈로 직접 투영 → 이상치 거부 → EMA ctx.font = 'bold 20px sans-serif'; @@ -1102,37 +1139,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; } - // 나침반 HUD — 우측 상단, 측점선 토글과 무관하게 항상 표시 - const compass = compassRef.current; - if (compass) { - const r = 52; - const cx = W - (r + 14), cy = 16 + r; - ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2); - ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fill(); - ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1; ctx.stroke(); - ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - for (const [label, deg] of [['N',0],['E',90],['S',180],['W',270]] as const) { - const rad = (deg-90)*Math.PI/180; - ctx.fillStyle = label==='N' ? '#ff6060' : 'rgba(255,255,255,0.5)'; - ctx.fillText(label, cx+Math.cos(rad)*(r-9), cy+Math.sin(rad)*(r-9)); - } - const yawRad = (compass.effectiveYaw-90)*Math.PI/180; - const tx = cx+Math.cos(yawRad)*(r-14), ty = cy+Math.sin(yawRad)*(r-14); - ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(tx, ty); - ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2.5; ctx.stroke(); - const ha = 0.42, hl = 9; - ctx.beginPath(); - ctx.moveTo(tx, ty); ctx.lineTo(tx-hl*Math.cos(yawRad-ha), ty-hl*Math.sin(yawRad-ha)); - ctx.moveTo(tx, ty); ctx.lineTo(tx-hl*Math.cos(yawRad+ha), ty-hl*Math.sin(yawRad+ha)); - ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2; ctx.stroke(); - ctx.beginPath(); ctx.moveTo(cx, cy); - ctx.arc(cx, cy, r-2, yawRad-compass.hFovRad/2, yawRad+compass.hFovRad/2); ctx.closePath(); - ctx.fillStyle = 'rgba(255,215,0,0.12)'; ctx.fill(); - ctx.strokeStyle = 'rgba(255,215,0,0.35)'; ctx.lineWidth = 1; ctx.stroke(); - ctx.font = '9px monospace'; ctx.textBaseline = 'top'; ctx.fillStyle = '#ffd700'; - ctx.fillText(`${((compass.effectiveYaw+360)%360).toFixed(1)}°`, cx, cy+r+2); - ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; - } + // 라벨 속성 팝업 위치 — 해당 라벨의 현재 표시좌표를 따라 매 프레임 갱신(재생 중 이동). // 라벨이 화면에서 사라지면(필터/화면밖) 팝업 숨김, 다시 보이면 표시. @@ -1155,15 +1162,14 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible popupMissRef.current.set(id, 0); const ph = el.offsetHeight || 120; const pw = el.offsetWidth || 240; - // 팝업은 항상 라벨 '아래'. 라벨 위로는 띄우지 않는다. - // 화면 하단을 넘칠 만큼 라벨이 내려가면(곧 사라지는 중) → 위로 올리지 않고 팝업을 숨김. - const GAP = 26; + // 기본은 라벨 '아래'. 아래 공간이 부족하면(화면 하단 근접) 라벨 '위'로 플립 → + // 라벨과 겹치거나 화면 밖으로 잘리는 것을 방지. 위로도 부족하면 화면 안으로 클램프. + const GAP = 16; const tx = Math.min(Math.max(8, sx - 10), Math.max(8, W - pw - 4)); - const ty = sy + GAP; + let ty = sy + GAP; // 아래 if (ty + ph > H - 2) { - // 라벨이 화면 하단에 근접 → 아래로 못 펼침. 라벨 위로 올리지 않고 숨김(라벨과 함께 사라짐). - el.style.visibility = 'hidden'; - return; + const aboveTy = sy - GAP - ph; // 위로 플립 (팝업 하단이 라벨 위 ~GAP 지점) + ty = aboveTy >= 2 ? aboveTy : Math.max(2, H - 2 - ph); } el.style.visibility = 'visible'; const pp = popupPosRef.current.get(id); @@ -1435,6 +1441,9 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible onPointerCancel={onPoiPointerUp} /> + {/* 나침반 미니맵 — 우측 상단(기존 캔버스 나침반 대체). 핀은 RAF 가 드론 방위로 회전. */} + {geoDataLoaded && } + {/* 라벨 속성 팝업(다중) — 위치는 RAF 가 라벨을 따라 갱신. DOM 이라 텍스트 선명. 컴팩트(기본 5필드) ↔ 전체 토글은 팝업 클릭. 자동(auto) 팝업은 ✕ 없음(가시 구조물 따라 표시). */} {infoPopups.map(pp => { @@ -1475,37 +1484,29 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible })} {showPanel && ( -
-
+
+
+ {/* 두 패널을 가로로 나란히(우→좌) 펼침 — 둘 다 열어도 세로로 안 쌓여 화면 위로 안 넘침. */} +
{/* ── 화면표시 옵션 (카메라 외 모든 표시 설정) ── */} {showDisplay && (
표시 토글
- - - {showDronePath && ( - <> - setDronePathZ(Math.round(v))} /> - setDronePathAlpha(v)} /> - - )} + {/* 선형/드론궤적/좌측패널 토글은 하단 재생바(VideoPlayer)로 이동. 측점 진단 버튼은 삭제. */} + {/* 경로표고·투명도는 드론궤적 ON/OFF 무관하게 항상 표시. */} + setDronePathZ(Math.round(v))} /> + setDronePathAlpha(v)} />
스무딩 (재계산 500ms 후)
@@ -1630,6 +1631,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
)} +
)} diff --git a/client/src/components/player/VideoPlayer.tsx b/client/src/components/player/VideoPlayer.tsx index f904ded..9d19f53 100644 --- a/client/src/components/player/VideoPlayer.tsx +++ b/client/src/components/player/VideoPlayer.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useImperativeHandle, forwardRef, useState, useEffect } from 'react'; +import React, { useRef, useImperativeHandle, forwardRef, useState, useEffect, useMemo } from 'react'; import StationOverlay from '../overlay/StationOverlay'; import RoutePanel from '../overlay/RoutePanel'; import RouteInfoOverlay from '../overlay/RouteInfoOverlay'; @@ -15,6 +15,27 @@ import { useCaptureStore } from '../../store/captureStore'; import HlsConversionStatus from './HlsConversionStatus'; import { StationBar } from '../../stationbar/StationBar'; +/** 드롭된 디렉토리/파일 엔트리에서 모든 파일을 재귀 수집 (폴더 드래그&드롭 지원). */ +function collectDropEntry(entry: FileSystemEntry, out: File[]): Promise { + return new Promise((resolve) => { + if (entry.isFile) { + (entry as FileSystemFileEntry).file((f) => { out.push(f); resolve(); }, () => resolve()); + } else if (entry.isDirectory) { + const reader = (entry as FileSystemDirectoryEntry).createReader(); + const readBatch = () => { + reader.readEntries(async (ents) => { + if (!ents.length) { resolve(); return; } // 모든 배치 소진 + await Promise.all(ents.map((en) => collectDropEntry(en, out))); + readBatch(); + }, () => resolve()); + }; + readBatch(); + } else { + resolve(); + } + }); +} + export interface VideoPlayerHandle { loadLocalFile: (file: File) => void; loadServerStream: (videoId: string, filename: string) => void | Promise; @@ -96,11 +117,20 @@ const VideoPlayer = forwardRef( }, [playerRef, source]); const loadFromFolder = useGeoStore((s) => s.loadFromFolder); const geoLoaded = useGeoStore((s) => s.loaded); + // 드론 정보 — 하단바에 GPS/고도 항상 표시. + const storeFrames = useGeoStore((s) => s.frames); // 시설등급(시설종별) 표시 필터 — 재생바/패널이 구독하는 설정 스토어. const gradeFilter = useSettingsStore((s) => s.gradeFilter); const setGradeFilter = useSettingsStore((s) => s.setGradeFilter); const poiOverlapExclude = useSettingsStore((s) => s.poiOverlapExclude); const setPoiOverlapExclude = useSettingsStore((s) => s.setPoiOverlapExclude); + // 선형(중심선)·드론 궤적 영상 오버레이 토글 — StationOverlay 와 공유(설정 스토어). + const showCenterline = useSettingsStore((s) => s.showCenterline); + const setShowCenterline = useSettingsStore((s) => s.setShowCenterline); + const showDronePath = useSettingsStore((s) => s.showDronePath); + const setShowDronePath = useSettingsStore((s) => s.setShowDronePath); + const showRoutePanel = useSettingsStore((s) => s.showRoutePanel); + const setShowRoutePanel = useSettingsStore((s) => s.setShowRoutePanel); // 하단 도구 패널: UI에서 숨김(코드는 보존). 다시 표시하려면 true 로. const SHOW_TOOLBAR = false; @@ -200,11 +230,30 @@ const VideoPlayer = forwardRef( containerRef: wrapperRef, }); - // Drag and drop local video file + // 드래그&드롭 — 폴더(영상+측점/POI) 또는 단일 영상 파일. + // 폴더는 dataTransfer.files 가 비어 있으므로 webkitGetAsEntry 로 디렉토리를 재귀 순회한다. const handleDrop = (e: React.DragEvent) => { e.preventDefault(); - const file = e.dataTransfer.files[0]; - if (file?.type.startsWith('video/')) loadLocalFile(file); + // await 후엔 dataTransfer 가 무효화될 수 있어 동기적으로 먼저 캡처. + const entries: FileSystemEntry[] = []; + const dt = e.dataTransfer; + if (dt.items) { + for (let i = 0; i < dt.items.length; i++) { + const en = dt.items[i].webkitGetAsEntry?.(); + if (en) entries.push(en); + } + } + const flatFiles = Array.from(dt.files); + void (async () => { + if (entries.length) { + const out: File[] = []; + await Promise.all(entries.map((en) => collectDropEntry(en, out))); + if (out.length) { handleSelectFolder(out); return; } + } + if (flatFiles.length > 1) { handleSelectFolder(flatFiles); return; } + const file = flatFiles[0]; + if (file?.type.startsWith('video/')) loadLocalFile(file); + })(); }; // VIDEO_FPS: 영상 실제 fps (29.97 = 30000/1001). Python SRT FrameCnt 기준과 일치. @@ -213,6 +262,18 @@ const VideoPlayer = forwardRef( const frame = secondsToFrame(currentTime, VIDEO_FPS); const videoId = source?.kind === 'server' ? source.videoId : null; + // 드론 정보(측점진단) — 현재 프레임에 가장 가까운 드론 프레임의 GPS/고도만 표시. + // 드론 GPS·고도 HUD 는 항상 표시 → 토글 없이 프레임만 있으면 계산. + const stationDiag = useMemo(() => { + if (!storeFrames.length) return null; + let best = storeFrames[0], bd = Math.abs(storeFrames[0].frame - frame); + for (const df of storeFrames) { + const d = Math.abs(df.frame - frame); + if (d < bd) { bd = d; best = df; } + } + return { f: best }; + }, [storeFrames, frame]); + return (
( /> {/* 노선 정보 배너 — 영상 좌상단 (route.json routeInfo) */} - {/* 좌하단 그룹(재생바 위): 위→아래 = 배속 → 토글(측점선/영상제어) + 프레임정보 */} + {/* 좌하단 그룹(재생바 위) — 한 줄: 영상제어 → 배속 → 프레임정보 → 겹침제외 → 시설등급 */} {source && (
- {/* 재생 배속 (영상제어 ON) */} + {/* 영상제어 토글 — 버튼만, 고정폭(가장 긴 'OFF' 기준이라 ON/OFF로 폭이 안 변함) */} + {showVideoControls && ( -
- 배속 - {[0.5, 1, 1.5, 2, 3, 4].map((r) => ( - - ))} -
+ <> + {/* 배속 패널 (영상제어 우측) */} +
+ 배속 + {[0.5, 1, 1.5, 2, 3, 4].map((r) => ( + + ))} +
+ {/* 영상 프레임 표시 패널 — 프레임 번호를 고정폭(6ch)으로 묶어 자릿수가 늘어도 폭 불변 */} + + {secondsToTimecode(currentTime)} | F{frame} | {fps}fps + + {geoLoaded && ( + <> + {/* 좌측 패널 토글 — 선형 왼편, 동일 폭(min-w-[96px]) */} + + {/* 선형(중심선) 토글 — 겹침제외와 동일 폭(min-w-[96px]) */} + + {/* 드론 궤적 토글 — 겹침제외와 동일 폭(min-w-[96px]) */} + + {/* 드론 위치정보 — 드론궤적 버튼 바로 우측에 붙임. GPS·고도 항상 표시. */} + {stationDiag && ( +
+ GPS {stationDiag.f.lat.toFixed(6)}, {stationDiag.f.lon.toFixed(6)} + · 절대고도 {stationDiag.f.altitude.toFixed(1)}m +
+ )} + {/* 겹침제외 토글 — UI 숨김(기능/상태는 보존, 기본 ON). 다시 보이려면 주석 해제. */} + {/* */} + {/* 시설등급 패널 */} +
+ 시설등급 + {KNOWN_GRADES.map((g) => ( + + ))} +
+ + )} + )} - {/* 한 줄(좌→우): 영상제어 토글 → 프레임표시 패널 → POI옵션 패널 → 시설등급 패널 */} -
- - {showVideoControls && ( - <> - {/* 영상 프레임 표시 패널 */} - - {secondsToTimecode(currentTime)} | F{frame} | {fps}fps - - {geoLoaded && ( - <> - {/* POI 옵션 패널 (시설등급 왼쪽) — 겹침제외 토글 */} -
- POI 옵션 - -
- {/* 시설등급 패널 (프레임표시 우측) */} -
- 시설등급 - {KNOWN_GRADES.map((g) => ( - - ))} -
- - )} - - )} -
)} {/* 루트 패널 미니맵 — 위(배너+카메라파라미터)·아래(배속/토글/재생바) 침범 방지 */} playerRef.current?.currentTime(time)} topPx={paramTop + 40} bottomPx={(barHeight || 130) + 90} @@ -357,6 +439,7 @@ const VideoPlayer = forwardRef( onTogglePlay={handleTogglePlay} showPanel={showVideoControls && !!source} topPx={paramTop} + barHeight={barHeight} /> {/* 측점 기반 재생 바 — 영상 하단에 오버레이로 앵커 (폴더 로드 후) */} diff --git a/client/src/stationbar/StationBar.module.scss b/client/src/stationbar/StationBar.module.scss index 1062590..b23a825 100755 --- a/client/src/stationbar/StationBar.module.scss +++ b/client/src/stationbar/StationBar.module.scss @@ -20,8 +20,10 @@ bottom: 0; width: 1920px; height: 1080px; - transform: scale(var(--bar-scale)); - transform-origin: bottom left; + /* transform: scale() 는 텍스트를 비트맵으로 축소해 흐려진다. zoom 은 축소된 크기로 '다시 렌더' + → 글자가 네이티브 해상도로 또렷. 내부 px 좌표/커서/seek 계산은 그대로 동작. + (지원: Chrome·Edge·Safari·Firefox 126+) */ + zoom: var(--bar-scale); pointer-events: none; } diff --git a/client/src/stationbar/StationBar.tsx b/client/src/stationbar/StationBar.tsx index 8abbef3..2f24f8f 100755 --- a/client/src/stationbar/StationBar.tsx +++ b/client/src/stationbar/StationBar.tsx @@ -94,6 +94,8 @@ export interface StructMark { props?: { k: string; v: string }[]; /** 종점역인데 영상이 도착하지 못함 → 끝에 '미도착' 스타일(속 빈 링)로 표시. */ unreached?: boolean; + /** 표시 측점값이 그 위치에 실제로 존재하는가. false=좌표 반경만으로 잡힌 통과(값 라벨·검색 제외). */ + kmExists?: boolean; } interface StationBarProps { @@ -182,15 +184,23 @@ export function StationBar({ ); const storeFrames = useGeoStore((s) => s.frames); const routeMeta = useGeoStore((s) => s.routeMeta); + const directionChanges = useGeoStore((s) => s.directionChanges); // v2.0: 구조물은 geoStore.structures(CSV 03)교량/04)터널/06)구교 + route.json 보정) 를 우선 사용. const storeStructuresRaw = useGeoStore((s) => s.structures); // 시설종별 필터(설정 스토어) — 체크 변경 시 즉시 반영(구독). const gradeFilter = useSettingsStore((s) => s.gradeFilter); + const poiOverlapExclude = useSettingsStore((s) => s.poiOverlapExclude); const storeStructures = useMemo( () => storeStructuresRaw.filter((s) => isGradeVisible(s.grade, gradeFilter)), [storeStructuresRaw, gradeFilter], ); const viewedRef = useRef([]); + // 이동거리축: time[i] ↔ frac[i](누적 이동거리 비율 0~1, 단조 비감소). + // 드론의 '실제 이동거리'를 옆으로 펴서 배치 → 같은 측점이라도 이동량만큼 떨어져 보인다. + // 호버(드론 이동 無) 구간은 frac 정체 → 커서 정지. 방향(전진/후진)은 barSegments 색 리본으로 구분. + // 이동거리(누적) 축: time[i]↔frac[i](누적 이동거리 비율 0~1, 단조 비감소) → 커서 항상 우측 이동. + // depChain=출발측점, arrChain=도착측점 → 방향색 기준(도착방향=주황/반대=파랑). + const chainRef = useRef<{ time: Float64Array; frac: Float64Array; depChain: number; arrChain: number } | null>(null); const [ready, setReady] = useState(false); const stations = useMemo( @@ -223,8 +233,9 @@ export function StationBar({ useEffect(() => { const frames = storeFrames; if (!frames.length || !stations.length || !stationLine) { setReady(false); return; } - const out: ViewedPoint[] = new Array(frames.length); - for (let i = 0; i < frames.length; i++) { + const n = frames.length; + const out: ViewedPoint[] = new Array(n); + for (let i = 0; i < n; i++) { const f = frames[i]; out[i] = { frameNum: f.frame, @@ -233,6 +244,31 @@ export function StationBar({ time: f.frame / videoFps, }; } + // 이동거리 축: 평활 측점값(±W 이동평균)의 프레임간 |Δ| 누적 = 실제 이동량 → 전체로 정규화(frac). + // 호버(이동 無)면 frac 정체 → 커서 정지. 이동하면(전진/후진 무관) frac 증가 → 커서 항상 우측. + // depChain/arrChain(앞·뒤 5% 평균)은 방향색 기준. + const W = 8; // 평활 반폭(프레임) + const pre = new Float64Array(n + 1); + for (let i = 0; i < n; i++) pre[i + 1] = pre[i] + out[i].chain; + const time = new Float64Array(n); + const sm = new Float64Array(n); + const frac = new Float64Array(n); + let cum = 0; + for (let i = 0; i < n; i++) { + const lo = Math.max(0, i - W), hi = Math.min(n, i + W + 1); + sm[i] = (pre[hi] - pre[lo]) / (hi - lo); + if (i > 0) cum += Math.abs(sm[i] - sm[i - 1]); + time[i] = out[i].time; + frac[i] = cum; + } + const total = cum > 0 ? cum : 1; + for (let i = 0; i < n; i++) frac[i] /= total; + const seg = Math.max(1, Math.floor(n * 0.05)); + let dep = 0, arr = 0; + for (let i = 0; i < seg; i++) { dep += sm[i]; arr += sm[n - 1 - i]; } + dep /= seg; arr /= seg; + if (Math.abs(arr - dep) < 1) arr = dep + 1; // 퇴화 방지 + chainRef.current = { time, frac, depChain: dep, arrChain: arr }; viewedRef.current = out; setReady(true); }, [stations, stationLine, storeFrames, videoFps]); @@ -273,35 +309,87 @@ export function StationBar({ // 시간→px 변환에 쓰는 유효 트랙폭 (미도착폭 제외). const timeTrackWidth = TRACK_WIDTH_PX - endGapPx; - // 프레임(시간) 진행률 px — 커서의 단조 이동 기준. - // 영상은 노선을 시간순으로 한 번 통과(leg 01→10)하므로, 시간 진행률이 곧 - // 트랙을 좌→우로 지나는 경로 진행률이다. - const progressPx = - duration > 0 - ? TRACK_START_PX + clamp(currentTime / duration, 0, 1) * timeTrackWidth - : TRACK_START_PX; + // 노선 진행방향 '상'|'하' — 동명 시설물의 (상)/(하) 변형 중 어느 쪽을 바에 표출할지 결정. + // 1순위 route.json direction, 없으면 directionChanges(상행↔하행 전환점)에서 가장 오래 지속된 방향. + const routeDirCh = useMemo<'상' | '하' | null>(() => { + const d = routeMeta?.routeInfo?.direction ?? ''; + const up = d.includes('상'), down = d.includes('하'); + if (up && !down) return '상'; + if (down && !up) return '하'; + const dc = directionChanges; + if (dc && dc.length && duration > 0) { + const acc: Record = {}; + let prevT = 0; + let cur = dc[0].from; + for (const c of dc) { + acc[cur] = (acc[cur] ?? 0) + Math.max(0, c.atSeconds - prevT); + prevT = c.atSeconds; + cur = c.to; + } + acc[cur] = (acc[cur] ?? 0) + Math.max(0, duration - prevT); + let best = '', bv = -1; + for (const k in acc) if (acc[k] > bv) { bv = acc[k]; best = k; } + if (best.includes('상') && !best.includes('하')) return '상'; + if (best.includes('하') && !best.includes('상')) return '하'; + } + return null; + }, [routeMeta, directionChanges, duration]); - // 커서 위치 = 프레임 진행률(progressPx)로만 단조 이동. - // 측점 km은 노선에서 여러 번 반복(증가↔감소)되어, km으로 px를 역산하면 - // 같은 km의 여러 후보 중 하나로 스냅되며 구간 점프가 생긴다(사용자 보고). - // → 진행은 유니크한 프레임 기준, 표시(배지)만 현재 보는 측점(realKm)으로 한다. - const cursorPx = progressPx; - - // 커서 배지 = 폴더 데이터 기반 연속 체이니지(realChain)를 10m 단위로 표시. - // mock ROUTE_LEGS(mileageAtPx) 의존 제거. 데이터 없으면 빈 문자열. - const cursorText = - realChain !== null && realChain >= 0 ? formatMileage10(realChain) : ''; + // 진행도(이동거리축): 시간 t → 누적 이동거리 비율(frac, 0~1). chainRef 미준비 시 시간선형 폴백. + const cumFracAtTime = useCallback( + (t: number): number => { + const c = chainRef.current; + if (!c || !ready || duration <= 0) + return duration > 0 ? clamp(t / duration, 0, 1) : 0; + const { time, frac } = c; + const n = time.length; + if (n === 0) return 0; + if (t <= time[0]) return frac[0]; + if (t >= time[n - 1]) return frac[n - 1]; + let lo = 0, hi = n - 1; + while (hi - lo > 1) { const m = (lo + hi) >> 1; if (time[m] <= t) lo = m; else hi = m; } + const span = time[hi] - time[lo]; + const r = span > 0 ? (t - time[lo]) / span : 0; + return frac[lo] + (frac[hi] - frac[lo]) * r; + }, + [ready, duration], + ); + // 역변환(이동거리 비율 → 시간): 바 클릭 seek 용. frac 단조 비감소 → 이진 탐색. + const timeAtFrac = useCallback( + (f: number): number => { + const c = chainRef.current; + if (!c || !ready || duration <= 0) return clamp(f, 0, 1) * duration; + const { time, frac } = c; + const n = frac.length; + if (n === 0) return 0; + const ff = clamp(f, 0, 1); + if (ff <= frac[0]) return time[0]; + if (ff >= frac[n - 1]) return time[n - 1]; + let lo = 0, hi = n - 1; + while (hi - lo > 1) { const m = (lo + hi) >> 1; if (frac[m] <= ff) lo = m; else hi = m; } + const span = frac[hi] - frac[lo]; + const r = span > 0 ? (ff - frac[lo]) / span : 0; + return time[lo] + (time[hi] - time[lo]) * r; + }, + [ready, duration], + ); // ── 데이터 기반 측점 바 ────────────────────────────────────────── - // 시간(프레임) 진행을 트랙 px로 선형 변환. + // 시간(프레임) → 트랙 px (이동거리축). 커서·구간색·구조물 마커가 이 매핑을 공유한다. const pxAtTime = useCallback( (t: number): number => - duration > 0 - ? TRACK_START_PX + clamp(t / duration, 0, 1) * timeTrackWidth - : TRACK_START_PX, - [duration, timeTrackWidth], + TRACK_START_PX + clamp(cumFracAtTime(t), 0, 1) * timeTrackWidth, + [cumFracAtTime, timeTrackWidth], ); + // 커서 위치 = 이동거리축 진행도 → 전진/후진 무관하게 항상 우측으로 이동. 색만 방향에 따라 바뀜. + const progressPx = pxAtTime(currentTime); + const cursorPx = progressPx; + + // 커서 배지 = 폴더 데이터 기반 연속 체이니지(realChain)를 10m 단위로 표시. 데이터 없으면 빈 문자열. + const cursorText = + realChain !== null && realChain >= 0 ? formatMileage10(realChain) : ''; + // viewedRef(프레임→측점 km) 추이로 구간 색·전환점 산출. // km 증가 구간 = dir 1(주황), 감소 구간 = dir -1(하늘색). HYST로 최근접 지터 무시. const { barSegments, kmLabels } = useMemo<{ @@ -309,25 +397,35 @@ export function StationBar({ kmLabels: KmLabel[]; }>(() => { const arr = viewedRef.current; - if (!ready || !arr.length || duration <= 0) + const n = arr.length; + if (!ready || !n || duration <= 0) return { barSegments: [], kmLabels: [] }; - // 방향은 연속 체이니지(chain)로 판정 — 100m 양자화 km은 전환을 ~13s 빨리 잡아 영상과 어긋남. - const HYST = 100; // m — 이 이상 반대로 움직여야 방향 전환으로 인정 (GPS 노이즈 무시) + // 측점값(chain)을 ±W 이동평균으로 평활 → GPS 지터 제거. 평활했으므로 HYST를 작게 잡아 + // '목적지에 가까워짐/멀어짐'의 작은 추세 전환(20m+)도 구간으로 잡는다. (커서 배지 = 그 구간 색) + const W = 8; + const pre = new Float64Array(n + 1); + for (let i = 0; i < n; i++) pre[i + 1] = pre[i] + arr[i].chain; + const sm = new Float64Array(n); + for (let i = 0; i < n; i++) { + const lo = Math.max(0, i - W), hi = Math.min(n, i + W + 1); + sm[i] = (pre[hi] - pre[lo]) / (hi - lo); + } + const HYST = 10; // m — 평활 후 기준. 이 이상 반대로 움직이면 방향(가까워짐↔멀어짐) 전환. const segs: BarSegment[] = []; const bounds: { chain: number; time: number; turn: boolean }[] = [ - { chain: arr[0].chain, time: arr[0].time, turn: false }, + { chain: sm[0], time: arr[0].time, turn: false }, ]; - // 시작 방향을 실제 데이터(첫 유의미 이동)로 판정. 기본 증가 가정이 틀리면 첫 구간 색이 반대로 나옴. + // 시작 방향을 실제 데이터(첫 유의미 이동)로 판정. let dir: 1 | -1 = 1; - for (let i = 1; i < arr.length; i++) { - const d = arr[i].chain - arr[0].chain; + for (let i = 1; i < n; i++) { + const d = sm[i] - sm[0]; if (Math.abs(d) >= HYST) { dir = d > 0 ? 1 : -1; break; } } - let extCh = arr[0].chain; + let extCh = sm[0]; let extIdx = 0; let startIdx = 0; - for (let i = 1; i < arr.length; i++) { - const c = arr[i].chain; + for (let i = 1; i < n; i++) { + const c = sm[i]; if (dir > 0 ? c > extCh : c < extCh) { extCh = c; extIdx = i; @@ -346,10 +444,10 @@ export function StationBar({ } segs.push({ startPx: pxAtTime(arr[startIdx].time), - endPx: pxAtTime(arr[arr.length - 1].time), + endPx: pxAtTime(arr[n - 1].time), dir, }); - bounds.push({ chain: arr[arr.length - 1].chain, time: arr[arr.length - 1].time, turn: false }); + bounds.push({ chain: sm[n - 1], time: arr[n - 1].time, turn: false }); // 전환(턴) 지점은 실제 위치를 10m 단위로, 시·종점 등 기본 라벨은 100m 단위로 표시. const labels: KmLabel[] = bounds.map((b) => ({ px: pxAtTime(b.time), @@ -469,50 +567,71 @@ export function StationBar({ if (!poiByName.has(base)) poiByName.set(base, { lat: p.lat, lon: p.lon, category: p.category }); } - // 첫·마지막 스테이션(역사)을 트랙 양끝에 고정 표출. - // 역사 마커 중 최좌측 → 트랙 시작(TRACK_START_PX), 최우측 → 트랙 끝(TRACK_END_PX)으로 스냅. - // (드론 항로가 역사에서 떨어져 지나가도 첫/끝 역이 바 양끝에 앵커되도록.) - const snapStationEnds = (marks: StructMark[]): StructMark[] => { - const st = marks - .map((m, i) => ({ i, px: m.px })) - .filter((_, i) => marks[i].category === '역사' || marks[i].category === '역'); - if (st.length < 2) return marks; - let lo = st[0], hi = st[0]; - for (const x of st) { - if (x.px < lo.px) lo = x; - if (x.px > hi.px) hi = x; - } - if (lo.i !== hi.i) { - marks[lo.i] = { ...marks[lo.i], px: TRACK_START_PX }; - // 종점역: 끝(END)에 두되, 미도착폭이 설정돼 있으면 '미도착'으로 표시 - // (영상 진행바·커서는 END−endGapPx 까지만 도달 → 끝의 회색 구간 = 미도착). - marks[hi.i] = { ...marks[hi.i], px: TRACK_END_PX, unreached: endGapPx > 0 }; + // 양끝 스냅 제거: 각 역사/구조물은 '실제 통과 위치(시간축 px)' 그대로 둔다 + // → 마커 위치가 그 지점 커서 측점값과 일치(스냅으로 인한 측점 어긋남 해소). + // 종점역 '미도착'만 별도 처리: 영상이 종점 측점에 못 미치면(endGapPx>0) 종점역(최우측 역사)을 + // 트랙 끝(TRACK_END = 미도착 gap 안, 실제 종점 측점 위치)에 두고 미도착 스타일로 표시한다. + const placeUnreachedTerminal = (marks: StructMark[]): StructMark[] => { + if (endGapPx <= 0) return marks; + let hiIdx = -1; + let hiPx = -Infinity; + for (let i = 0; i < marks.length; i++) { + if ((marks[i].category === '역사' || marks[i].category === '역') && marks[i].px > hiPx) { + hiPx = marks[i].px; + hiIdx = i; + } } + if (hiIdx >= 0) marks[hiIdx] = { ...marks[hiIdx], px: TRACK_END_PX, unreached: true }; return marks; }; + // 경로상 시설물만: 시설물명에 방향표기((상…/(하…)가 있으면 영상 진행방향과 일치하는 것만. + // 하행이면 '(상…' 제외, 상행이면 '(하…' 제외. (예: 회덕천교(상)/(상인상)은 하행 영상에서 숨김) + // 진행방향(routeDirCh)의 반대 변형은 바에서 제외 → 상행이면 (상), 하행이면 (하) 만 남는다. + const oppDirCh = routeDirCh === '하' ? '상' : routeDirCh === '상' ? '하' : ''; + const isOppositeDir = (name: string): boolean => + !!oppDirCh && new RegExp(`[((]${oppDirCh}`).test(name); + // v2.0: CSV 유래 구조물(storeStructures) 우선. route.json 보정은 geoData 에서 이미 병합됨. if (storeStructures && storeStructures.length) { const out: StructMark[] = []; for (const s of storeStructures) { // 구교(06)는 영상 오버레이에 표출 → 스테이션바에서는 제외(교량/터널만). if (s.category === '구교') continue; + if (isOppositeDir(s.name)) continue; // 반대 방향(다른 선로) 시설물 제외 const cat = s.type === 'tunnel' ? '터널' : s.type === 'bridge' ? '교량' : '역사'; const sBase = s.name.replace(/\s*[((].*$/, '').trim(); // 이름 매칭 POI 실좌표 우선 → 없으면 route.json 이정값 폴백. const match = poiByName.get(sBase) ?? [...poiByName.entries()].find(([k]) => k.includes(sBase) || sBase.includes(k))?.[1]; - // 우선순위: station(측점값) → 직접 좌표 → POI 이름매칭 → 이정값 폴백. + // 우선순위: station(측점값) → 좌표를 측점선에 투영한 '측점 기준' 탐지 → 좌표 근접(최후 폴백) → 이정값. // 각 통과 지점마다 마커(드론이 2번 지나면 2개). 동명 시설물은 각자 station 으로 구분. + // ※ 좌표 근접(pxPassesTo)은 출발점 부근 등에서 엉뚱한 '조기 통과'를 잡아 마커가 앞쪽(좌측)으로 + // 잘못 배치될 수 있다 → 좌표를 측점값으로 환산해 station-기준으로 탐지하면 실제 통과순서와 일치. let passes: { px: number; km: number }[] = []; const off = s.offset ?? 0; if (s.station != null) { const sM = mileageToMeters(s.station); if (sM >= 0) passes = pxPassesAtMileage(sM); } + if (!passes.length && s.lat != null && s.lon != null && stationLine) + passes = pxPassesAtMileage(projectChainage(s.lat, s.lon, stationLine)); + if (!passes.length && match && stationLine) + passes = pxPassesAtMileage(projectChainage(match.lat, match.lon, stationLine)); if (!passes.length && s.lat != null && s.lon != null) passes = pxPassesTo(s.lat, s.lon, off); if (!passes.length && match) passes = pxPassesTo(match.lat, match.lon, off); + // 역사(역) 통과 보강: 측점선 투영이 불안정한 종점/조차장 구역에서는 측점-기준 탐지가 + // 일부 통과를 놓친다(예: 대전조차장 162k080 재진입은 투영 측점이 ±20m 밖이라 누락). + // 이미 한 통과를 찾으면 위 단락평가로 좌표근접 탐지가 안 돌아 영영 빠지므로, 여기서 + // 좌표근접(pxPassesTo) 통과를 '합집합'으로 합쳐 빠진 재진입까지 마커를 찍는다. + // 같은 통과(거의 같은 px)는 24px 임계로 중복 제거 → 멀리 떨어진 실제 재진입만 추가됨. + if (cat === '역사' && s.lat != null && s.lon != null) { + for (const cp of pxPassesTo(s.lat, s.lon, off)) { + if (!passes.some((p) => Math.abs(p.px - cp.px) < 24)) passes.push(cp); + } + passes.sort((a, b) => a.px - b.px); + } if (!passes.length && s.startMileage != null && s.endMileage != null) { const mid = (s.startMileage + s.endMileage) / 2; const px = pxAtMileage(mid); @@ -522,33 +641,54 @@ export function StationBar({ // 역은 바 양끝에 스냅되므로, 시작/끝 커서값(= 역 위치 측점)과 일치시키기 위함. const stationKmVal = cat === '역사' && s.lat != null && s.lon != null && stationLine ? projectChainage(s.lat, s.lon, stationLine) : null; - for (const p of passes) out.push({ px: p.px, title: s.name, category: cat, km: stationKmVal ?? p.km, props: s.props }); + const existTol = routeMeta?.routeInfo?.stationTolerance ?? 20; + for (const p of passes) { + // 표시 측점값(stationKmVal)과 그 통과의 실제 측점(p.km)이 허용오차 밖이면(=좌표 반경만으로 + // 잡힌 통과, 그 위치엔 실제로 그 측점값이 없음) 측점값 라벨/검색에서 제외. 마커(동그라미)는 유지. + const kmExists = stationKmVal == null || Math.abs(p.km - stationKmVal) <= existTol; + out.push({ px: p.px, title: s.name, category: cat, km: stationKmVal ?? p.km, kmExists, props: s.props }); + } } - return snapStationEnds(out); + return placeUnreachedTerminal(out); } // 폴백: POI category 기반 (실좌표 GPS 근접, 모든 통과). const out: StructMark[] = []; for (const [base, info] of poiByName) { + if (isOppositeDir(base)) continue; // 반대 방향(다른 선로) 시설물 제외 const stationKmVal = info.category === '역사' && stationLine ? projectChainage(info.lat, info.lon, stationLine) : null; - for (const p of pxPassesTo(info.lat, info.lon)) { - out.push({ px: p.px, title: base, category: info.category, km: stationKmVal ?? p.km }); + const existTol = routeMeta?.routeInfo?.stationTolerance ?? 20; + // 측점 기준 탐지 우선(통과순서 정확), 측점선 없으면 좌표 근접 폴백. + const ps = stationLine + ? pxPassesAtMileage(projectChainage(info.lat, info.lon, stationLine)) + : pxPassesTo(info.lat, info.lon); + for (const p of ps) { + const kmExists = stationKmVal == null || Math.abs(p.km - stationKmVal) <= existTol; + out.push({ px: p.px, title: base, category: info.category, km: stationKmVal ?? p.km, kmExists }); } } - return snapStationEnds(out); + return placeUnreachedTerminal(out); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ready, duration, storeFrames, pois, stations, pxAtTime, routeMeta, storeStructures, pxAtMileage, endGapPx, stationLine, videoFps]); + }, [ready, duration, storeFrames, pois, stations, pxAtTime, routeMeta, routeDirCh, storeStructures, pxAtMileage, endGapPx, stationLine, videoFps]); - // 방향 색 트랙 gradient 빌더(전진/후진 색을 받아 구성). 구간 경계는 부드럽게 섞는다. + // 여정 정방향(목적지=도착 방향): 측점값이 출발(depChain)→도착(arrChain) 으로 변하는 방향. + // 그 방향으로 움직이는 구간=정방향(황색), 반대=역방향(청색). + const forwardDir = useMemo<1 | -1>(() => { + const c = chainRef.current; + if (!ready || !c) return 1; + return c.arrChain >= c.depChain ? 1 : -1; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ready, storeFrames]); + + // 방향 색 트랙 gradient — 이동거리축은 구간 px가 좌→우 단조라 gradient 1장으로 충분(경량). + // 정방향(도착방향)=FWD, 역방향=BWD. 구간 경계는 하드 스톱. const buildGradient = useCallback( (FWD: string, BWD: string): string => { const segs = barSegments; if (!segs.length) return ''; - const col = (d: 1 | -1) => (d > 0 ? FWD : BWD); - const pct = (px: number) => - clamp(((px - TRACK_START_PX) / TRACK_WIDTH_PX) * 100, 0, 100); - // 전환점에서 색이 '한번에' 바뀌도록 하드 스톱(같은 위치에 두 색). + const col = (d: 1 | -1) => (d === forwardDir ? FWD : BWD); + const pct = (px: number) => clamp(((px - TRACK_START_PX) / TRACK_WIDTH_PX) * 100, 0, 100); const stops: string[] = [`${col(segs[0].dir)} 0%`]; for (let i = 1; i < segs.length; i++) { const bp = pct(segs[i].startPx).toFixed(2); @@ -558,31 +698,27 @@ export function StationBar({ stops.push(`${col(segs[segs.length - 1].dir)} 100%`); return `linear-gradient(to right, ${stops.join(', ')})`; }, - [barSegments, duration], + [barSegments, forwardDir], ); - // 통과 구간 음영 그라데이션(videoplayer 원본): 구간마다 좌→우 3색 셰이딩, 구간 경계는 하드. - // 전진 주황: #ffc257 → #ff8a25 → #ff7b1b / 역방향 청록: #5ca887 → #35a7a7 → #06a4c8 + // 통과 음영(구간마다 좌→우 3색). 정방향 주황 / 역방향 청록. const buildShaded = useCallback( (fwd: [string, string, string], bwd: [string, string, string]): string => { const segs = barSegments; if (!segs.length) return ''; - const cols = (d: 1 | -1) => (d > 0 ? fwd : bwd); - const pct = (px: number) => - clamp(((px - TRACK_START_PX) / TRACK_WIDTH_PX) * 100, 0, 100); + const cols = (d: 1 | -1) => (d === forwardDir ? fwd : bwd); + const pct = (px: number) => clamp(((px - TRACK_START_PX) / TRACK_WIDTH_PX) * 100, 0, 100); const stops: string[] = []; for (const s of segs) { - const a = pct(s.startPx); - const b = pct(s.endPx); - const c = cols(s.dir); + const a = pct(s.startPx), b = pct(s.endPx), c = cols(s.dir); stops.push(`${c[0]} ${a.toFixed(2)}%`); stops.push(`${c[1]} ${((a + b) / 2).toFixed(2)}%`); stops.push(`${c[2]} ${b.toFixed(2)}%`); } return `linear-gradient(to right, ${stops.join(', ')})`; }, - [barSegments], + [barSegments, forwardDir], ); - // 통과(지나간): 구간별 음영 그라데이션. 미통과: 단색(전진 회색 / 역방향 청회색). + // 통과(지나간): 음영. 미통과: 단색(정방향 회색 / 역방향 청회색). const trackGradient = useMemo( () => buildShaded(['#ffc257', '#ff8a25', '#ff7b1b'], ['#5ca887', '#35a7a7', '#06a4c8']), [buildShaded], @@ -591,13 +727,14 @@ export function StationBar({ // 방향(색)이 바뀌는 전환점 px — 구분선 위치. const dividers = useMemo(() => barSegments.slice(1).map((s) => s.startPx), [barSegments]); - // 현재 위치가 역방향(km 감소, 하늘색) 구간이면 커서도 파란색. + // 커서 배지 색 = 현재 위치한 '구간' 색과 동일(바 배경과 일치). 역방향(목적지 반대) 구간이면 파란색. + // → 배지·바 배경이 항상 같은 색(기존 방식). 구간 색은 forwardDir 기준 정/역방향. const currentReverse = useMemo(() => { for (const s of barSegments) { - if (cursorPx >= s.startPx && cursorPx <= s.endPx) return s.dir < 0; + if (cursorPx >= s.startPx && cursorPx <= s.endPx) return s.dir !== forwardDir; } return false; - }, [barSegments, cursorPx]); + }, [barSegments, cursorPx, forwardDir]); // 컨테이너 폭 기준 균등 스케일 useEffect(() => { @@ -638,7 +775,7 @@ export function StationBar({ const el = wrapRef.current; if (el && duration > 0) { const t = timeRef.current ?? 0; - const pos = TRACK_START_PX + clamp(t / duration, 0, 1) * timeTrackWidth; + const pos = pxAtTime(t); // 이동거리축: 현재 진행도 → px el.style.setProperty('--pos-px', `${pos}px`); el.style.setProperty('--cursor-x', `${renderX(pos)}px`); } @@ -646,18 +783,17 @@ export function StationBar({ }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); - }, [timeRef, duration, timeTrackWidth]); + }, [timeRef, duration, pxAtTime]); - // 클릭/드래그 트랙 px → 시간(프레임) 선형 변환으로 seek (시간축 일관). + // 클릭/드래그 트랙 px → 이동거리 비율 → 시간(역변환)으로 seek (이동거리축 일관). const seekToTrackX = useCallback( (trackX: number) => { if (duration <= 0) return; const px = clamp(trackX, TRACK_START_PX, TRACK_END_PX); - // 미도착 구간(END−endGapPx 우측)을 클릭하면 영상 끝(duration)으로 클램프. - const targetTime = ((px - TRACK_START_PX) / timeTrackWidth) * duration; + const targetTime = timeAtFrac((px - TRACK_START_PX) / timeTrackWidth); onSeek(clamp(targetTime, 0, duration)); }, - [duration, onSeek, timeTrackWidth], + [duration, onSeek, timeTrackWidth, timeAtFrac], ); const seekFromClientX = useCallback( @@ -709,31 +845,74 @@ export function StationBar({ // 측점입력(예: 158k200) → 그 측점을 보는(최근접) 프레임으로 seek (실데이터 기반). // 해당 영상이 커버하는 측점(체이니지) 범위 밖이면 무시(seek 안 함). + // 측점 검색 순환 상태 — 같은 측점값을 연속 Enter 할 때 '직전 점프 위치'에서 다음으로 이어가기 위함. + const jumpRef = useRef<{ km: number; time: number } | null>(null); const handleJumpToMileage = useCallback( (km: number) => { const arr = viewedRef.current; if (!arr.length || duration <= 0) return; - // 연속 체이니지(chain) 기준으로 입력 측점에 가장 가까운 프레임 탐색 + 커버 범위 산출. - let best = -1; - let bd = Infinity; - let lo = Infinity; - let hi = -Infinity; + // 커버 측점 범위 산출 (구간 밖 입력은 무시). + let lo = Infinity, hi = -Infinity; for (let i = 0; i < arr.length; i++) { const c = arr[i].chain; if (c < lo) lo = c; if (c > hi) hi = c; - const d = Math.abs(c - km); - if (d < bd) { - bd = d; - best = i; - } } - // 구간 밖(커버 측점 범위 ± 여유 20m)이면 무시. const MARGIN = 20; if (km < lo - MARGIN || km > hi + MARGIN) return; - if (best >= 0) onSeek(clamp(arr[best].time, 0, duration)); + + // 1순위: '화면에 보이는 마커(structureMarks)' 중 표시 측점값이 일치하는 것들의 시각. + // → 마커는 좌표근접 탐지까지 포함하므로(조차장 등 chain 이 어긋난 통과 포함) 검색이 모든 + // 동그라미 위치로 이동 가능. px 는 timeAtFrac 로 시각 역변환. 매칭은 표시(10m 반올림) 일치. + const tgt10 = Math.round(km / 10); + let times: number[] = structureMarks + .filter((m) => m.km >= 0 && m.kmExists !== false && Math.round(m.km / 10) === tgt10) + .map((m) => timeAtFrac((m.px - TRACK_START_PX) / timeTrackWidth)); + // 2순위(마커 없음=임의 측점): 연속 체이니지 기준 통과(허용오차 내 연속구간=1통과). + if (!times.length) { + const TOL = routeMeta?.routeInfo?.stationTolerance ?? 20; + let i = 0; + while (i < arr.length) { + if (Math.abs(arr[i].chain - km) < TOL) { + let j = i, bj = i, bd = Math.abs(arr[i].chain - km); + while (j < arr.length && Math.abs(arr[j].chain - km) < TOL) { + const d = Math.abs(arr[j].chain - km); + if (d < bd) { bd = d; bj = j; } + j++; + } + times.push(arr[bj].time); + i = j; + } else i++; + } + if (!times.length) { + let best = 0, bd = Infinity; + for (let k = 0; k < arr.length; k++) { + const d = Math.abs(arr[k].chain - km); + if (d < bd) { bd = d; best = k; } + } + times.push(arr[best].time); + } + } + // 시간 오름차순 + 근접 중복 제거(같은 통과가 두 경로/마커로 잡힌 경우). + times.sort((a, b) => a - b); + const passes: number[] = []; + for (const t of times) if (!passes.length || t - passes[passes.length - 1] > 0.3) passes.push(t); + + // 기준 시각: 같은 측점을 '직전 점프 위치 그대로'에서 다시 Enter 하면 연속(다음 통과), + // 재생/클릭으로 커서가 움직였거나 다른 측점이면 현재 커서(라이브) 시각에서 새로 시작. + const liveT = timeRef?.current ?? currentTime; + const same = !!jumpRef.current + && Math.abs(jumpRef.current.km - km) < 1e-6 + && Math.abs(liveT - jumpRef.current.time) < 0.1; + const baseT = same ? jumpRef.current!.time : liveT; + // 통과방향(시간 증가)으로 baseT 다음 통과. 끝까지 가면 처음으로 순환. + const EPS = 1e-3; + const target = passes.find((t) => t > baseT + EPS) ?? passes[0]; + + jumpRef.current = { km, time: target }; + onSeek(clamp(target, 0, duration)); }, - [duration, onSeek], + [duration, onSeek, routeMeta, currentTime, timeRef, structureMarks, timeAtFrac, timeTrackWidth], ); return ( @@ -764,11 +943,8 @@ export function StationBar({ startStationName={startStationName} endStationName={endStationName} endGapPx={endGapPx} - routeDir={ - routeMeta?.routeInfo?.direction?.includes('하') ? '하' - : routeMeta?.routeInfo?.direction?.includes('상') ? '상' - : null - } + routeDir={routeDirCh} + overlapExclude={poiOverlapExclude} />
): void => { if (e.key !== 'Enter') return; const mileage = parseMileageQuery(query); - if (mileage !== null) { - onJumpToMileage(mileage); - setQuery(''); - } + // Enter 후에도 입력값 유지 → 같은 측점 재Enter 시 통과방향 다음 위치로 순환 검색. + if (mileage !== null) onJumpToMileage(mileage); }; + // 포커스를 잃으면(다른 곳 클릭 등) 입력값 삭제. + const handleQueryBlur = (): void => setQuery(''); + return (
@@ -67,6 +68,7 @@ export function PlaybackControls({ value={query} onChange={handleQueryChange} onKeyDown={handleQueryKeyDown} + onBlur={handleQueryBlur} placeholder="측점입력" />
diff --git a/client/src/stationbar/components/Timeline/Timeline.tsx b/client/src/stationbar/components/Timeline/Timeline.tsx index a619a82..8d3dbe7 100755 --- a/client/src/stationbar/components/Timeline/Timeline.tsx +++ b/client/src/stationbar/components/Timeline/Timeline.tsx @@ -51,9 +51,9 @@ function splitStructLabel(text: string): string[] { interface TimelineProps { posPx: number; onSeekDown: (e: MouseEvent) => void; - /** 통과 구간 트랙 gradient (전진=주황/후진=청록). */ + /** 통과 구간 트랙 gradient (정방향=주황/역방향=청록). */ trackGradient: string; - /** 미통과 구간 트랙 gradient (전진=회색/후진=청회색). */ + /** 미통과 구간 트랙 gradient (정방향=회색/역방향=청회색). */ trackGradientIdle: string; /** 색(방향)이 바뀌는 전환점 px(스테이지 좌표) — 구분선 위치. */ dividers: number[]; @@ -69,6 +69,8 @@ interface TimelineProps { endGapPx?: number; /** 영상 진행 방향('상'|'하'). 평행 상/하행 구조물 겹침 시 해당 방향을 우선 표출. */ routeDir?: '상' | '하' | null; + /** 겹침제외(POI 옵션). true=겹친 구조물 1개만, false=모두 표시. */ + overlapExclude?: boolean; } export function Timeline({ @@ -83,6 +85,7 @@ export function Timeline({ endStationName, endGapPx = 0, routeDir = null, + overlapExclude = true, }: TimelineProps) { // 하단 스테이션바는 겹쳐도 무방 → 역사(역, KAKAO_RAIL)는 dedup 대상에서 제외하고 항상 표출. // (종점 '대전조차장'이 ~70px 앞 '법동가도교'에 밀려 사라지던 문제 방지.) @@ -90,18 +93,84 @@ export function Timeline({ // 평행 상/하행 구조물(예: 회덕터널 상/하)은 ~6px 차이라 한쪽만 남는데, 영상 방향(routeDir)에 // 해당하는 쪽(하행=‘(하)’, 상행=‘(상)’)을 우선 남긴다. → 드론이 실제 지나는 선로와 일치. const isStation = (s: StructMark): boolean => s.category === '역사' || s.category === '역'; - const dirTag = routeDir === '상' ? '(상)' : routeDir === '하' ? '(하)' : ''; + // '(상' / '(하' 로 매칭 — 이름이 "법동가도교(상,인상,고속)" 처럼 괄호 안 다중 토큰이어도 인식. + // (이전 '(상)' 완전일치는 다중토큰 이름에서 매칭 실패해 잘못된 변형이 선택되던 버그) + const dirTag = routeDir === '상' ? '(상' : routeDir === '하' ? '(하' : ''; + const baseStruct = (t: string): string => t.replace(/\s*[((].*$/, '').trim(); const others = structures.filter((s) => !isStation(s)); const ordered = dirTag ? [...others].sort((a, b) => (a.title.includes(dirTag) ? 0 : 1) - (b.title.includes(dirTag) ? 0 : 1) || a.px - b.px) : [...others].sort((a, b) => a.px - b.px); - const keptOthers: StructMark[] = []; + // 같은 구조물의 상/하/인상 변형(같은 base = 같은 좌표)은 '겹침제외 옵션과 무관하게' 진행방향 1개만. + // (dir-우선 정렬돼 있어 방향 변형이 먼저 = 채택. 예: 법동가도교(하)/(인상) → (하) 1개) + const byBase = new Map(); for (const it of ordered) { - if (keptOthers.every((k) => Math.abs(k.px - it.px) >= 130)) keptOthers.push(it); + const b = baseStruct(it.title); + if (!byBase.has(b)) byBase.set(b, it); } + const keptOthers = [...byBase.values()]; + // 라벨은 긴 이름 2줄(아래 splitStructLabel)로 폭을 줄여 표시. 그래도 가까워 겹치면 그대로 둠(엇갈림 없음). + void overlapExclude; const structs = [...structures.filter(isStation), ...keptOthers].sort((a, b) => a.px - b.px); + // 측점값 라벨 겹침 숨김: px 오름차순으로 보며, 직전에 '표시한' 라벨과 시각적으로 겹치면 + // (중앙 간격 < 두 라벨 평균 폭) 뒤엣것을 숨긴다 → 먼저 나온(좌측) 것만 1개 표시. + // 같은/근접 측점을 여러 번 통과해 측점값이 겹쳐 보이던 문제 방지. (아이콘·점선은 그대로 둠) + const labelW = (v: string): number => v.length * 7.5 + 4; // 13px bold + 2px stroke 대략 폭(px) + const showMileage: boolean[] = new Array(structs.length).fill(false); + { + let lastPx = -Infinity; + let lastW = 0; + for (let i = 0; i < structs.length; i++) { + const s = structs[i]; + // 측점값 없음(s.km<0) 또는 '그 위치에 실제 측점값이 존재하지 않음'(kmExists=false, + // 좌표 반경만으로 잡힌 통과)은 측점값 라벨을 그리지 않는다. (마커 동그라미는 별도로 유지) + if (s.km < 0 || s.kmExists === false) continue; + const w = labelW(fmtKm(s.km)); + if (s.px - lastPx >= (lastW + w) / 2) { + showMileage[i] = true; + lastPx = s.px; + lastW = w; + } + } + } + + // 라벨 그룹핑: 같은 항목(동일 title)을 드론이 여러 번 지나면 통과 마커가 흩어져 이름이 반복된다. + // → 동명 통과를 거리와 무관하게 하나로 묶어, 각 통과에 점선 드롭 + 양끝을 잇는 하단 수평 점선(브래킷)을 + // 그리고 묶음 중앙에 라벨 1개만 표시한다. (단일 통과는 브래킷 없이 자기 위치 라벨 1개) + // 아이콘·측점값은 통과별로 그대로 표출. + const labelGroups: { + title: string; + km: number; + category: string; + unreached?: boolean; + centerPx: number; // 라벨 위치 및 passed 판정 기준 (묶음 중앙) + bracketPxs: number[]; // 길이 ≥2면 브래킷(드롭+수평선) 표시 대상 통과 px (정렬) + }[] = []; + { + const byTitle = new Map(); + for (const s of structs) { + const a = byTitle.get(s.title); + if (a) a.push(s); + else byTitle.set(s.title, [s]); + } + for (const arr of byTitle.values()) { + const sorted = [...arr].sort((a, b) => a.px - b.px); + // 동명 통과(역사 포함)는 하나로 묶어 브래킷(드롭+수평선) + 묶음 라벨 1개. 단일 통과는 자기 위치 라벨. + const lo = sorted[0].px; + const hi = sorted[sorted.length - 1].px; + labelGroups.push({ + title: sorted[0].title, + km: sorted[0].km, + category: sorted[0].category, + unreached: sorted.some((m) => m.unreached), + centerPx: (lo + hi) / 2, + bracketPxs: sorted.length > 1 ? sorted.map((m) => m.px) : [], + }); + } + } + // 구조물 마커 클릭 시 속성 팝업(클릭 좌표 기준 fixed — 트랙 래퍼 transform 영향 회피). const [sel, setSel] = useState<{ s: StructMark; x: number; y: number } | null>(null); @@ -146,7 +215,7 @@ export function Timeline({ {/* 데이터 기반 색 트랙(그라데이션): 전진=주황 / 후진=하늘색. 전체는 저톤(드론 순/역방향 미리보기), 재생되어 커서가 지나간 구간은 원래 색으로 복원. */}
- {/* 미재생(미통과): 회색/청회색 (전체 폭) */} + {/* 미재생(미통과): 방향별 저톤 미리보기 (전체 폭) */}
- {/* 재생 불가 구간(종점역 미도착): 트랙 우측 endGapPx 전체. - 재생영역(회색 idle)보다 약간 밝은 단색 배경으로만 구분(빗금 없음). */} + {/* 재생 불가 구간(종점역 미도착): 트랙 우측 endGapPx 전체. */} {endGapPx > 0 && (
)} - {/* 재생된 구간: 원래 색 복원 (커서까지 clip). 폭은 CSS 변수(--pos-px)로 매 프레임 - 직접 갱신되어 React 리렌더 없이 부드럽게 늘어난다. */} + {/* 재생된 구간: 원래 색 복원 (커서까지 clip, --pos-px CSS변수로 매 프레임 갱신). */}
{structs.map((s, i) => - s.km >= 0 ? ( + s.km >= 0 && showMileage[i] ? ( - {/* 구조물 클릭 핫스팟 — 클릭 시 속성 팝업(회덕화물역 등 역사 포함). seekArea(z20) 위로. */} -
- {structs.map((s, i) => - s.props && s.props.length ? ( -
{ e.stopPropagation(); setSel({ s, x: e.clientX, y: e.clientY }); }} - style={{ position: 'absolute', left: s.px - 12, top: 28, width: 24, height: 28, cursor: 'pointer', pointerEvents: 'auto' }} - /> - ) : null, - )} + {/* 구조물 클릭 핫스팟 제거 — 스테이션바 클릭은 속성 팝업을 띄우지 않고 seek 으로만 동작. + (POI 정보는 화면 오버레이 라벨 클릭에서 확인) */} + + {/* 동일 항목 묶음 — 각 통과 위치를 수직 점선으로 내려, 라벨 '문자 세로 중앙'을 지나는 + 수평 점선으로 잇는다. 수평선은 라벨 뒤로 지나가 글자 양옆으로만 보인다(두번째 참고 이미지). + z-index 미지정(auto) → DOM 상 뒤의 labelRow 가 위에 그려져 글자가 선을 덮는다. */} +
+ {labelGroups + .filter((g) => g.bracketPxs.length > 1) + .map((g, gi) => { + const lo = g.bracketPxs[0]; + const hi = g.bracketPxs[g.bracketPxs.length - 1]; + // 라벨: 절대 top 53, 줄높이 18px(structName). 문자 세로 중앙 y = 53 + 줄수*9. + const lineCount = splitStructLabel(g.title).length; + const lineY = 53 + lineCount * 9; + const dash = '1.5px dashed rgba(232, 232, 232, 0.6)'; + return ( + + {/* 문자 세로 중앙을 지나는 수평 점선 (라벨 뒤) */} +
+ {/* 각 통과 수직 점선 (트랙 하단 → 수평선) */} + {g.bracketPxs.map((bp, j) => ( +
+ ))} + + ); + })}
- {/* 구조물명 라벨 (바 아래) — 통과 시 아이콘과 함께 색 변경. 긴 명칭은 2줄. */} + {/* 구조물명 라벨 (바 아래) — 통과 시 아이콘과 함께 색 변경. 긴 명칭은 2줄. + 같은 항목 다중통과는 labelGroups(클러스터)로 묶여 클러스터당 라벨 1개만 표출. */}
- {structs.map((s, i) => { - const W = s.category === '터널' ? 24 : 22; + {labelGroups.map((g, i) => { + const W = g.category === '터널' ? 24 : 22; // 미도착 종점역은 항상 미통과(neutral) 색 유지. - const passed = s.unreached ? false : posPx >= s.px - W / 2; - const lines = splitStructLabel(s.title); + const passed = g.unreached ? false : posPx >= g.centerPx - W / 2; + // 폭 처리: 긴 이름은 2줄(균등 분할)로 폭을 줄여 표시. 그래도 가까워 겹치면 그대로 둠. + const lines = splitStructLabel(g.title); return (
{lines.length === 1 ? lines[0] : <>{lines[0]}
{lines[1]}}
diff --git a/client/src/store/settingsStore.ts b/client/src/store/settingsStore.ts index fabfa16..a2d601b 100644 --- a/client/src/store/settingsStore.ts +++ b/client/src/store/settingsStore.ts @@ -22,6 +22,18 @@ interface SettingsStore { /** 화면 POI 겹침제외(겹치면 1개만). true=겹친 것 숨김(기본), false=모든 POI 표시. */ poiOverlapExclude: boolean; setPoiOverlapExclude: (v: boolean) => void; + /** 좌측 노선 패널(RoutePanel) 표시 여부. '화면표시 옵션' 토글로 제어. 기본 표시. */ + showRoutePanel: boolean; + setShowRoutePanel: (v: boolean) => void; + /** 측점 진단 HUD(드론 GPS·투영측점) 하단바 표시 여부. 기본 off. */ + showStationDiag: boolean; + setShowStationDiag: (v: boolean) => void; + /** 선형(중심선) 영상 오버레이 표시 여부. 기본 표시. (VideoPlayer 바 토글로 제어) */ + showCenterline: boolean; + setShowCenterline: (v: boolean) => void; + /** 드론 궤적 영상 오버레이 표시 여부. 기본 표시. (VideoPlayer 바 토글로 제어) */ + showDronePath: boolean; + setShowDronePath: (v: boolean) => void; } const DEFAULT_GRADE_FILTER: Record = { @@ -39,6 +51,14 @@ export const useSettingsStore = create()( set((s) => ({ gradeFilter: { ...s.gradeFilter, [grade]: checked } })), poiOverlapExclude: true, setPoiOverlapExclude: (v) => set({ poiOverlapExclude: v }), + showRoutePanel: true, + setShowRoutePanel: (v) => set({ showRoutePanel: v }), + showStationDiag: false, + setShowStationDiag: (v) => set({ showStationDiag: v }), + showCenterline: true, + setShowCenterline: (v) => set({ showCenterline: v }), + showDronePath: true, + setShowDronePath: (v) => set({ showDronePath: v }), }), { name: 'ghivideo.settings', diff --git a/client/src/utils/chainage.ts b/client/src/utils/chainage.ts new file mode 100644 index 0000000..734b729 --- /dev/null +++ b/client/src/utils/chainage.ts @@ -0,0 +1,64 @@ +/** 측점값(체이니지) 공유 유틸 — 드론 GPS 를 측점 폴리라인에 투영해 측점값/최근접측점 산출. + * StationBar(배지)·VideoPlayer(측점 진단 HUD) 가 동일 계산을 공유한다. */ + +export interface ChainLine { + pts: { x: number; y: number; km: number; lat: number; lon: number; title: string }[]; + k: number; // 경도→m 환산 (cos(lat0)*111000) +} + +/** "157K970" → 157970(m). 실패 시 -1. */ +export function kmFromTitle(title: string): number { + const m = title.match(/(\d+)[Kk](\d+)/); + return m ? parseInt(m[1], 10) * 1000 + parseInt(m[2], 10) : -1; +} + +/** 미터값 → "157k970" (10m 단위). */ +export function fmtKm10(m: number): string { + const r = Math.round(m / 10) * 10; + return `${Math.floor(r / 1000)}k${String(r % 1000).padStart(3, '0')}`; +} + +/** 측점 POI 목록 → 평면투영 폴리라인(km 오름차순). 측점이 없으면 null. */ +export function buildChainLine(stations: { title: string; lat: number; lon: number }[]): ChainLine | null { + const sts = stations.filter((s) => kmFromTitle(s.title) >= 0); + if (!sts.length) return null; + const sorted = [...sts].sort((a, b) => kmFromTitle(a.title) - kmFromTitle(b.title)); + const lat0 = sorted.reduce((s, p) => s + p.lat, 0) / sorted.length; + const k = Math.cos((lat0 * Math.PI) / 180) * 111000; + return { + pts: sorted.map((p) => ({ x: p.lon * k, y: p.lat * 111000, km: kmFromTitle(p.title), lat: p.lat, lon: p.lon, title: p.title })), + k, + }; +} + +/** 드론 lat/lon 을 폴리라인에 투영 → { km(측점값), offsetM(선로 수직이격) }. */ +export function projectToChain(lat: number, lon: number, line: ChainLine): { km: number; offsetM: number } { + const px = lon * line.k; + const py = lat * 111000; + const pts = line.pts; + let bestD = Infinity; + let bestKm = pts.length ? pts[0].km : -1; + for (let i = 0; i < pts.length - 1; i++) { + const a = pts[i], b = pts[i + 1]; + const dx = b.x - a.x, dy = b.y - a.y, L2 = dx * dx + dy * dy; + const t = L2 === 0 ? 0 : Math.max(0, Math.min(1, ((px - a.x) * dx + (py - a.y) * dy) / L2)); + const cx = a.x + dx * t, cy = a.y + dy * t; + const d = (px - cx) ** 2 + (py - cy) ** 2; + if (d < bestD) { bestD = d; bestKm = a.km + (b.km - a.km) * t; } + } + return { km: bestKm, offsetM: Math.sqrt(bestD) }; +} + +/** 드론 lat/lon 의 최근접 측점(점) → { title, km, distM }. */ +export function nearestChainPoint(lat: number, lon: number, line: ChainLine): { title: string; km: number; distM: number } | null { + const pts = line.pts; + if (!pts.length) return null; + let nIdx = 0, nD = Infinity; + for (let i = 0; i < pts.length; i++) { + const dx = (lat - pts[i].lat) * 111000; + const dy = (lon - pts[i].lon) * line.k; + const d = dx * dx + dy * dy; + if (d < nD) { nD = d; nIdx = i; } + } + return { title: pts[nIdx].title, km: pts[nIdx].km, distM: Math.sqrt(nD) }; +} diff --git a/docs/assets/report.css b/docs/assets/report.css new file mode 100644 index 0000000..350b375 --- /dev/null +++ b/docs/assets/report.css @@ -0,0 +1,77 @@ +@page { + size: A4; + margin: 18mm 16mm 16mm 16mm; + @bottom-center { + content: counter(page) " / " counter(pages); + font-family: "Malgun Gothic", sans-serif; + font-size: 9pt; + color: #999; + } +} + +html { font-size: 11pt; } + +body { + font-family: "Malgun Gothic", "Hancom Gothic", sans-serif; + color: #23272e; + line-height: 1.65; + max-width: 920px; + margin: 0 auto; + padding: 24px; +} + +h1 { + font-size: 1.7rem; + color: #b45309; /* amber-700 — 앱 활성색 계열 */ + border-bottom: 3px solid #f59e0b; /* amber-500 */ + padding-bottom: 8px; + margin: 0 0 4px; +} + +h2 { + font-size: 1.25rem; + color: #b45309; + border-bottom: 1px solid #e5d3b3; + padding-bottom: 5px; + margin-top: 1.6em; +} + +h3 { font-size: 1.05rem; color: #92400e; margin-top: 1.1em; } + +a { color: #b45309; } + +hr { border: none; border-top: 1px solid #e2e2e2; margin: 1.6em 0; } + +ul { padding-left: 1.25em; } +li { margin: 0.18em 0; } + +strong { color: #1f2937; } + +code { + font-family: "D2Coding", Consolas, monospace; + background: #f4f1ea; + border: 1px solid #e7e0d2; + border-radius: 3px; + padding: 0.5px 5px; + font-size: 0.92em; +} + +table { + border-collapse: collapse; + width: 100%; + margin: 0.8em 0; + font-size: 0.95em; +} +th, td { border: 1px solid #d8d2c4; padding: 6px 10px; text-align: left; vertical-align: top; } +th { background: #fdf3df; color: #7c2d12; } + +blockquote { + border-left: 4px solid #f59e0b; + margin: 0.8em 0; + padding: 0.2em 0 0.2em 14px; + color: #555; + background: #fffbf2; +} + +/* ★ 강조 항목이 페이지 경계에서 잘리지 않게 */ +h1, h2, h3 { break-after: avoid; } diff --git a/docs/history/2026-06-26_0913_동일항목-묶음-브래킷.md b/docs/history/2026-06-26_0913_동일항목-묶음-브래킷.md new file mode 100644 index 0000000..54b21c5 --- /dev/null +++ b/docs/history/2026-06-26_0913_동일항목-묶음-브래킷.md @@ -0,0 +1,36 @@ +# 동일 항목 묶음 브래킷 표시 + +**소요 시간**: 약 15분 +**Context 사용량**: input ~55k / output ~6k tokens + +## 결론 +스테이션바 타임라인에서 같은 항목(동일 역명)을 드론이 여러 번 통과할 때, 두 번째 참고 이미지처럼 +**점선 브래킷(각 통과 수직 드롭 + 하단 수평 연결선)** 으로 묶고 **클러스터 중앙에 라벨 1개** 만 +표시하도록 변경. + +## 근거 / 변경 내용 +- 파일: `client/src/stationbar/components/Timeline/Timeline.tsx` +- 기존: 다중통과 시 `labelEntries`(마지막 통과 라벨 1개 + 나머지 중앙 라벨 1개) + 통과별 점선 틱(`leaderPxs`) + → 같은 항목인데 라벨이 2개로 쪼개져 표시됨. +- 변경: `labelGroups` 로 재구성. + - 같은 title 통과들을 px 간격(`GROUP_GAP_PX = 350`)으로 **클러스터 분할** — 가까운 통과는 한 묶음, + 멀리 떨어진 통과(예: 대전조차장 두 차례 진입)는 별도 묶음 → 각자 브래킷/라벨 1개. + - 클러스터당: 각 통과 px에 수직 드롭(top 46, h4) + 양끝을 잇는 수평 점선(top 50) + 중앙(centerPx) 라벨 1개. + - 단일 통과는 브래킷 없이 자기 위치 라벨 1개(기존 동작 유지). + - 색(passed/neutral) 판정 기준을 통과 px → 클러스터 `centerPx` 로 변경. 미도착(unreached)은 묶음 내 + 하나라도 있으면 neutral 유지. + +## 검증 +- `tsc --noEmit` 통과 (node 20 환경). + +## 후속 수정 (가로 점선 미표시 → 거리 클러스터링 제거) +- 1차 구현은 `GROUP_GAP_PX = 350` 으로 동명 통과를 거리별 클러스터로 분할 → 대전조차장역 두 통과가 + ~1050px 떨어져 각각 단일 묶음이 되어 브래킷(가로 점선)이 안 나옴. +- 수정: 거리 클러스터링 제거. **동명(title) 통과는 거리와 무관하게 하나로 묶어** lo~hi 전체에 + 수평 점선 + 각 통과 드롭 + 중앙 라벨 1개를 그림. (사용자 요청: "가로 점선이 보여야해") + +## 다음 단계 참고 +- 브래킷 수직 위치(드롭 46~50, 수평 50, 라벨행 51)는 트랙 하단(46.5)/labelRow(51) 기준. 라벨 폰트/행간 + 변경 시 함께 점검. +- 동명 통과가 매우 멀리 떨어진 경우 중앙 라벨이 빈 트랙 중앙에 놓일 수 있음(가로 점선으로 연결됨). 추후 + 묶음 분할이 다시 필요하면 거리 임계값 방식을 재도입할 것. diff --git a/docs/history/2026-06-26_1411_스테이션바-거리축-전환.md b/docs/history/2026-06-26_1411_스테이션바-거리축-전환.md new file mode 100644 index 0000000..b7ec67c --- /dev/null +++ b/docs/history/2026-06-26_1411_스테이션바-거리축-전환.md @@ -0,0 +1,35 @@ +# 스테이션바 거리축(이동량 기반) 전환 + +**소요 시간**: 약 20분 +**Context 사용량**: input ~90k / output ~9k tokens + +## 결론 +스테이션바 커서/구간/마커의 가로축을 **시간(프레임) 선형 → 누적 이동거리(거리축)** 로 전환. +드론이 호버링(이동 없음)하면 커서가 멈추고, 실제로 이동할 때만 커서가 진행한다. (사용자 요청 A안) + +## 근거 / 변경 내용 +- 파일: `client/src/stationbar/StationBar.tsx` +- 기존: `px = 시작 + (currentTime/duration) × 트랙폭` — 순수 시간 정비례. 호버 중에도 커서 전진(배지 측점값은 정지 → 불일치). +- 변경: 시간↔진행도 매핑을 거리축으로 교체. 핵심은 매핑 함수 한 곳에 집중되어 커서·구간색·구조물 마커가 일관되게 따라옴. + 1. `progRef`: 프레임별 `{time, frac}` 배열. `frac` = 누적 이동거리 비율(0~1, 단조 비감소). + 2. 진행도 산출(viewedRef 빌드 useEffect): 연속 측점값(chain)을 ±W(8프레임) 이동평균으로 평활(GPS 지터 제거) 후 + 프레임간 `|Δ|` 누적 = 실제 이동량. 전체로 정규화. + 3. `cumFracAtTime(t)`: 시간→진행비율(이진탐색+선형보간). progRef 미준비 시 시간선형 폴백. + 4. `timeAtFrac(f)`: 역변환(진행비율→시간) — 바 클릭/측점 seek 용. + 5. `pxAtTime(t) = 시작 + cumFracAtTime(t) × 트랙폭` — 모든 배치가 공유. + 6. 라이브 커서 RAF, `seekToTrackX` 도 거리축 함수 사용. + +## 동작 +- 호버(이동 無): frac 정체 → 커서 정지. 이동: frac 증가 → 좌→우 진행. +- 전진/후진 모두 이동량으로 더해져 커서는 단조 진행(방향은 기존 주황/청록 구간색으로 구분). +- 배지 측점값과 커서가 동일 데이터(연속 측점값)에서 파생 → 표시·이동 일관. + +## 검증 +- `tsc --noEmit` 통과 + `client/dist` 빌드 완료. (pm2 ghiVideo 는 정적 서빙 → 재시작 불필요, 브라우저 새로고침) + +## 다음 단계 참고 / 되돌리기 +- 사용자가 "이상하면 원복" 예정. 되돌리려면 `pxAtTime`/`cumFracAtTime`/`timeAtFrac` 를 기존 시간선형 + (`(t/duration)`)으로 복귀시키고 `progRef`/평활 블록 제거하면 됨. +- 평활 반폭 `W=8`, 호버 판정은 평활로 처리(별도 데드밴드 없음). 호버 중 커서가 미세하게 기어가면 W를 키우거나 + 소량 데드밴드 추가. +- 부작용: 호버가 길어도 바에서 폭 0으로 수렴 → 바 클릭으로 호버 내부 시간 미세 seek 불가(영상 타임라인 사용). diff --git a/docs/history/2026-06-26_1520_스테이션바-측점축-전환.md b/docs/history/2026-06-26_1520_스테이션바-측점축-전환.md new file mode 100644 index 0000000..b9d8c7a --- /dev/null +++ b/docs/history/2026-06-26_1520_스테이션바-측점축-전환.md @@ -0,0 +1,43 @@ +# 스테이션바 측점거리(체이니지) 비례 축 전환 (B안) + +**소요 시간**: 약 30분 +**Context 사용량**: input ~150k / output ~14k tokens + +## 결론 +스테이션바 x축을 직전 A안(누적 이동거리) → **측점값(체이니지) 비례 축**으로 전환. +바 가로위치가 실제 측점거리에 비례하고, 커서는 드론의 현재 측점 위치를 표시한다(멈추면 정지, 후진하면 좌로 이동). +측점 10m 차이 구조물이 바에서도 가깝게 표시된다. 색: '진행색 + 커서색' 안 선택. + +## 배경 +- A안(누적 이동거리)은 두 구조물이 측점상 10m여도 그 사이 드론이 앞뒤로 많이 이동하면 누적거리가 커져 바에서 멀게 표시됨 → 사용자 지적. +- 요구: 바는 측점거리에 비례, 재생 중 실제 드론 위치를 바에 표시. + +## 변경 내용 (client/src/stationbar/StationBar.tsx) +- `chainRef = {time, chain(평활), minC, maxC}` 생성(ready useEffect). chain 은 ±8프레임 이동평균(지터 제거). +- 매핑: `pxAtChain(측점) = START + clamp((c-minC)/(maxC-minC)) * timeTrackWidth`, + `chainAtTime(t)`(이진탐색+보간), `pxAtTime(t) = pxAtChain(chainAtTime(t))`. 커서·구조물 공유. +- 커서: `progressPx = pxAtTime(currentTime)`, RAF 도 `pxAtTime(liveTime)`. +- 색(측점축은 방향 리본 표현 불가): `trackGradient`=주황 음영 단일, `trackGradientIdle`=회색 단일, + `dividers=[]`. 방향은 `currentReverse`(측점값 0.4s 전 대비 감소=후진)로 커서 색만 구분. +- 구조물 중복 제거: `dedupeMarksByTitle`(title 당 1개) — 측점축에선 동명 다중통과가 한 자리에 겹침. + → Timeline 다중통과 묶음 점선(브래킷)은 자연 소멸. +- seek: 클릭 px → 목표 측점 → 허용오차 내 프레임 중 현재 시간에 가장 가까운 통과로 onSeek. +- `barSegments` 는 렌더 미사용(쿼리 useMemo 는 kmLabels 위해 유지), `cumFracAtTime/timeAtFrac/progRef` 제거. + +## 후속 수정 — 축 범위 '노선 고정' (데이터 무관 절대 측점) +- 문제: 축 범위를 드론 커버 구간 [minC,maxC] 로 잡아 영상마다 양끝 측점이 바뀜 → 같은 구조물이 다른 + 위치/측점으로 표시됨(사용자 지적: "어떤 데이터로 재생하느냐에 따라 측점이 바뀌면 안돼"). +- 수정: + 1. 축 범위 minC/maxC = stationLine(역 POI, km 오름차순) 첫/끝 측점값 → 노선 고정. 드론 chain 은 + projectChainage 로 이 범위 안에 들어옴. + 2. 구조물 px = pxAtChain(구조물 측점값) → 통과 시각이 아닌 '절대 측점'으로 배치(마일리지 라벨과 일치, + 영상 달라도 동일 위치). dedupeMarksByTitle 로 동명 1개. +- 효과: 좌(낮은 측점)→우(높은 측점) 고정 자. 구조물 위치 불변, 커서만 드론 현재 측점을 가리킴. + +## 검증 +- `tsc --noEmit` 통과 + client 빌드 완료. + +## 알려진 한계 / 다음 단계 +- 진짜 비례라 측점이 매우 가까운 라벨끼리 겹쳐 보일 수 있음(법동가도교/대전조차장역). 필요시 라벨 충돌회피 후처리. +- 방향 전진/후진 색 리본 제거됨(커서 색으로만 방향). endGapPx(미도착 종점역) 우측 밴드/표식은 유지. +- 이상 시 A안(누적거리 축) 또는 그 이전 시간선형으로 되돌릴 수 있음. 핵심 매핑은 pxAtChain/chainAtTime/pxAtTime 3개에 집중. diff --git a/docs/history/2026-06-29_1029_역사-통과-좌표근접-보강.md b/docs/history/2026-06-29_1029_역사-통과-좌표근접-보강.md new file mode 100644 index 0000000..f638522 --- /dev/null +++ b/docs/history/2026-06-29_1029_역사-통과-좌표근접-보강.md @@ -0,0 +1,40 @@ +# 역사(역) 통과 마커 누락 수정 — 좌표근접 합집합 보강 + +**소요 시간**: 약 30분 +**Context 사용량**: input ~150k / output ~9k tokens + +## 결론 +대전조차장역처럼 같은 역을 여러 번 지날 때, 일부 통과(예: 162k080 재진입)에 스테이션바 동그라미(역사 원형 마커)가 +빠지던 문제를 수정. 측점-기준 탐지에 더해 **좌표근접(`pxPassesTo`) 통과를 합집합으로 보강**해 누락된 재진입까지 마커를 찍는다. + +## 원인 +- 파일: `client/src/stationbar/StationBar.tsx` (`structureMarks`) +- 역사(대전조차장역)는 `geoData`에서 좌표(lat/lon)만 있고 측점값(`station`)이 없는 RouteStructure 로 들어온다. +- 통과 탐지가 `projectChainage(좌표)` 한 값으로 `pxPassesAtMileage` 만 수행. +- 탐지 체인이 `if (!passes.length)` **단락 평가**라, 측점-기준이 통과를 하나라도 찾으면 좌표근접 탐지가 안 돌아감. +- 종점/조차장 구역은 측점선(폴리라인) 투영이 불안정 → 한 통과는 투영 측점이 ±`stationTolerance`(기본 20m) + 안에 들어와 잡히지만, 다른 통과는 20m 밖으로 벗어나 누락. 첫 통과를 이미 찾았으니 좌표근접 보강도 안 돌아 영영 빠짐. + +## 변경 내용 +- `cat === '역사'` 이고 lat/lon 이 있으면, 측점-기준 탐지 결과에 `pxPassesTo(lat, lon, off)` 통과를 합집합으로 추가. +- 같은 통과(거의 동일 px)는 24px 임계로 중복 제거 → 멀리 떨어진 실제 재진입만 추가됨. + (TRACK_WIDTH_PX = 1509.5, 24px ≈ 1.6% → 동일 best 프레임 중복만 합쳐지고 떨어진 재진입은 보존) +- km 라벨 동작(stationKmVal 우선)은 기존 유지 — 이번 수정은 '마커 누락'만 대상. + +## 검증 +- `tsc --noEmit` + `vite build` 통과 (node v20.20.2, `~/.nvm/versions/node/v20.20.2/bin`). +- 사용자 화면(포트 55000)에서 폴더 재선택 후 162k080 재진입 동그라미 표출 **확인 완료**. + +## ★ 진짜 블로커 — 빌드/서빙 구조 (이번 세션 최대 교훈) +- 증상: 소스를 여러 번 고쳐도 화면이 전혀 안 바뀜("여전히 빠져있어" 반복). +- 원인: 사용자가 보는 화면은 dev 서버가 아니라 **PM2 앱 `ghiVideo`(포트 55000)** 이고, + `server/dist/.../app.js` 가 `express.static(client/dist)` 로 **빌드 결과를 서빙**한다(ecosystem.config.js). + → `client/src` 만 고치면 절대 반영 안 됨. `client/dist` 가 6/26 빌드로 고정돼 있었음. +- 형제 디렉토리 혼선도 있었음: `videoplayer`(PM2, 5173), `defVideo`(PM2) 가 함께 떠 있음. 작업 경로는 GhiVideo. +- 해결: `npm run build -w client` (node 20) 로 `client/dist` 갱신 → 강력 새로고침(Ctrl+Shift+R) → 폴더 재선택. +- 메모리에 구조 기록: `ghivideo-serve-build`. + +## 참고 / 후속 +- `stationTolerance` 타입 주석(geo.ts:139)은 "기본 40"으로 적혀 있으나 실제 코드 기본값은 `?? 20`. 불일치 정리 권장. +- 좌표근접 탐지 임계는 `pxPassesTo` 내부 `Math.max(100, gmin*3)`(StationBar.tsx:499) — 측점 tolerance(20m)와 별개. +- 출발점 부근 '조기 통과' 오탐 우려는 역사 마커에 한해 합집합으로 한정 → 일반 측점-기준 동작에는 영향 없음. diff --git a/docs/history/2026-06-29_1057_측점값-라벨-겹침-숨김.md b/docs/history/2026-06-29_1057_측점값-라벨-겹침-숨김.md new file mode 100644 index 0000000..2ecee50 --- /dev/null +++ b/docs/history/2026-06-29_1057_측점값-라벨-겹침-숨김.md @@ -0,0 +1,23 @@ +# 측점값 라벨 겹침 숨김 (먼저 나온 것만 표시) + +**소요 시간**: 약 10분 +**Context 사용량**: input ~190k / output ~11k tokens + +## 결론 +스테이션바에서 측점값(예: 157k990) 라벨이 근접 통과로 겹쳐 보일 때, px 오름차순으로 **먼저 나온(좌측) 라벨만 +표시하고 뒤엣것은 숨김** 처리. 아이콘(동그라미)과 점선 브래킷은 그대로 둔다(측점값 텍스트만 숨김). + +## 변경 내용 +- 파일: `client/src/stationbar/components/Timeline/Timeline.tsx` +- `structs`(px 오름차순) 직후 `showMileage: boolean[]` 산출: + - `labelW(v) = v.length * 7.5 + 4` (13px bold + 2px stroke 대략 폭) + - 직전 '표시한' 라벨과 중앙 간격이 `(lastW + w)/2` 미만이면 겹침으로 보고 숨김(`showMileage[i]=false`). + - 표시한 경우에만 `lastPx/lastW` 갱신 → 연쇄 겹침도 누적 기준으로 정확히 판정. +- `mileageRow` 렌더 조건을 `s.km >= 0` → `s.km >= 0 && showMileage[i]` 로 변경. + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드 → 55000 강력 새로고침으로 확인. + +## 참고 +- 라벨은 trackWrapper transform 하위라 px 공간 비교가 시각 겹침과 동치(균일 스케일). +- 폭 추정 상수(7.5/+4)는 폰트/스트로크 변경 시 재조정. diff --git a/docs/history/2026-06-29_1106_선형-드론궤적-바버튼-이동.md b/docs/history/2026-06-29_1106_선형-드론궤적-바버튼-이동.md new file mode 100644 index 0000000..36f4e9a --- /dev/null +++ b/docs/history/2026-06-29_1106_선형-드론궤적-바버튼-이동.md @@ -0,0 +1,26 @@ +# 선형·드론궤적 토글을 재생바 버튼으로 이동 + +**소요 시간**: 약 12분 +**Context 사용량**: input ~210k / output ~13k tokens + +## 결론 +영상 오버레이의 "선형(중심선)"·"드론 궤적" 표시 토글을 **하단 재생바(VideoPlayer)의 겹침제외 버튼 왼쪽**에 +버튼 2개로 추가하고, 화면표시 옵션 창의 기존 동일 기능 버튼 2개는 제거. 버튼 폭은 겹침제외와 동일(min-w-[96px]). + +## 변경 내용 +- `client/src/store/settingsStore.ts` + - `showCenterline`(기본 true), `showDronePath`(기본 true) + 각 setter 추가. persist 로 지속. +- `client/src/components/overlay/StationOverlay.tsx` + - 로컬 `useState` 였던 `showCenterline`/`showDronePath` → 설정 스토어 구독으로 전환(refs/리셋은 그대로). + - 화면표시 옵션 창의 선형/드론궤적 토글 버튼 2개 제거. (드론궤적 경로표고·투명도 슬라이더는 유지) +- `client/src/components/player/VideoPlayer.tsx` + - 스토어 셀렉터 추가, 겹침제외 버튼 바로 앞(좌측)에 `선형 ON/OFF`·`드론궤적 ON/OFF` 버튼 삽입. + - 스타일: 겹침제외와 동일 폭(min-w-[96px]), 색은 기능별(선형=red, 드론궤적=sky). + - 노출 조건은 겹침제외와 동일: 영상제어 ON + geoLoaded. + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드 → 55000 강력 새로고침 확인. + +## 참고 +- 상태를 스토어로 올려 바 버튼과 오버레이 렌더가 단일 소스 공유. +- '기본값' 리셋(StationOverlay)도 스토어 setter 로 동작 유지. diff --git a/docs/history/2026-06-29_1111_경로표고-투명도-항상표시+겹침제외숨김.md b/docs/history/2026-06-29_1111_경로표고-투명도-항상표시+겹침제외숨김.md new file mode 100644 index 0000000..43a99c3 --- /dev/null +++ b/docs/history/2026-06-29_1111_경로표고-투명도-항상표시+겹침제외숨김.md @@ -0,0 +1,22 @@ +# 경로표고·투명도 항상 표시 + 겹침제외 버튼 숨김 + +**소요 시간**: 약 5분 +**Context 사용량**: input ~225k / output ~14k tokens + +## 결론 +화면표시 옵션 창의 "경로 표고"·"투명도" 슬라이더를 드론궤적 ON/OFF와 무관하게 항상 표시하도록 변경. +재생바의 "겹침제외 ON/OFF" 버튼은 UI에서 숨김(기능/상태는 보존). + +## 변경 내용 +- `client/src/components/overlay/StationOverlay.tsx` + - 경로표고·투명도 ParamRow 를 감싸던 `{showDronePath && (...)}` 게이트 제거 → 항상 렌더. +- `client/src/components/player/VideoPlayer.tsx` + - 겹침제외 토글 버튼 JSX 주석 처리(숨김). `poiOverlapExclude` 상태는 스토어에 그대로 존재(기본 ON), + StationOverlay/StationBar/RoutePanel 이 계속 구독. 버튼만 비표시. 다시 보이려면 주석 해제. + +## 검증 +- `tsc --noEmit` + `vite build` 통과(node 20). client/dist 재빌드(11:11). + +## 참고 +- 겹침제외 selector(poiOverlapExclude/setPoiOverlapExclude)는 VideoPlayer 에 선언만 남아 미사용 상태지만 + tsc 통과(noUnusedLocals 미적용). 버튼 재노출 대비 보존. diff --git a/docs/history/2026-06-29_1115_좌측패널-바이동+측점진단버튼삭제.md b/docs/history/2026-06-29_1115_좌측패널-바이동+측점진단버튼삭제.md new file mode 100644 index 0000000..8baac9a --- /dev/null +++ b/docs/history/2026-06-29_1115_좌측패널-바이동+측점진단버튼삭제.md @@ -0,0 +1,28 @@ +# 좌측 패널 버튼 바 이동 + 측점진단 버튼 삭제(GPS·고도 항상 표시) + +**소요 시간**: 약 8분 +**Context 사용량**: input ~245k / output ~16k tokens + +## 결론 +- "좌측 노선 패널 표시" 버튼을 화면표시 옵션 창에서 제거하고, 하단 재생바의 **선형 버튼 왼편**에 + "좌측 패널 ON/OFF" 버튼으로 이동(동일 폭 min-w-[96px], emerald). +- "측점 진단" 토글 버튼 삭제. 드론 GPS·절대고도 HUD 는 **항상 표시**(토글 없음)로 변경. + +## 변경 내용 +- `client/src/components/overlay/StationOverlay.tsx` + - 표시 토글 영역에서 "좌측 노선 패널", "측점 진단" 버튼 2개 제거. + - 미사용된 `showRoutePanel`/`setShowRoutePanel`, `showStationDiag`/`setShowStationDiag` 선언 제거. +- `client/src/components/player/VideoPlayer.tsx` + - `setShowRoutePanel` 셀렉터 추가, 선형 버튼 앞(좌측)에 "좌측 패널 ON/OFF" 버튼 삽입. + - `stationDiag` useMemo 의 `!showStationDiag` 게이트 제거 → 프레임만 있으면 항상 계산. + - GPS·고도 HUD 렌더의 `showStationDiag &&` 게이트 제거 → 항상 표시. + - 미사용된 `showStationDiag` 셀렉터 제거. + +## 바 버튼 순서 (영상제어 ON + geoLoaded) +좌측 패널 → 선형 → 드론궤적 (겹침제외는 이전 요청대로 숨김) + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드(11:15). + +## 참고 +- settingsStore 의 showStationDiag 필드는 남겨둠(다른 잠재 구독 대비). 현재 토글 UI 는 없음. diff --git a/docs/history/2026-06-29_1119_재생바-드론정보-시설등급-재배치.md b/docs/history/2026-06-29_1119_재생바-드론정보-시설등급-재배치.md new file mode 100644 index 0000000..80ebc2c --- /dev/null +++ b/docs/history/2026-06-29_1119_재생바-드론정보-시설등급-재배치.md @@ -0,0 +1,19 @@ +# 재생바 요소 재배치 — 드론궤적+위치정보 인접, 시설등급 우측 + +**소요 시간**: 약 3분 +**Context 사용량**: input ~260k / output ~17k tokens + +## 결론 +하단 재생바에서 드론 위치정보(GPS·고도) HUD 를 드론궤적 버튼 바로 우측에 붙이고, 시설등급 옵션 패널을 +그 오른편(맨 뒤)으로 옮겨 순서를 정리. + +## 변경 내용 +- `client/src/components/player/VideoPlayer.tsx` + - 드론 위치정보 HUD(`{stationDiag && ...}`)를 시설등급 패널 뒤 → **드론궤적 버튼 직후**로 이동. + - 시설등급 패널이 HUD 뒤(우측)에 오도록 순서 변경. + +## 바 버튼/요소 순서 (영상제어 ON + geoLoaded) +좌측 패널 → 선형 → 드론궤적 → 드론 위치정보(GPS·고도) → 시설등급 + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드(11:19). diff --git a/docs/history/2026-06-29_1123_활성색-amber-통일.md b/docs/history/2026-06-29_1123_활성색-amber-통일.md new file mode 100644 index 0000000..7e29b67 --- /dev/null +++ b/docs/history/2026-06-29_1123_활성색-amber-통일.md @@ -0,0 +1,25 @@ +# 활성화 상태 색상 amber(겨자/오렌지)로 통일 + +**소요 시간**: 약 4분 +**Context 사용량**: input ~280k / output ~18k tokens + +## 결론 +재생바·우측 토글의 활성화(ON/선택) 표시 색을 제각각(파랑/초록/빨강/하늘/회색)에서 **amber-400 + 검정 글씨**로 +통일. 사용자 요청(노란/겨자/오렌지 계열). + +## 변경 내용 +- `client/src/components/player/VideoPlayer.tsx` + - 영상제어 ON: blue → `bg-amber-400 border-amber-300 text-black` + - 배속 선택: `bg-blue-600 text-white` → `bg-amber-400 text-black` + - 좌측 패널 / 선형 / 드론궤적 ON: emerald/red/sky → amber-400 + - 시설등급 체크박스: `accent-blue-500` → `accent-amber-400` +- `client/src/components/overlay/StationOverlay.tsx` + - "화면표시 옵션"(showDisplay) 활성: sky-700 → amber-400 + - "카메라 파라미터"(showControls) 활성: gray-700 → amber-400 + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드(11:23). + +## 참고 +- 비활성(OFF)은 기존 `bg-black/70 border-gray-600 text-gray-300` 유지. +- 더 진하게 원하면 amber-500 으로 교체. diff --git a/docs/history/2026-06-29_1146_보고서-md-html-pdf-생성.md b/docs/history/2026-06-29_1146_보고서-md-html-pdf-생성.md new file mode 100644 index 0000000..6750032 --- /dev/null +++ b/docs/history/2026-06-29_1146_보고서-md-html-pdf-생성.md @@ -0,0 +1,27 @@ +# 작업 보고서 md/html/pdf 3종 생성 + 변환 스크립트 + +**소요 시간**: 약 12분 +**Context 사용량**: input ~310k / output ~21k tokens + +## 결론 +오늘 작업 내용을 팀장 보고용 문서로 작성하고, **md(베이스) → html + pdf** 3종 산출물을 생성. +재생성용 변환 스크립트와 스타일을 저장소에 추가. + +## 산출물 +- `docs/보고서_2026-06-29_스테이션바_UI개선.md` (베이스) +- `docs/보고서_2026-06-29_스테이션바_UI개선.html` (자체완결, CSS 인라인) +- `docs/보고서_2026-06-29_스테이션바_UI개선.pdf` (A4 3p, 한글 임베드, 페이지번호) + +## 추가 파일 +- `scripts/md2docs.sh` — `md2docs.sh <파일.md>` 로 같은 경로에 html+pdf 생성. +- `docs/assets/report.css` — 보고서 스타일(앰버 헤더/표/@page). + +## 변환 환경 (메모: report-doc-toolchain) +- pandoc 2.11 `--self-contained` 로 md→html (CSS 인라인 임베드). +- weasyprint 61.2 로 html→pdf. **pydyf 는 0.10 으로 핀**(0.11 은 API 변경으로 TypeError). +- 한글 폰트: 환경에 CJK 폰트 부재 → Windows 맑은고딕(`/mnt/c/Windows/Fonts/malgun*.ttf`)을 + `~/.local/share/fonts` 로 복사 후 `fc-cache`. WSL 이라 가능. + +## 검증 +- PDF 헤더 %PDF-1.7, 3페이지, `/FontFile`(Malgun 글리프 임베드) 확인 → 모든 뷰어에서 한글 정상. +- scripts/md2docs.sh 재실행으로 동일 산출물 재현 확인. diff --git a/docs/history/2026-06-29_1212_측점검색-순환점프+입력유지.md b/docs/history/2026-06-29_1212_측점검색-순환점프+입력유지.md new file mode 100644 index 0000000..ae7018f --- /dev/null +++ b/docs/history/2026-06-29_1212_측점검색-순환점프+입력유지.md @@ -0,0 +1,27 @@ +# 측점 검색 순환 점프 + 입력값 유지/블러삭제 + +**소요 시간**: 약 12분 +**Context 사용량**: input ~360k / output ~24k tokens + +## 결론 +측점값이 여러 곳(여러 통과)에 존재할 때, 측점 검색을 **통과방향 순환(find-next)** 으로 개선. +Enter 반복으로 다음 통과로 이동, 끝에서 처음으로 순환. 입력값은 Enter 후 유지하고 blur 시 삭제. + +## 변경 내용 +- `client/src/stationbar/StationBar.tsx` — `handleJumpToMileage` 재작성 + - 입력 측점의 '모든 통과'를 허용오차(stationTolerance, 기본 20m) 내 연속구간=1통과로 묶어 시간 오름차순 수집. + - 기준 시각(baseT)에서 통과방향(시간 증가) 다음 통과로 이동, 없으면 passes[0] 으로 순환. + - `jumpRef`(km,time)로 연속 검색 판정: 같은 측점이고 커서가 직전 점프 위치 그대로(|Δ|<0.1s)일 때만 + 연속(다음 통과). 재생/클릭으로 커서 이동했거나 다른 측점이면 현재 커서(라이브)에서 새로 시작. + - 라이브 시각은 `timeRef?.current ?? currentTime`. 커버 범위 밖(±20m) 입력은 무시(기존 유지). + - 허용오차 내 통과가 없으면 전역 최근접 1개로 폴백. +- `client/src/stationbar/components/PlaybackControls/PlaybackControls.tsx` + - Enter 시 `setQuery('')` 제거 → 입력값 유지(같은 값 재Enter 로 순환). + - input `onBlur` → 입력값 삭제(다른 곳 클릭 시 언셀렉트). + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드(12:12). + +## 참고 +- 순환 순서는 '시간(이동거리축 진행) 오름차순' = 화면상 좌→우. +- 향후 입력창 옆 'n/m' 위치 표시·매칭 하이라이트를 추가하면 인지성 더 향상. diff --git a/docs/history/2026-06-29_1219_측점검색-마커기준-전환.md b/docs/history/2026-06-29_1219_측점검색-마커기준-전환.md new file mode 100644 index 0000000..d514f0b --- /dev/null +++ b/docs/history/2026-06-29_1219_측점검색-마커기준-전환.md @@ -0,0 +1,27 @@ +# 측점 검색을 '화면 마커' 기준으로 전환 (누락 통과까지 검색) + +**소요 시간**: 약 7분 +**Context 사용량**: input ~390k / output ~26k tokens + +## 결론 +측점 검색 점프가 일부 통과로 가지 못하던 문제 해결. 검색 후보를 chain 기준 → **화면에 보이는 +마커(structureMarks) 기준**으로 바꿔, 바에 표시된 모든 동그라미(좌표근접 탐지 통과 포함)를 순환하도록 함. + +## 원인 +- 마커(structureMarks)는 측점기준 + 좌표근접(pxPassesTo) 합집합으로 통과를 찍음(조차장 등 chain 어긋난 통과 포함). +- 그러나 검색(handleJumpToMileage)은 `|arr[i].chain - km| < TOL` 측점기준으로만 통과를 찾음 + → chain 이 162080 에서 벗어난 통과(마커는 있음)는 검색이 못 감. + +## 변경 내용 (`client/src/stationbar/StationBar.tsx`) +- handleJumpToMileage 후보 산출 1순위: + `structureMarks.filter(표시 10m 반올림값 == 입력값).map(px → timeAtFrac 로 시각 역변환)`. +- 2순위(마커 없는 임의 측점): 기존 chain 연속구간 탐지 → 없으면 전역 최근접 폴백. +- 시간 정렬 + 0.3s 근접 중복 제거 후, 통과방향 순환(기존 jumpRef 로직 유지). +- deps 에 structureMarks, timeAtFrac, timeTrackWidth 추가. + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드(12:19). + +## 참고 +- 매칭은 표시 라벨과 동일하게 10m 반올림 일치 → 사용자가 보는 값과 검색 일치. +- 종점 미도착으로 px=TRACK_END 로 이동된 마커는 역변환 시 끝 시각이 됨(허용). diff --git a/docs/history/2026-06-29_1228_좌표반경-마커-측점값숨김.md b/docs/history/2026-06-29_1228_좌표반경-마커-측점값숨김.md new file mode 100644 index 0000000..0eaa87e --- /dev/null +++ b/docs/history/2026-06-29_1228_좌표반경-마커-측점값숨김.md @@ -0,0 +1,30 @@ +# 좌표 반경으로만 잡힌 마커의 측점값 라벨/검색 제외 + +**소요 시간**: 약 8분 +**Context 사용량**: input ~420k / output ~28k tokens + +## 결론 +역 마커가 좌표 반경(pxPassesTo)으로만 잡혀 그 위치에 실제 측점값이 존재하지 않는 경우, +**측점값 라벨을 숨기고 검색 대상에서도 제외**. 마커(동그라미)는 유지. + +## 판정 (kmExists) +- 각 통과의 실제 측점 = 그 통과 프레임의 연속 chain(p.km, 좌표탐지) / 측점기준 탐지는 targetM(=stationKmVal). +- 표시 측점값 = stationKmVal(역 좌표 투영). +- `kmExists = stationKmVal==null || |p.km - stationKmVal| <= stationTolerance(기본 20m)` + - 측점기준 통과 → 일치 → true + - 좌표 반경만으로 잡힌 통과(측점 동떨어짐) → false + +## 변경 내용 +- `client/src/stationbar/StationBar.tsx` + - `StructMark`에 `kmExists?: boolean` 추가. + - structureMarks 두 경로(storeStructures / POI 폴백) push 시 kmExists 계산. + - handleJumpToMileage 마커 후보 필터에 `m.kmExists !== false` 추가(값 없는 마커로는 검색 점프 안 함). +- `client/src/stationbar/components/Timeline/Timeline.tsx` + - 측점값 라벨 표시 루프에서 `s.kmExists === false`면 라벨 생략(동그라미·이름 라벨은 유지). + +## 검증 +- `tsc + vite build` 통과(node 20). client/dist 재빌드(12:27). + +## 참고 +- 측점기준 통과는 km=targetM=stationKmVal 로 동일 → 항상 kmExists=true(정상 측점은 영향 없음). +- 마커 동그라미/브래킷/역명 라벨은 그대로. 숨기는 것은 '측점값(10m)' 라벨과 그 값으로의 검색뿐. diff --git a/docs/history/2026-06-29_1338_55000-외부접속-포트포워딩.md b/docs/history/2026-06-29_1338_55000-외부접속-포트포워딩.md new file mode 100644 index 0000000..eb3b772 --- /dev/null +++ b/docs/history/2026-06-29_1338_55000-외부접속-포트포워딩.md @@ -0,0 +1,28 @@ +# 55000 포트 외부(LAN) 접속 활성화 + +**소요 시간**: 약 10분 +**Context 사용량**: input ~450k / output ~30k tokens + +## 결론 +PM2 ghiVideo(55000) 를 같은 네트워크의 다른 기기에서 접속 가능하게 설정. +접속 주소: **http://172.16.40.179:55000/** (Windows 이더넷 LAN IP). + +## 점검 결과 +- 서버: `0.0.0.0:55000` 바인딩 ✓, CORS `origin:true`(모든 origin 허용) ✓ → 서버측 준비 완료. +- 차단 원인: WSL2 기본 NAT 모드 → WSL IP(172.21.50.250)는 LAN 직접 도달 불가. + +## 조치 (Windows 관리자/UAC) +- netsh portproxy 추가: `0.0.0.0:55000 → 172.21.50.250:55000` + (기존에 8080/3000/5173 등 동일 패턴 사용 중이라 55000만 추가) +- 방화벽 인바운드 허용 규칙 "WSL ghiVideo 55000" (TCP 55000) 추가. +- 스크립트: `C:\Windows\Temp\wsl_port_55000.cmd` (UAC 로 실행). + +## 검증 +- portproxy 목록에 55000 등록 확인. +- 방화벽 규칙 Enabled/Inbound/Allow 확인. +- Windows→`http://localhost:55000/` Invoke-WebRequest → **200**. + +## 주의 / 후속 +- WSL IP 는 재부팅/`wsl --shutdown` 시 변경 → portproxy(고정 172.21.50.250) 끊김. 재적용 필요. + (요청 시 WSL IP 자동감지 재설정 스크립트 작성 가능) +- 172.16.x 사내망 한정. 인터넷 접속은 공유기/회사 방화벽 포트포워딩 추가 필요. diff --git a/docs/history/2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md b/docs/history/2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md new file mode 100644 index 0000000..215817f --- /dev/null +++ b/docs/history/2026-06-29_1436_드론자세평활-오버레이렌더링-기술문서화.md @@ -0,0 +1,302 @@ +# 드론 자세 평활/보간 + 영상 오버레이 렌더링 파이프라인 + +> 후임자 인계 + 특허 문서용 상세 기술 문서 +> 대상 소스: [client/src/components/overlay/StationOverlay.tsx](../../client/src/components/overlay/StationOverlay.tsx) (1639줄), 투영 유틸 [client/src/utils/geoProjection.ts](../../client/src/utils/geoProjection.ts) + +--- + +## 1. 기술 개요 / 목적 + +드론으로 촬영한 철도 선로 주행영상 위에, 측점(스테이션)·선로구조물(교량/터널/구교)·POI·선로 중심선·드론 GPS 궤적·나침반을 **영상 픽셀에 정확히 정합(AR 오버레이)** 시켜 표출하는 렌더링 파이프라인이다. + +입력 데이터는 두 종류로, (a) 프레임별 드론 자세/위치 로그(SRT 추출: `frame, lat, lon, altitude, yaw, pitch, roll`)와 (b) 지리정보(측점·중심선·POI·구조물의 위경도+표고)이다. 핵심 변환은 핀홀 카메라 모델 기반의 월드(EPSG:5186 TM ENU) → 카메라 → 정규화 픽셀 투영([geoProjection.ts#L301](../../client/src/utils/geoProjection.ts#L301), [#L126](../../client/src/utils/geoProjection.ts#L126))이다. + +이 문서가 다루는 본질적 과제는 **두 가지 노이즈/지연 문제**이다. + +1. **자세/GPS 노이즈** — 원시 SRT 로그는 떨림이 있어 그대로 투영하면 라벨이 매 프레임 진동한다. 그러나 단순 평균은 회전 구간을 뭉개 라벨이 늦게 따라온다(지연). → **에지 보존 적응형 평활(smoothFrame)** 로 해결. +2. **렌더 부드러움 vs 계산 비용** — 라벨 가시성/겹침 판정은 비싸고(전 프레임 × 전 항목), 60fps 매끄러운 이동은 매 프레임 재투영을 요구한다. → **가시성은 사전계산(precompute), 좌표는 RAF에서 연속포즈로 재투영**하는 2단 분리로 해결. + +--- + +## 2. 핵심 알고리즘 + +### 2.1 에지(회전경계) 보존 적응형 이동평균 — `smoothFrame` + +[StationOverlay.tsx#L577](../../client/src/components/overlay/StationOverlay.tsx#L577) + +원시 드론 프레임 배열에서 인덱스 `i`를 중심으로 좌우 최대 `halfWin`(기본 60프레임 ≈ 2초)까지 이동평균을 취하되, **중심 프레임의 yaw와 차이가 임계 `YAW_EDGE = 8°` 이내인 인접 프레임까지만 창을 확장**한다. + +```typescript +// StationOverlay.tsx:577 +const smoothFrame = useCallback((frames: DroneFrameBasic[], i: number, halfWin: number): DroneFrameBasic => { + let lo = i, hi = i; + if (frames.length > 1 && halfWin > 0) { + const YAW_EDGE = 8; // deg — 중심 대비 이 이상 벌어지면 다른(회전) 구간으로 보고 평균 제외 + const yc = frames[i].yaw; + const near = (idx: number) => Math.abs(((frames[idx].yaw - yc + 540) % 360) - 180) <= YAW_EDGE; + const loMin = Math.max(0, i - halfWin); + const hiMax = Math.min(frames.length - 1, i + halfWin); + while (lo > loMin && near(lo - 1)) lo--; // 회전경계에서 멈춤 + while (hi < hiMax && near(hi + 1)) hi++; + } +``` + +**수식.** 창 `[lo, hi]`(요소 수 `n = hi − lo + 1`)에 대해 lat/lon/altitude/pitch/roll은 산술평균. yaw는 각도이므로 단순 산술평균 시 0°/360° 경계에서 깨진다 → **원형평균(circular mean)**: + +``` +yaw̄ = atan2( (1/n)·Σ sin(yawₖ), (1/n)·Σ cos(yawₖ) ) +``` + +[StationOverlay.tsx#L590](../../client/src/components/overlay/StationOverlay.tsx#L590): + +```typescript +const yr = f.yaw * Math.PI / 180; +sinYaw += Math.sin(yr); cosYaw += Math.cos(yr); +// ... +yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI, +``` + +`near()`의 `((Δ + 540) % 360) − 180`은 yaw 차를 항상 `[−180°, 180°]`로 정규화하는 최단각 차이 패턴이다. + +**의도 / 트레이드오프.** +- 직선 주행: yaw가 거의 일정 → 창이 풀폭(±60fr)으로 펴져 GPS/자세 노이즈를 강하게 억제. +- 회전 중: 회전경계에서 창이 좁아져(한쪽 또는 양쪽이 중심 근처에서 멈춤) **실제 자세를 즉시 추종(지연 없음)**. +- 회전 종료 후: 회전 프레임을 배제한 채 창이 **한 프레임씩** 점진적으로 재확장 → 튐 없음. +- 트레이드오프: `YAW_EDGE`가 너무 작으면 완만한 곡선도 회전으로 보아 노이즈 억제가 약해지고, 너무 크면 회전이 평균에 섞여 지연. 8°는 떨림(보통 <2°)과 실제 회전을 가르는 경험값. +- (주석에 기록된) 과거 '변화율 기반 창 축소' 방식은 회전 종료 시 창이 한꺼번에 재확장되며 회전 프레임이 다시 섞여 한 번 튀는 부작용이 있어 경계보존 방식으로 교체됨 ([StationOverlay.tsx#L569](../../client/src/components/overlay/StationOverlay.tsx#L569) 주석). + +### 2.2 연속 프레임번호 보간 포즈 — `poseAt` + +[StationOverlay.tsx#L670](../../client/src/components/overlay/StationOverlay.tsx#L670) + +영상의 부드러운 재생시간(60fps 벽시계 보간, §4.1)에서 산출된 **실수(實數) 프레임번호 `estFrame`** 에 대응하는 자세를 만든다. 이산 SRT 프레임만으로는 30fps 격자에 갇혀 라벨이 계단식으로 움직이므로, 양 이웃 프레임의 **평활 포즈(smoothFrame 결과)** 를 선형보간한다. + +```typescript +// StationOverlay.tsx:670 — 이진탐색으로 estFrame을 끼우는 두 프레임 찾기 +let lo = 0, hi = arr.length - 1; +while (lo < hi) { const m = (lo + hi) >> 1; if (arr[m].frame < estFrame) lo = m + 1; else hi = m; } +const i2 = lo, i1 = Math.max(0, lo - 1); +const f1 = arr[i1].frame, f2 = arr[i2].frame; +const frac = f2 > f1 ? Math.min(1, Math.max(0, (estFrame - f1) / (f2 - f1))) : 0; +const a = smoothFrame(arr, i1, h), b = smoothFrame(arr, i2, h); +const L = (x: number, y: number) => x + (y - x) * frac; +let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180; // 최단각 +return { ...a, frame: estFrame, + lat: L(a.lat, b.lat), lon: L(a.lon, b.lon), altitude: L(a.altitude, b.altitude), + pitch: L(a.pitch, b.pitch), roll: L(a.roll, b.roll), yaw: a.yaw + dy * frac }; +``` + +**수식.** 보간계수 `frac = (estFrame − f1)/(f2 − f1)`. 위치/피치/롤은 `L(x,y) = x + (y−x)·frac`. yaw는 359°→1° 같은 경계에서 긴 쪽으로 도는 것을 막기 위해 **최단각 보간**: `Δ = ((b.yaw − a.yaw + 540) % 360) − 180`, `yaw = a.yaw + Δ·frac`. + +**트레이드오프.** O(log n) 이진탐색으로 호출당 비용이 낮아 매 RAF 프레임 호출 가능. smoothFrame을 매 프레임 두 번 호출하므로(`a`, `b`) 평활창이 매우 클 때는 비용이 늘 수 있으나, 실데이터 규모에서는 무시 가능. + +### 2.3 속도적응 EMA + 이상치 거부 — `smoothStep` (One-Euro 방식) + +[StationOverlay.tsx#L48](../../client/src/components/overlay/StationOverlay.tsx#L48) + +`poseAt`로 재투영한 화면 정규좌표를 매 프레임 그대로 쓰면 잔여 떨림이 보인다. 화면단에서 One-Euro 필터 사상(idea)을 적용: **느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종**. + +```typescript +// StationOverlay.tsx:48 +function smoothStep(prev, tx, ty, maxAlpha, minAlpha, speedRef): DispPos { + if (!prev) return { x: tx, y: ty, rej: 0, vx: 0, vy: 0 }; + const dx = tx - prev.x, dy = ty - prev.y; + const d = Math.hypot(dx, dy); + if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES) { + return { x: prev.x, y: prev.y, rej: prev.rej + 1, vx: prev.vx, vy: prev.vy }; // 이상치: 위치 유지 + } + // 속도 벡터 평활 → 떨림(왕복)은 상쇄돼 작게, 실제 이동은 유지. + const vx = prev.vx + (dx - prev.vx) * SMOOTH_VEL_BETA; + const vy = prev.vy + (dy - prev.vy) * SMOOTH_VEL_BETA; + const speed = Math.hypot(vx, vy); + const a = Math.min(maxAlpha, minAlpha + (maxAlpha - minAlpha) * Math.min(1, speed / Math.max(1e-4, speedRef))); + return { x: prev.x + dx * a, y: prev.y + dy * a, rej: 0, vx, vy }; +} +``` + +**수식.** +- 이상치 거부: 프레임당 점프 `d = ‖(dx,dy)‖`가 `REJECT_DIST = 0.12`(정규화 화면거리)를 넘으면 갱신을 버리고 이전 위치 유지. 단 연속 거부가 `MAX_REJECT_FRAMES = 8`을 넘으면(=시크/재등장 같은 진짜 큰 이동) 수용 → 영구 고착 방지. +- 속도 평활: `v ← v + (Δ − v)·β`, `β = SMOOTH_VEL_BETA = 0.25`. 떨림은 매 프레임 방향이 번갈아(왕복) `v`가 상쇄돼 ≈0, 실제 이동은 방향이 일관돼 `v` 유지(저크 제거). +- 적응 alpha: `α = min(maxAlpha, minAlpha + (maxAlpha − minAlpha)·min(1, speed/speedRef))`. + - 정지/떨림(`speed≈0`) → `α≈minAlpha`(기본 0.12, 강한 평활). + - 빠른 팬(`speed ≥ speedRef`, 기본 `speedRef=0.010`) → `α=maxAlpha`(기본 `emaAlpha=1.0`, 즉시 추종). +- 위치 갱신: `pos ← pos + Δ·α`. + +**의도 / 트레이드오프.** 입력이 이미 smoothFrame(±60fr) + poseAt 보간으로 한 차례 평활됐으므로 화면 EMA의 `maxAlpha`는 가볍게 둔다(주석: 과거 고정 α=0.01은 지연 ~1.65s로 라벨이 늦게 따라옴). `minAlpha`↓ / `speedRef`↑ 일수록 1배속 떨림은 줄지만 미세 추종 지연이 늘어난다. 이 세 값은 패널에서 데이터셋별로 실시간 튜닝·저장된다([StationOverlay.tsx#L494](../../client/src/components/overlay/StationOverlay.tsx#L494)). + +추가로 **픽셀 히스테리시스**가 EMA 위에 한 겹 더 적용된다: 화면 px 위치가 0.75px 이상 움직일 때만 정수 좌표를 갱신해 저속에서 반올림 깜빡임을 제거한다([StationOverlay.tsx#L1048](../../client/src/components/overlay/StationOverlay.tsx#L1048), [#L1084](../../client/src/components/overlay/StationOverlay.tsx#L1084)). + +--- + +## 3. 라벨 사전계산 + 겹침 억제 + +### 3.1 `startLabelPrecompute` — requestIdleCallback 청크 처리, 월드좌표 저장 + +[StationOverlay.tsx#L692](../../client/src/components/overlay/StationOverlay.tsx#L692) + +데이터 로드 완료 시(또는 params/평활/범위 변경 시 500ms debounce, [#L824](../../client/src/components/overlay/StationOverlay.tsx#L824)) 전 프레임에 대해 **어떤 라벨이 보이는가(가시집합)와 겹침 해소 결과**를 미리 계산해 `Map`에 적재한다. 핵심 설계는 **화면좌표가 아니라 월드좌표(lat/lon/z)를 저장**한다는 점이다([LabelCache 정의 #L123](../../client/src/components/overlay/StationOverlay.tsx#L123)). 가시성/겹침은 한 번만 결정하고, 실제 화면 위치는 RAF가 매 프레임 연속포즈로 재투영한다. + +**청크 처리.** 메인스레드 블로킹을 피하려 `requestIdleCallback`으로 한 번에 `CHUNK = 200`프레임씩 처리한다. + +```typescript +// StationOverlay.tsx:717 +const step = () => { + if (precomputeIdRef.current !== id) return; // 취소됨 (새 계산이 시작되면 id 불일치 → 중단) + const end = Math.min(processed + CHUNK, total); + while (processed < end) { + const idx = (startIdx + processed) % total; // startIdx→끝→0→… 순환 + processed++; + const drone = smoothFrame(frames, idx, SMOOTH_HALF); + // ... 측점/POI 가시판정 + 겹침억제 → newMap.set(drone.frame, {...}) + } + if (!published) { labelMapRef.current = newMap; published = true; } // 첫 청크 후 즉시 노출 + if (processed < total) requestIdleCallback(step, { timeout: 200 }); +}; +step(); // 첫 청크는 동기 실행 → 정지 중에도 토글/재계산 즉시 반영 +``` + +세 가지 영리한 설계가 있다. +- **현재 프레임부터 순환 처리** (`startIdx = currentFrameIdxRef.current`, `idx = (startIdx + processed) % total`): 지금 화면에 보이는 구간을 먼저 계산해 재생 즉시 라벨이 뜬다. +- **재계산 시 옛 맵 복사로 시작** ([#L696](../../client/src/components/overlay/StationOverlay.tsx#L696)): 빈 구간 없이 새 청크가 덮어쓰며 갱신. +- **취소 토큰** `precomputeIdRef`: 새 계산이 시작되면 `id`가 증가해 진행 중이던 step이 자동 중단(중복 작업 방지). + +가시판정은 카메라 클리핑(`Zc < CLIP_Z = 0.1` 제외 = 카메라 뒤/너무 가까움), 화면 밖(`pxRaw/pyRaw`가 [−0.02, 1.02] 밖) 제외, POI는 수평직선거리 `distH > MAX_RANGE` 제외로 이뤄진다([#L731](../../client/src/components/overlay/StationOverlay.tsx#L731), [#L748](../../client/src/components/overlay/StationOverlay.tsx#L748)). + +### 3.2 겹침 억제 / 우선순위 / 진행방향 변형 + +[StationOverlay.tsx#L755](../../client/src/components/overlay/StationOverlay.tsx#L755) + +화면상 가까이 겹치는 라벨을 솎아낸다. 임계: 가로 `POI_MERGE_X = 0.10`, 세로 `POI_MERGE_Y = 0.035`(정규화). 비등방(가로 관대, 세로 엄격)은 텍스트 라벨이 가로로 길기 때문. + +**우선순위 정렬**(겹침 시 누가 남는가): ① 철도역/역사(`railRank=0`)를 최우선, ② 같은 등급 내에서는 드론에 가까운 것(`dist` 작은 것)을 우선. + +```typescript +// StationOverlay.tsx:757 +const railRank = (cat: string): number => (cat === '철도역' || cat === '역사' ? 0 : 1); +poiCand.sort((a, b) => railRank(a.category) - railRank(b.category) || a.dist - b.dist); +const dirTag = routeDirRef.current === '상' ? '(상)' : routeDirRef.current === '하' ? '(하)' : ''; +``` + +**진행방향 변형 우선**: 같은 구조물이 상/하행 형제로 두 개 있을 때(예: `회덕제1가도교(상)` / `회덕제1가도교(하)`), 드론이 실제 지나는 선로 방향(routeMeta의 direction, [#L447](../../client/src/components/overlay/StationOverlay.tsx#L447))의 변형을 남긴다. `baseStruct()`로 괄호 변형을 떼 형제임을 판정하고, 이미 수용된 라벨이 비-방향태그인데 새 후보가 방향태그면 교체한다([#L777](../../client/src/components/overlay/StationOverlay.tsx#L777)): + +```typescript +const k = accepted[overlapIdx]; +if (dirTag && baseStruct(c.title) === baseStruct(k.title) && c.title.includes(dirTag) && !k.title.includes(dirTag)) { + accepted[overlapIdx] = c; +} +``` + +겹침제외 토글 OFF면 억제 없이 전부 표시하며, 동일좌표 라벨은 `labelRow / labelRowCount`로 세로 행 배치된다([#L784](../../client/src/components/overlay/StationOverlay.tsx#L784))(렌더 시 `ROW_H = 24px`로 중앙정렬, [#L1101](../../client/src/components/overlay/StationOverlay.tsx#L1101)). + +--- + +## 4. RAF 렌더 루프 (계산 없이 조회 + 투영만) + +[StationOverlay.tsx#L906](../../client/src/components/overlay/StationOverlay.tsx#L906) + +매 프레임 `requestAnimationFrame`에서 캔버스를 다시 그린다. **무거운 계산(가시성/겹침)은 하지 않고**, precompute가 채운 가시집합을 조회 + 연속포즈로 재투영 + 그리기만 한다. + +### 4.1 연속 시간 → 보간 포즈 + +```typescript +// StationOverlay.tsx:941 +const estTime = timeRef + ? timeRef.current + : currentTimeSecRef.current + (performance.now() - timeUpdateWallRef.current) / 1000; +const estFrame = estTime * VIDEO_FPS; // VIDEO_FPS = 30000/1001 ≈ 29.97 +const dronePose = poseAt(estFrame) ?? (...폴백 smoothFrame...); +const lines = buildLines(dronePose); +``` + +`timeRef`는 VideoPlayer의 `smoothTimeRef`로, 매 RAF마다 단조(monotonic) 벽시계 보간으로 갱신되는 재생시간이다([VideoPlayer.tsx#L84](../../client/src/components/player/VideoPlayer.tsx#L84)). 일시정지·시크·배속을 모두 보정하므로 라벨/선/궤적이 영상과 동기된 채 60fps로 매끄럽게 흐른다. + +### 4.2 object-fit:cover 정규좌표 → 화면 변환 (`coverRef`) + +영상은 컨테이너를 비율유지·크롭하며 채우므로(CSS `object-fit:cover`), 오버레이도 같은 cover 사각형에 매핑해야 정합된다. + +```typescript +// StationOverlay.tsx:923 +const { w: vW, h: vH } = videoSizeRef.current; +let dispW = W, dispH = H, offX = 0, offY = 0; +if (vW > 0 && vH > 0) { + const s = Math.max(W / vW, H / vH); // cover: 더 큰 배율(넘침=크롭) + dispW = vW * s; dispH = vH * s; + offX = (W - dispW) / 2; offY = (H - dispH) / 2; // 음수 = 화면 밖(크롭) +} +coverRef.current = { offX, offY, dispW, dispH, W, H }; +const vx = (nx: number) => offX + nx * dispW; // 정규(0~1, 영상프레임) → 화면 px +const vy = (ny: number) => offY + ny * dispH; +``` + +`coverRef`는 RAF가 매 프레임 갱신하고, 포인터 핸들러(클릭→정규좌표 역변환 `clientToVideoNorm`, [#L1211](../../client/src/components/overlay/StationOverlay.tsx#L1211))와 자동팝업 동기화도 공유한다. + +### 4.3 중심선 / 드론궤적 투영 (`buildLines`) + +[StationOverlay.tsx#L606](../../client/src/components/overlay/StationOverlay.tsx#L606). 보간포즈를 받아 매 프레임 직접 투영한다(캐시 미사용 → 라인이 라벨처럼 부드럽게 이동). +- **근거리 클리핑**: 세그먼트 양 끝의 `Zc`가 `CLIP_Z=0.1` 경계를 가로지르면 교점 `t = (CLIP_Z − z1)/(z2 − z1)`로 잘라 투영([#L622](../../client/src/components/overlay/StationOverlay.tsx#L622)) → 카메라 뒤 점이 화면 반대편으로 튀는 것 방지. +- **화면밖 컬링**: 비트마스크 `oc()` outcode로 양 끝이 같은 변 밖이면 세그먼트 폐기([#L612](../../client/src/components/overlay/StationOverlay.tsx#L612)). +- 드론궤적: GPS 경로(lat/lon)를 표고 `Z`에 투영, `PW=15` 폭 이동평균으로 평활, `STEP=3` 샘플, `MAX_FWD=200m` 앞까지만([#L646](../../client/src/components/overlay/StationOverlay.tsx#L646)). + +라벨 자체도 캐시의 월드좌표를 `dronePose`로 재투영 → `smoothStep` → 히스테리시스 후 그린다([#L1039](../../client/src/components/overlay/StationOverlay.tsx#L1039) 측점, [#L1071](../../client/src/components/overlay/StationOverlay.tsx#L1071) POI). + +### 4.4 나침반 미니맵 (heading-up) + +[StationOverlay.tsx#L954](../../client/src/components/overlay/StationOverlay.tsx#L954). 본 포즈는 ±60fr 평활이라 회전 시 나침반이 지연된다 → **별도로 ±3fr 가벼운 평활 yaw**를 보간해 회전에 즉시 반응. `--rot` CSS 변수로 DOM 회전을 갱신하며, **360° 누적 언랩**으로 359°→0° 점프를 방지한다: + +```typescript +const target = -(rawYaw + paramsRef.current.yawOffset); +const delta = (((target - minimapRotRef.current) % 360) + 540) % 360 - 180; +minimapRotRef.current += delta; +minimapRef.current.style.setProperty('--rot', `${minimapRotRef.current}deg`); +``` + +### 4.5 자동 컴팩트 팝업 동기화 (setInterval 150ms) + +[StationOverlay.tsx#L376](../../client/src/components/overlay/StationOverlay.tsx#L376). 가시 구조물(`visStructRef`, RAF가 매 프레임 채움)마다 라벨 옆에 선명한 DOM 팝업(시설종별/구조형식/연장 등 컴팩트 3필드)을 자동 생성/제거한다. **60fps RAF에서 setState를 남발하면 리렌더 폭주**하므로, 가시집합 변화 감지를 **150ms 인터벌**로 분리해 추가/제거가 있을 때만 `setInfoPopups`를 호출한다. 팝업의 실제 화면 위치는 다시 RAF가 라벨 표시좌표를 따라 매 프레임 갱신(EMA `PA=0.18`, 화면하단 근접 시 위로 플립, [#L1146](../../client/src/components/overlay/StationOverlay.tsx#L1146)). + +--- + +## 5. 성능 설계 — 60fps 유지 전략 + +| 작업 | 비용 | 배치 | 빈도 | +|------|------|------|------| +| 가시성/겹침 판정 (전 프레임 × 전 항목) | 높음 | precompute | 1회(+설정변경 시 debounce 재계산), idle 청크 200fr | +| 좌표 투영 (보간포즈 → 화면) | 중간 | RAF | 매 프레임 | +| 평활/EMA/히스테리시스 | 낮음 | RAF | 매 프레임 | +| 팝업 가시집합 setState | 리렌더 유발 | setInterval 150ms | 변화 시에만 | + +**핵심 = precompute(가시성)와 RAF(좌표) 분리.** "어떤 라벨을 그릴지"(비싼 결정)는 영상 변화와 무관하게 한 번 계산해 두고, "어디에 그릴지"(가벼운 투영)만 매 프레임 한다. 이로써 RAF 루프는 Map 조회 + 행렬 투영 + 캔버스 드로잉만 남아 60fps를 유지한다([파일 상단 주석 #L1](../../client/src/components/overlay/StationOverlay.tsx#L1)). + +부가 전략: 사전계산의 idle 청크 처리(메인스레드 비블로킹) + 취소토큰, 현재 프레임 우선 순환계산, 첫 청크 즉시노출, 픽셀 히스테리시스(불필요한 재드로 억제), 팝업 위치 EMA(물결/잔상 방지), 데이터셋별 보정값 localStorage 영속화([#L490](../../client/src/components/overlay/StationOverlay.tsx#L490)). + +--- + +## 6. 한계 / 업그레이드 포인트 + +- **단일 캐시 프레임키 의존 잔존**: 라벨 가시집합은 `Math.floor(estFrame)`로 정수 프레임의 캐시를 조회([#L1022](../../client/src/components/overlay/StationOverlay.tsx#L1022)). 좌표는 연속이지만 가시집합 자체는 프레임 경계에서 바뀐다 → 빠른 배속에서 라벨 등장/퇴장이 미세하게 튈 수 있음. +- **중심선 per-frame 투영 비용**: `buildLines`가 전 중심선 점을 매 프레임 투영. 점 수가 많아지면 부담 → 화면 근방 구간만 투영하는 공간색인(앞서 컬링 전 prefilter)으로 개선 여지. +- **POI 표고 부정확**: POI는 실측 표고가 없어 최근접 선로표고 또는 '드론고도 − N'으로 가정([#L744](../../client/src/components/overlay/StationOverlay.tsx#L744)) → 드래그/DEM override로 수동보정. DEM 자동조회 통합이 업그레이드 포인트. +- **VFR/프레임 누락**: `VIDEO_FPS` 상수(29.97) 가정. 가변프레임율 영상은 frame↔time 매핑 오차 발생. +- **평활 파라미터 수동 튜닝**: YAW_EDGE/REJECT_DIST/speedRef 등이 상수 또는 수동슬라이더 → 데이터셋 통계 기반 자동 캘리브레이션 가능. +- **단일 스레드**: precompute가 메인스레드 idle 의존. Web Worker로 옮기면 대용량에서 더 안정. + +--- + +## 7. 특허 관점 신규성 + +1. **에지(회전경계) 보존 적응형 이동평균** — yaw 차 임계로 평균창을 회전경계에서 멈추고, 회전 종료 후 한 프레임씩 점진 재확장하여 *직선구간 노이즈 억제*와 *회전구간 무지연 추종*을 동시에 달성. 회전 프레임 재혼입에 의한 1회 튐을 구조적으로 제거(변화율 기반 방식 대비). + +2. **월드좌표 사전계산 + 연속포즈 재투영의 2단 분리** — 라벨 가시성/겹침/우선순위(비싼 결정)는 월드좌표로 1회 사전계산하고, 화면 위치(가벼운 투영)는 매 RAF 연속 보간포즈로 재계산. "무엇을 그릴지"와 "어디에 그릴지"의 분리로 60fps AR 오버레이를 단일스레드에서 달성. + +3. **연속 프레임번호 보간 포즈 + 최단각 yaw** — 단조 벽시계 보간 재생시간을 실수 프레임번호로 변환, 이산 SRT 자세를 평활-후-보간하여 이산격자(30fps)를 넘는 60fps 연속 자세를 생성. yaw는 원형평균(평활)·최단각(보간)으로 360° 경계 안정. + +4. **속도적응 화면 평활(One-Euro 사상) + 연속거부 한계** — 속도벡터 평활로 떨림(왕복상쇄)과 실이동(방향일관)을 분리해 alpha를 적응시키고, 이상치 거부에 *연속거부 한계*를 두어 시크/재등장 시 영구고착을 방지. + +5. **진행방향 인지 형제 라벨 해소** — 상/하행 형제 구조물이 화면상 겹칠 때 노선 진행방향((상)/(하)) 변형을 우선 표시. 철도역 최우선 + 근접거리 차선의 다단 우선순위와 결합. + +6. **object-fit:cover 정합 + 양방향 좌표 변환** — 영상 크롭 사각형에 오버레이를 일치시키는 정규좌표 변환과 그 역변환(클릭→월드 역투영)으로, 영상 위 라벨 클릭/드래그 보정(focal 역산·표고 역산·지면교차)을 동일 좌표계에서 처리. + +--- + +**소요 시간**: 18분 +**Context 사용량**: input 88k / output 12k tokens diff --git a/docs/history/2026-06-29_1447_종합기술명세서-작성.md b/docs/history/2026-06-29_1447_종합기술명세서-작성.md new file mode 100644 index 0000000..41d2991 --- /dev/null +++ b/docs/history/2026-06-29_1447_종합기술명세서-작성.md @@ -0,0 +1,32 @@ +# GhiVideo 종합 기술 명세서 작성 (인계+특허 기초) + +**소요 시간**: 약 35분 +**Context 사용량**: input ~600k / output ~36k tokens + +## 결론 +사용된 전 기술을 소스 인용과 함께 1개 종합 문서로 정리. 후임 인계 + 특허 출원 기초자료 겸용. +md/html/pdf 3종 생성(PDF 18p, 한글 임베드). + +## 산출물 +- `docs/기술명세_GhiVideo_종합기술문서.md` (베이스) +- `docs/기술명세_GhiVideo_종합기술문서.html` (자체완결) +- `docs/기술명세_GhiVideo_종합기술문서.pdf` (A4 18p, Malgun 임베드, 페이지번호) + +## 방법 +- 5개 서브시스템을 병렬 서브에이전트로 정밀 조사 후 종합: + 1) 카메라 투영·FOV 정합(geoProjection.ts) 2) 드론 자세 평활·오버레이 렌더(StationOverlay.tsx) + 3) 측점·스테이션바(chainage.ts/StationBar/Timeline) 4) 백엔드·데이터·상태(server/geoData/store) + 5) 기존 특허/발명 문서 + history 타임라인 +- 투영 파이프라인은 정본 1개(§2)로 통합, 지오이드 N=25.8m 통일, 특허/인계 섹션 통합. + +## 문서 구성 (13장) +개요·아키텍처 / 좌표·투영 정본 / 카메라·FOV / 측점표고정합(발명B) / 자세평활·렌더 / +측점·스테이션바 / 위치보정 역투영(발명C) / 백엔드·데이터 / 개발타임라인 / 특허종합 / +검증결과표 / 후임자 인계 / 한계·출원전 확인. + +## 검증 +- scripts/md2docs.sh 로 html+pdf 생성, %PDF-1.7 18페이지 /FontFile(Malgun) 임베드 확인. + +## 참고 +- 특허 출원 시 §3~§7·§10 + history 원문(특히 2026-06-22 POI투영오차)을 변리사에 함께 제공 권장. +- KIPRIS 전문검색은 미수행 — 출원 전 필수. diff --git a/docs/기술명세_GhiVideo_종합기술문서.html b/docs/기술명세_GhiVideo_종합기술문서.html new file mode 100644 index 0000000..30c52a5 --- /dev/null +++ b/docs/기술명세_GhiVideo_종합기술문서.html @@ -0,0 +1,834 @@ + + + + + + + 기술명세_GhiVideo_종합기술문서 + + + + + +
+

기술명세_GhiVideo_종합기술문서

+
+

GhiVideo 종합 기술 명세서

+
+

목적: 본 문서는 (1) 특허 출원/발명 신고의 기초 자료와 (2) 후임 개발자 인계·업그레이드 자료를 겸한다. 대상 시스템: GhiVideo — 시간축이 아니라 노선 위치(측점·체이니지) 를 1차 좌표로 삼아 드론 주행영상을 색인·탐색하고, 프레임별 카메라 자세로 측점·POI·선로중심선을 영상 위에 실시간 정합(geo-registration)하는 철도 주행영상 분석 플레이어. 작성일: 2026-06-29 · 모든 소스 참조는 [경로](경로#Lxx) 형식(클릭 가능).

+
+
+

0. 문서 구성과 읽는 법

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
내용주 독자
1시스템 개요·아키텍처·데이터 흐름전체
2좌표·투영 정본 파이프라인 (모든 정합의 기반)전체
3카메라 투영 & 화각(FOV) 정합개발/특허
4측점 표고 정합 (DEM-free) — 발명 B특허
5드론 자세 평활/보간 + 오버레이 렌더링개발
6측점(체이니지) 시스템 & 스테이션바개발/특허
7위치보정 역투영 — 발명 C특허
8백엔드 스트리밍/FFmpeg·데이터·상태관리개발
9개발 타임라인(주요 의사결정)전체
10특허 관점 종합(인벤토리·후보·청구포인트·선행기술)특허
11검증 결과표전체
12후임자 인계(데이터흐름·튜닝상수·빌드·업그레이드)개발
13한계 / 출원 전 확인사항전체
+
+

1. 시스템 개요 & 아키텍처

+

1.1 핵심 아이디어

+

드론으로 촬영한 철도 선로 주행영상의 각 프레임에는 SRT 메타데이터로 드론 위치(lat/lon/고도)·자세(yaw/pitch/roll)·초점거리가 기록된다. 한편 철도 GIS에는 측점·POI·선로 중심선의 WGS84 좌표(+표고) 가 있다. 이 둘을 핀홀 카메라 투영으로 결합하면:

+
    +
  1. 순방향 투영 — "이 GIS 지점이 지금 화면 어느 픽셀에 보이는가" → 라벨/중심선/궤적 오버레이.
  2. +
  3. 역투영 — "사용자가 화면에서 끈 픽셀은 어느 지리좌표인가" → POI 위치/표고 보정.
  4. +
  5. 측점 색인 — 드론 GPS를 측점 폴리라인에 투영해 영상 매 시점을 측점값(체이니지) 으로 환산 → 측점 기반 탐색/스크러빙.
  6. +
+

1.2 데이터 흐름

+
[폴더 선택: base.csv(드론프레임) + building/측점·교량·터널·역사 CSV + route.json + KMZ]
+        │  geoData.loadFolderGeoData (인코딩 자동감지·병렬 파싱·KMZ 우선)
+        ▼
+[geoStore]  frames / stations / pois / structures / centerline / origin / routeMeta
+        │
+        ├─▶ StationOverlay (영상 위 AR 오버레이)
+        │     precompute(가시성/겹침, requestIdleCallback) → RAF(연속포즈 재투영) → Canvas
+        │     geoProjection: WGS84→TM→ENU→카메라→핀홀
+        │
+        ├─▶ StationBar (하단 측점 스크러버)
+        │     chainage: 드론GPS→측점폴리라인 수직투영→연속 체이니지
+        │     이동거리/측점축, 방향색, 구조물 마커, 측점검색 순환
+        │
+        └─▶ VideoPlayer (Video.js + hls.js, 이중 재생경로, 프레임 정확도)
+

1.3 기술 스택

+
    +
  • 프론트엔드: React 18 + TypeScript, Video.js 8.23 + @videojs/http-streaming, hls.js 1.6, Zustand(상태), Vite, Canvas 2D(오버레이), proj4(좌표변환), fflate(KMZ 압축해제).
  • +
  • 백엔드: Node.js 20 + Express, child_process.spawn FFmpeg 래퍼, Range Request 스트리밍, HLS 변환 + SSE 진행률.
  • +
  • 배포: PM2(ghiVideo, 포트 55000)가 client/dist 정적 서빙(빌드 결과물). 소스 변경 시 npm run build -w client(Node 20) 후 반영.
  • +
+
+

2. 좌표·투영 정본 파이프라인 (★ 모든 정합의 기반)

+
+

본 절을 정본으로 하고, 3~7장은 이를 참조한다. Python 레퍼런스 advanced_tuner_v2.py와 회전행렬·축정렬·정규화 공식이 1:1 동치로 이식되어 있다(geoProjection.ts:1-12).

+
+

2.1 5단계 변환

+
[1] WGS84 (lat, lon, EL정표고)
+      │  proj4 EPSG:4326 → EPSG:5186 (한국 중부원점 TM)
+      ▼
+[2] 월드 ENU [East(m), North(m), Up(m)]   (East=X, North=Y)
+      │  rel = target_ENU − (drone_ENU + offset)
+      ▼
+[3] 카메라좌표 [Xc, Yc, Zc]   (R_w2c = R_align · R_b2w(yaw,pitch,roll)ᵀ)
+      │  핀홀 + focal/sensor
+      ▼
+[4] 정규 이미지 좌표 [px, py] ∈ (0~1)   (0.5 = 영상 중심)
+      │  object-fit:cover 변환
+      ▼
+[5] 화면 px
+

2.2 단계별 수식·코드

+

단계 1 — WGS84 → TM (geoProjection.ts:75-85)

+
proj4.defs('EPSG:5186', '+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs');
+function latLonToTM(lat, lon) { const [e, n] = _toTM.forward([lon, lat]); return [e, n]; }
+

단계 2 — TM → ENU + 표고 datum 정합 (geoProjection.ts:91-97, 263-271). X/Y는 절대 TM, Z만 기준고도 대비 상대(상대벡터를 빼므로 무관). 핵심: GIS 정표고(EL) + 지오이드고(N) → 타원체고로 변환해 드론 GPS 타원체고와 datum을 맞춘다.

+
const stEnu = geoToEnu(targetLat, targetLon, targetAlt + (params.geoidOffset ?? 0), ...); // EL + N
+const drEnuAdj = [drEnu[0]+offX, drEnu[1]+offY, drEnu[2]+offZ];
+const relEnu = [stEnu[0]-drEnuAdj[0], stEnu[1]-drEnuAdj[1], stEnu[2]-drEnuAdj[2]];
+
+

지오이드고 N: 대전지역 ≈ 25.8m(= 84.244 − 16.853 − 41.59, KNGeoid 검증값). 초기 문서엔 25.449m로 기록된 바 있으나 본 명세는 25.8m로 통일(§11). 지역 상수이므로 타 지역 데이터셋은 재튜닝 필요.

+
+

단계 3 — ENU → 카메라좌표 (geoProjection.ts:275-295). R_b2w = Rz(−yaw)·Rx(pitch)·Ry(roll), R_w2c = R_align·R_b2wᵀ, R_align=[[1,0,0],[0,0,−1],[0,1,0]]. 행렬을 명시 생성하지 않고 전치(행=열)·부호치환으로 직접 계산:

+
function applyRw2c(b2w, rel) {
+  return {
+    Xc:   b2w[0][0]*rel[0] + b2w[1][0]*rel[1] + b2w[2][0]*rel[2],  //  col0
+    Yc: -(b2w[0][2]*rel[0] + b2w[1][2]*rel[1] + b2w[2][2]*rel[2]), // -col2
+    Zc:   b2w[0][1]*rel[0] + b2w[1][1]*rel[1] + b2w[2][1]*rel[2],  //  col1
+  };
+}
+

규약: Zc>0 = 카메라 정면(영상 깊이축), Xc=우, Yc=하. Zc≤0은 카메라 뒤 → null.

+

단계 4 — 카메라좌표 → 정규 픽셀(핀홀) (geoProjection.ts:126-137). 영상 중심 = 0.5, 해상도 독립:

+
pxRaw = (0.5 + cx0) + (Xc/Zc) * (f / sensorW);   // 가로
+pyRaw = (0.5 + cy0) + (Yc/Zc) * (f / sensorH);   // 세로
+

단계 5 — 정규 → 화면 px (object-fit:cover) (StationOverlay.tsx:923-933). 영상 크롭 사각형에 맞춰 정합:

+
const s = Math.max(W / vW, H / vH);              // cover: 더 큰 배율(넘침=크롭)
+dispW = vW * s; dispH = vH * s; offX = (W - dispW)/2; offY = (H - dispH)/2;
+const vx = (nx) => offX + nx * dispW;  const vy = (ny) => offY + ny * dispH;
+

2.3 두 투영 경로의 의도적 분리

+
    +
  • 렌더(정밀): geoProjection.ts — proj4 EPSG:5186 TM + 회전행렬 + geoid/포커스 보정. (3~5장)
  • +
  • 검색(경량): geoSearch.ts — cos-lat 근사 ENU. "현재 시야 내 POI" 또는 "특정 POI가 보이는 프레임 구간" 산출용.
  • +
+

ENU 월드원점은 첫 측점(없으면 첫 드론프레임, 없으면 중심선 첫 점)(geoSearch.ts:117-128).

+
+

3. 카메라 투영 & 화각(FOV) 정합

+
+

좌표 변환 파이프라인은 §2 참조. 본 절은 화각·내부표정 보정과 역투영을 다룬다.

+
+

3.1 focal ↔︎ FOV 관계

+

화각은 focal과 센서 치수로 결정된다(핀홀): FOV = 2·atan(sensor / (2·focal)) (StationOverlay.tsx:864). 기본값 focal=24mm, sensorW=36mm(풀프레임 환산), sensorH=20.25mm(=36×9/16, 16:9) (geoProjection.ts:48-66). 이 hFOV는 나침반/미니맵 시야부채꼴에도 사용.

+

3.2 fovMode — 세로 화각(sensorH) 역산 보정 (★)

+

라벨이 멀리/가까이에서 상하로 어긋날 때, 사용자가 POI 하나를 영상 속 실제 위치로 드래그하면 그 한 점의 대응으로 세로 화각을 역산한다. 설계 핵심: 가로 focal(f/sensorW)은 건드리지 않고 sensorH만 역산 → 빨간 중심선이 좌우로 틀어지지 않으면서 상하 배율만 교정.

+

투영식 py = 0.5 + cy0 + (Yc/Zc)·(f/sH) 를 sH에 대해 풀면 (StationOverlay.tsx:1326-1344):

+
const cc = toCameraCoords(drone, drag.lat, drag.lon, poiZ, p, wo);
+const b = cc.Yc / cc.Zc;
+const v = pos.y - 0.5 - (p.cy0 ?? 0);
+const sHNew = (b * p.focalLen) / v;            //  sH = (Yc/Zc)·f / (py − 0.5 − cy0)
+setParam('sensorH', Math.max(6, Math.min(36, sHNew)));
+

영상 세로 중심(v≈0)은 발산 → |v|<0.03이면 거부하고 안내.

+

3.3 핵심 함수

+
    +
  • toCameraCoords (geoProjection.ts:301-317) — GIS점→카메라좌표. 부산물 distH(수평 직선거리), fwd/side(진행방향 앞/옆 성분, 비등방 거리필터용). pixelFromCamera와 분리한 이유: 중심선처럼 선분 단위 근거리 Zc 클리핑·보간 후 투영해야 하기 때문.
  • +
  • pixelFromCamera (geoProjection.ts:126-137) — 카메라좌표→정규픽셀(클램프 없음).
  • +
  • groundPointFromPixel (geoProjection.ts:218-251) — 화면픽셀→지면 lat/lon. 광선-수평면 교차: +
    const t = relUpTarget / dir[2];          // dir = R_w2cᵀ·((px−0.5−cx0)·sW/f, (py−0.5−cy0)·sH/f, 1)
    +if (t <= 0) return null;                 // 카메라 뒤/위
    +const E = drEnu[0]+offX + t*dir[0]; const N = drEnu[1]+offY + t*dir[1];
    +const [lon, lat] = _toTM.inverse([E, N]);
  • +
  • 관련 역투영 2종: worldFromPixel(슬랜트거리 유지 lat/lon/z 동시복원, 149-183), solveZForPixelY(lat/lon 고정, py에 맞는 z만 1차식 역산, 194-211). → 발명 C(§7).
  • +
+

3.4 보정값 영속화 (데이터셋별)

+
    +
  • 카메라/내부표정·표시설정: ghivideo:calib:{baseName} (로드 1회, 저장 800ms debounce, StationOverlay.tsx:473-500).
  • +
  • POI 위치/표고 override(DEM·드래그): ghivideo:poiov:{baseName} (폴더 파일 override 위에 localStorage 병합, 502-524).
  • +
+

3.5 설계 트레이드오프·한계

+
    +
  • 렌즈 왜곡 미보정: 핀홀만 사용(방사/접선 왜곡 계수 없음). 분석 보조 라벨 정합엔 표준 정확도로 충분(가장자리 수 px 오차 가능).
  • +
  • 클라이언트 순수 계산: proj4만 의존, 서버 왕복 없이 60fps RAF에서 직접 투영.
  • +
  • focal 분리 보정: 한 점 대응으로 sensorH 한 축만 역산(가로 정합 보존).
  • +
  • POI 표고 가정: 실측 표고 없으면 최근접 선로 지면표고(또는 "드론고도−이격") → §4·드래그·DEM 보정.
  • +
+
+

4. 측점 표고 정합 (DEM-free 영상정합) — 발명 B

+

4.1 무엇을 / 왜

+

표시할 시설물(POI)은 주소 기반이라 표고값이 없는 경우가 대부분이다. 표고가 없으면 부각(수직각) 산출 불가 → 종래는 지면을 평면으로 가정. 그러나 노선은 실제 기복(정표고 41.6m→57.5m)이 있어 평면 가정 시 라벨이 화면 밖/지면 아래로 이탈한다. 실측에서 잘못된 고도(≈0) 사용 시 부각오차 42.7°.

+

→ 별도 DEM 취득 없이, 노선을 따라 실측된 측점의 정표고를 지면고도원으로 삼아 정합한다.

+

4.2 구성(독립항 필수요소)

+
    +
  1. 측점 표고 수열 보유 — 노선 따라 실측된 복수 측점의 (lat/lon + 정표고 EL). (실데이터: 측점 47개, 41.6→57.5m)
  2. +
  3. 최근접 측점 지면고도 채택 — 표고 미상 대상에 가장 가까운 측점의 정표고를 지면고도로. 프레임 진행에 따라 최근접 측점이 바뀌어 지면고도가 노선 기복을 따라 갱신.
  4. +
  5. 지오이드 datum 정합 — 정표고(EL) + 지오이드고(N) → 타원체고 → 드론 GPS와 동일 기준. rel_up = (targetZ + N) − droneAlt.
  6. +
  7. 정합 투영 — §2 파이프라인 수행.
  8. +
  9. (종속) 대상별 표고 오프셋(poiZOffset) — 건물높이/지반차 보정, 측점·중심선엔 미적용.
  10. +
+

4.3 검증값

+
타원체고 = 정표고(EL) + 지오이드고(N≈25.8m, 대전)
+# 측점 157K900, frame0, 수평거리 91.3m
+정표고≈0     → 부각 42.7°  ❌
+정표고 41.5  → 부각 25.1°
+정표고+N≈67  → 부각 11.0°  ✅ (드론 rel_alt 16.85m와 일치)
+
    +
  • 데이터 함정(핵심): building/01)측점.csv에 표고 컬럼이 둘 — Z좌표(로컬 ≈0) / Z좌표_한국(EPSG:5186 정표고 ≈41.5m). 기존 코드가 Z좌표(≈0)를 읽어 42.7° 오차 발생 → Z좌표_한국 우선으로 교정(geoData.ts 측점 파서).
  • +
  • 효과: 부각오차 42.7°→11.0°, 구글어스 DEM 대비 0.2m 일치(2160p에서 ~6px). DEM 취득비용 불필요. 공개 DEM(SRTM 30m)보다 측량 측점이 더 정밀.
  • +
+
+

5. 드론 자세 평활/보간 + 영상 오버레이 렌더링

+
+

두 본질 과제: (a) 자세/GPS 노이즈(단순평균은 회전 뭉갬→지연), (b) 렌더 부드러움 vs 계산비용(가시성 판정은 비싸고 60fps는 매 프레임 재투영 요구).

+
+

5.1 에지(회전경계) 보존 적응형 이동평균 — smoothFrame

+

StationOverlay.tsx:577-604. 중심 i에서 좌우 최대 halfWin(기본 60fr≈2초)까지 평균하되, 중심 yaw와 차이가 YAW_EDGE=8° 이내인 인접까지만 창 확장.

+
const YAW_EDGE = 8;
+const near = (idx) => Math.abs(((frames[idx].yaw - yc + 540) % 360) - 180) <= YAW_EDGE;
+while (lo > loMin && near(lo - 1)) lo--;   // 회전경계에서 멈춤
+while (hi < hiMax && near(hi + 1)) hi++;
+// yaw는 원형평균: atan2(Σsin, Σcos)
+yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI,
+
    +
  • 직선: 창 풀폭 → 노이즈 강하게 억제. 회전 중: 창 좁아져 즉시 추종(지연 없음). 회전 후: 한 프레임씩 점진 재확장(튐 없음). 과거 '변화율 기반 축소'는 회전 종료 시 한꺼번에 재확장돼 회전 프레임이 다시 섞여 1회 튀던 부작용 → 경계보존으로 교체.
  • +
+

5.2 연속 프레임번호 보간 포즈 — poseAt

+

StationOverlay.tsx:670-690. 실수 프레임번호 estFrame에 대해 양 이웃의 평활포즈를 선형보간(이산 30fps 격자→60fps 연속). yaw는 최단각 보간:

+
let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180;   // 최단각
+return { ...a, frame: estFrame, lat:L(a.lat,b.lat), ..., yaw: a.yaw + dy*frac };
+

O(log n) 이진탐색으로 매 RAF 호출 가능.

+

5.3 속도적응 EMA + 이상치 거부 — smoothStep (One-Euro 사상)

+

StationOverlay.tsx:48-61. 느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종.

+
if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES)  // 0.12 초과 점프 = 이상치 → 위치 유지
+  return { x: prev.x, y: prev.y, rej: prev.rej + 1, ... };
+const vx = prev.vx + (dx - prev.vx) * SMOOTH_VEL_BETA; // β=0.25 속도평활(떨림 왕복은 상쇄)
+const a = Math.min(maxAlpha, minAlpha + (maxAlpha - minAlpha) * Math.min(1, speed/speedRef));
+return { x: prev.x + dx*a, y: prev.y + dy*a, ... };
+
    +
  • 이상치 거부에 연속거부 한계(MAX_REJECT_FRAMES=8) → 시크/재등장 같은 진짜 큰 이동에 영구 고착 방지.
  • +
  • 추가로 픽셀 히스테리시스(0.75px 이상일 때만 정수좌표 갱신)로 저속 반올림 깜빡임 제거(1048).
  • +
+

5.4 라벨 사전계산 + 겹침 억제

+
    +
  • startLabelPrecompute (StationOverlay.tsx:692-746): 전 프레임의 가시집합·겹침해소 결과Map<frameNum, LabelCache>에 사전계산. 화면좌표가 아니라 월드좌표(lat/lon/z)를 저장 → RAF가 매 프레임 연속포즈로 재투영. requestIdleCallback 200프레임 청크, 취소토큰(precomputeIdRef), 현재 프레임부터 순환 처리(재생 즉시 라벨), 첫 청크 즉시 노출, 재계산 시 옛 맵 복사.
  • +
  • 겹침 억제 (755-784): 임계 가로 POI_MERGE_X=0.10/세로 0.035(비등방, 텍스트가 가로로 긴 특성). 우선순위 ①철도역/역사 최우선 ②근접거리. 진행방향 변형 우선: 상/하행 형제 겹침 시 노선 방향((상)/(하)) 변형을 남김.
  • +
+

5.5 RAF 렌더 루프 & 성능 설계

+

매 프레임 계산 없이 조회+투영+드로잉만. 라이브 시간은 VideoPlayer smoothTimeRef(단조 벽시계 보간, 일시정지·시크·배속 보정)에서 받음.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
작업비용배치빈도
가시성/겹침 판정높음precompute1회(+설정변경 debounce), idle 청크
좌표 투영중간RAF매 프레임
평활/EMA/히스테리시스낮음RAF매 프레임
팝업 가시집합 setState리렌더setInterval 150ms변화 시만
+

핵심 = "무엇을 그릴지"(precompute) ↔︎ "어디에 그릴지"(RAF) 분리로 단일스레드 60fps 달성. 중심선/드론궤적은 buildLines로 매 프레임 직접 투영(근거리 Zc 클리핑·outcode 컬링). 나침반은 별도 ±3fr 평활 yaw + 360° 누적 언랩.

+
+

6. 측점(체이니지) 시스템 & 스테이션바

+

6.1 연속 체이니지 수직투영 (핵심 ①)

+

드론 한 프레임의 위경도를 측점 폴리라인에 수직 투영해 100m 양자화가 아닌 10m 해상도 연속 측점값을 얻는다(부산물: 선로 수직 이격 offsetM).

+
    +
  • 폴리라인 구축 buildChainLine (chainage.ts:22-32): 측점값 오름차순 정렬, 평면투영 x=lon·k, y=lat·111000 (k=cos(lat0)·111000).
  • +
  • 점-선분 투영 projectToChain (chainage.ts:35-50): +
    const t = L2===0?0:Math.max(0,Math.min(1, ((px-a.x)*dx + (py-a.y)*dy)/L2));
    +const d = (px-cx)**2 + (py-cy)**2;
    +if (d < bestD) { bestD = d; bestKm = a.km + (b.km-a.km)*t; }   // 최근접 선분 위 km 보간
  • +
  • 왜 연속 투영인가: 최근접 측점점(100m 양자화)은 값이 계단처럼 튀고 두 측점 중간에서 전진/후진 전환 시점이 어긋난다. 수직투영은 선분 보간으로 매 프레임 부드럽게 변해 방향 전환점(극값) 타이밍이 실제 주행과 일치.
  • +
+

6.2 이중 측점값: 배지용 km vs 방향용 chain

+

프레임별 precompute(StationBar.tsx:233-246): km=최근접 측점점(100m, 배지/폴백), chain=연속투영(10m, 방향/축/탐지). videoFps는 실데이터(마지막 프레임/재생시간)에서 유도, 미비 시 30000/1001 폴백.

+

6.3 축 설계: 시간축이 아닌 위치축 (핵심 ②)

+

선로 점검 영상은 호버(정지비행)전·후진 반복 특성이 있어 시간축이 부적합하다(호버 시 커서가 위치와 어긋남).

+
    +
  • 이동거리축 (StationBar.tsx:250-271): 연속 측점값을 ±8fr 평활 후 프레임간 절대 변화량 누적(cum += |sm[i]−sm[i−1]|)을 정규화(frac, 단조 비감소). 호버=frac 정체(커서 정지), 이동=frac 증가. 방향은 색 리본으로 직교 분리.
  • +
  • 측점축(후속 채택안, history 2026-06-26_1520): x축을 측점값 비례로 두고, 축 범위를 역 POI 첫/끝으로 노선 고정(영상 무관 절대 측점) → 어떤 데이터로 재생하든 같은 구조물이 같은 위치/측점에 표시(회차 간 정렬).
  • +
  • 시간↔︎축 양방향 변환: cumFracAtTime/timeAtFrac(또는 chainAtTime/pxAtChain)를 이진탐색+보간으로. 커서·구간색·구조물 마커가 동일 매핑 공유. 라이브 커서는 React 리렌더 없이 RAF에서 CSS 변수(--pos-px)만 갱신(771-786).
  • +
+

6.4 방향 판정 & 방향색 트랙

+

평활 측점값 추세를 극값추적 + 히스테리시스 HYST=10m로 방향 전환점 확정(395-458). forwardDir = arrChain≥depChain?1:−1. 정방향=주황 음영, 역방향=청록. 이동거리/측점축은 구간 px가 단조라 linear-gradient 1장으로 경량 처리(686-726).

+

6.5 구조물 마커 — 좌표/측점 이중 탐지 & kmExists (핵심 ③)

+

두 통과 탐지기:

+
    +
  • pxPassesTo(lat,lon,off) (489-534) — 좌표 근접. 임계 TH=max(100, gmin·3).
  • +
  • pxPassesAtMileage(targetM) (537-559) — 연속 체이니지가 TH=stationTolerance(기본 20m) 이내.
  • +
+

탐지 우선순위(단락평가): ①명시 측점값 ②좌표를 측점선 투영→측점기준(통과순서 정확) ③이름매칭 POI 투영 ④좌표근접 폴백 … (612-639).

+

역사 합집합 보강: 종점/조차장은 측점선 투영이 불안정해 재진입을 놓침(예 대전조차장 162k080). 단락평가 탓에 한 통과를 찾으면 좌표근접이 안 돌아 누락 → 역사에 한해 좌표근접 통과를 합집합으로 합치되 24px 임계 중복제거(629-634).

+

kmExists (★): 좌표 반경만으로 잡힌 통과는 실제 측점값이 그 위치에 없을 수 있다.

+
const kmExists = stationKmVal == null || Math.abs(p.km - stationKmVal) <= existTol;  // existTol = stationTolerance
+

(StationBar.tsx:648) false측점값 라벨·측점검색에서 제외하되 마커(동그라미)는 유지 — "위치 표시"와 "측점 단정"의 분리.

+

종점 미도착: endGapPx=min(폭·0.15, (gapM/lenM)·폭), placeUnreachedTerminal이 최우측 역사를 트랙 끝에 미도착(속 빈 링)으로 고정(303-308, 574-586).

+

6.6 측점검색 순환 — handleJumpToMileage

+

StationBar.tsx:850-916. 측점값 입력 시 여러 통과를 통과방향 순환:

+
    +
  1. 후보 1순위: 화면 마커(structureMarks) 중 kmExists!==false 이고 표시 10m값 일치 → px→timeAtFrac 역변환. (좌표근접 통과 포함 → 모든 동그라미 도달)
  2. +
  3. 2순위: 마커 없으면 chain 연속구간, 최종 전역 최근접 폴백.
  4. +
  5. 정렬 + 0.3s 중복제거 → baseT 기준 다음 통과(끝→처음 순환). jumpRef로 연속검색 판정. Enter 후 입력값 유지, blur 시 삭제(PlaybackControls.tsx).
  6. +
+

6.7 타임라인 보조 기술 (Timeline.tsx)

+
    +
  • 동일항목 브래킷 클러스터: 동명 다통과를 거리 무관 한 묶음으로(드롭+수평 점선) 중앙 라벨 1개(143-172).
  • +
  • 라벨 2줄 균등분할 splitStructLabel: CJK 1.0/기타 0.55폭, 괄호그룹은 통째로 아랫줄(30-49).
  • +
  • 측점값 라벨 겹침 숨김 showMileage: 직전 표시와 (lastW+w)/2 미만이면 숨김, kmExists===false는 애초 생략(117-137).
  • +
  • 아이콘 passed 전환: upcoming/passed 두 PNG를 opacity로 전환(무깜빡임), 전 상태 사전로드.
  • +
+
+

7. 위치보정 역투영 (단일 제스처) — 발명 C

+

7.1 무엇을 / 왜

+

지오코딩 POI는 ±수십 m 오차. 2D 화면 이동만으로는 카메라로부터의 거리(깊이)를 결정 못함(깊이 모호성). 종래는 수평/높이를 따로 조정하거나 깊이맵(LiDAR) 요구. → 단일 드래그로 수평·수직(3D) 동시 보정, 깊이맵 불요.

+

7.2 구성(독립항 필수요소)

+
    +
  1. 슬랜트거리 보존 — 드래그 개시 시점의 카메라–객체 슬랜트거리(range)를 고정 → 깊이 모호성 해소 핵심 구속.
  2. +
  3. 역투영 — 드롭 픽셀을 카메라 방향벡터로 환산 후 보존 거리만큼 투사.
  4. +
  5. 월드좌표 복원 — 자세 회전·평행이동 역변환으로 lat/lon/z 복원(지오이드 역환원). 한 번의 드래그로 수평·수직 동시.
  6. +
  7. 영속·재적용 — 복원 좌표를 객체 식별자(title)에 연계해 <base>_poi_overrides.json + ghivideo:poiov:<baseName> 저장.
  8. +
  9. (종속) 가시영역 한정 — 전방(Zc>0)만 보정.
  10. +
+
dir_cam  ((px−0.5−cx0)·sW/f, (py−0.5−cy0)·sH/f, 1)
+cam = dir_cam/|dir_cam| · range          // ★거리 보존(드래그 시작값)
+rel = R_w2cᵀ · cam                        // 회전 역변환
+z   = droneAlt + offZ + rel.up − N_geoid  // 정표고 복원
+(lat,lon) = TM⁻¹(droneE+offX+rel.x, droneN+offY+rel.y)
+

변형 — z 전용 역산 solveZForPixelY: lat/lon 고정, 드롭 세로위치로 z만 1차식 역산. 왕복검증 z=42→py→42.000.

+

7.3 효과

+

2D 드래그 1회로 3D 보정(단계 절반), 역투영 왕복오차 ≈0, 깊이맵 불요, 파일+localStorage 재현성. "depth map 없이 거리보존" 을 명시해 US9188444B2와 구별(§10).

+
+

8. 백엔드 스트리밍/FFmpeg · 데이터 파이프라인 · 상태관리 · 플레이어

+

8.1 대용량(2GB+) 재생 — 이중 경로

+
    +
  • 로컬(useVideoPlayer.ts:80-88): URL.createObjectURL Blob URL 직접 재생(업로드/변환 없음), 교체 시 revokeObjectURL(누수 방지). 현 운영 주 경로.
  • +
  • 서버: Range Request 즉시 재생, 비표준 코덱(HEVC 등)이면 HLS 자동 전환(15-17). (서버/HLS 경로는 보존되나 UI는 SHOW_TOOLBAR=false로 숨김.)
  • +
+

8.2 Range Request (streaming.ts:52-77)

+

핵심: 한 응답에 전체를 싣지 않음. 끝값 없으면 start+10MB−1. pipeline()(백프레셔/에러 자동), highWaterMark:1MB, Accept-Ranges:bytes, path traversal 방어(resolveVideoPath). JS Number(2^53−1)로 4GB+ 안전(BigInt 불요). 멀티레인지 미지원.

+

8.3 HLS 변환 + SSE (hls.ts)

+

멱등(디스크에 index.m3u8 있으면 즉시 done), 코덱 분기(H.264 -c copy 수 초 / 그 외 libx264), /progress SSE로 stderr time= 파싱 푸시. hls.js 필수: backBufferLength:30(기본 Infinity면 장시간 재생 메모리 누수→크래시), maxBufferLength 30, enableWorker(useVideoPlayer.ts:7-13).

+

8.4 FFmpeg spawn 래퍼 (ffmpeg.ts)

+

-y auto, stderr time=HH:MM:SS.cc 정규식 진행률, exit≠0 시 stderr 끝 500자 reject. 프레임 추출은 -accurate_seek -ss {t} -i {f} -frames:v 1(플래그 순서 = 입력단 정확 시킹, frame.ts:37-44).

+

8.5 데이터 파이프라인 (geoData.ts)

+

<input webkitdirectory>/드래그로 받은 File[]을 병렬 파싱(Promise.all). 인코딩 자동감지(BOM→UTF-8/EUC-KR), 헤더명 우선+위치 폴백(makeFieldIndexer), 좌표 유효성 게이트(lat 33~39/lon 124~132), KMZ 우선(있으면 CSV POI/구조물 미사용, fflate unzipSync).

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
소스파서대상매핑
<base>.csvparseDroneFramesDroneFrame[]frame_cnt, lat/lon/alt, yaw/pitch/roll, focal
building/01)측점.csvparseStationsstation + DirectionChange측점→title, Z좌표_한국(정표고) 우선, 비고→방향전환
<base>_POI.csv / 02)지장물.csvparsePoispoicategory_clean/명칭, lat/lon
02)지장물_역사.csvparseHistoricStations역사 구조물명칭→name(스테이션바 마커)
03)교량/04)터널/06)구교.csvparseStructuresbridge/tunnel구분/시설물명, 연장→lengthM, 시설종별→grade
05)출입문번호.csvparseAccessDoors출입문 poi출입문번호→title
*.kmz/kmlparseKmzpoi+구조물Placemark + description표→props
route.jsonmergeStructures보정station/offset/이정 override
+

중심선은 v2.0에 별도 파일이 없어 측점을 측점값 순 정렬한 폴리라인으로 합성.

+

8.6 상태관리

+
    +
  • geoStore(메모리, persist 없음): frames/stations/pois/structures/centerline/origin/routeMeta. basePois(원본) vs pois(override 적용) 분리로 원본 불변성(geoStore.ts:96-110).
  • +
  • settingsStore(localStorage ghivideo.settings): 시설종별 필터, 표시 토글들. 커스텀 merge로 스키마 진화 대비. isGradeVisible 공유 규칙.
  • +
+

8.7 플레이어 통합 & 프레임 정확도

+
    +
  • Video.js data-vjs-player + Strict Mode 가드, controls:false(측점바로 대체), fill:true.
  • +
  • FPS 감지(useFrameStep.ts:21-49): requestVideoFrameCallback, 첫 콜백은 시작점만 기록(31fps 오감지 방지), 연속 2회 일치 확정. 스텝은 1/fps(누적 금지=드리프트 방지).
  • +
  • 이원 fps: 스텝 이동=실측 fps, 프레임번호 표시=29.97 고정(SRT 기준). 의도된 분리.
  • +
  • 매끄러운 커서: rAF 단조 벽시계 보간 smoothTimeRef(매 프레임, transform) + smoothTime(throttle, 배지) 분리.
  • +
+
+

9. 개발 타임라인 (주요 기술 의사결정)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
시기핵심 결정/구현
2026-04-01labelMap 사전계산 + requestIdleCallback + RAF, ±10fr 이동평균(yaw sin/cos), 30→60fps 보간
2026-04-02투영 파이프라인 정립: EPSG:5186 TM, 핀홀(Rz·Rx·Ry), 타원체고=정표고+지오이드, Cohen-Sutherland 클리핑
2026-06-19v1→v2 데이터 형상 전환(실측 폴더), 인코딩 자동감지, 측점 비고→방향전환 추출, 시설종별 필터
2026-06-22측점 표고 컬럼 교정(Z좌표_한국) → 부각 42.7°→11°(발명 B). 역투영 드래그 보정+override(발명 C 최초). title-키 보간+이상치 거부. 비등방 거리필터
2026-06-23에지보존 평활(YAW_EDGE=8°). 글로벌+KIPRIS 선행기술 조사 → B·C 신규성 확인
2026-06-241클릭 FOV 보정(→sensorH 역산), object-fit:cover 정렬, 라벨 월드좌표 저장+RAF 재투영, One-Euro EMA, KMZ 직접 로딩
2026-06-25~26종점역 누락 수정, 양끝 역 고정, 라벨 2줄분할, 동일항목 브래킷, 거리축→측점축 전환
2026-06-29좌표/측점 이중탐지+합집합 보강, 측점값 라벨 겹침숨김, 측점검색 순환+마커기준 전환+kmExists, UI 통합(토글 재생바 이동, 활성색 amber), 보고서 3종 생성, 55000 외부접속
+

상세 근거: docs/history/2026-06-22_POI투영오차-개선.md(발명 B·C 1차 출처), 2026-06-26_1520_측점축-전환, 2026-06-29_1029_역사-통과-좌표근접-보강 등.

+
+

10. 특허 관점 종합

+
+

출처: docs/특허_스테이션기반_주행영상_플레이어.html(기술발굴), docs/발명신고서_측점표고정합_역투영보정.html(변리사 제출용).

+
+

10.1 기술요소 인벤토리 (E1~E9)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
코드요소특허 적합성
E1측점(체이니지) 기반 영상 색인·탐색후보 A(좁게)
E2지리객체 실시간 영상 정합(labelMap 사전계산)공지(큰 틀)
E3측점 표고 기반 DEM-free 지면고도 추정후보 B(상)
E4역투영+슬랜트거리 보존 단일드래그 위치보정후보 C(상)
E5비등방 거리필터+소실점 클러터 억제후보 D(종속)
E6노이즈 강건 오버레이(이중평활·title짝·이상치거부)후보 E(종속)
E7측점 폴리라인→중심선제외(통상 처리)
E8이종 CSV 융합(인코딩 자동감지)제외
E9비고 텍스트 방향전환 추출제외
+

10.2 출원 권고 우선순위

+
    +
  • 1순위: 발명 B + C 결합 — 신규성/진보성 우수, 글로벌+한국 신규성 확인.
  • +
  • 2순위: 후보 A(좁게) — LRS 비선형 재좌표화 + 회차간 위치앵커 + 다중회차 동기. 한국 선행기술(KR102268318B1·KR100411587B1) 위험 → 좁게 청구. 본 명세의 측점축 노선좌표 고정(§6.3)·측점검색 순환(§6.6)·이중탐지 kmExists(§6.5)가 A의 구현 근거.
  • +
  • 3순위: D·E 종속항 흡수. 제외: E7·E8·E9.
  • +
+

10.3 청구 포인트 핵심 (B·C)

+
    +
  • 발명 B 독립항: ①측점 실측 정표고 수열 → ②표고미상 대상에 최근접 측점 정표고를 지면고도 채택(노선기복 반영) → ③지오이드고 가산으로 타원체고 변환·datum 정합 → ④정합 투영. ※ ②+③ 결합을 한 청구항에(개별요소는 공지).
  • +
  • 발명 C 독립항: ①드래그 개시 시 슬랜트거리 보존 → ②드롭픽셀 역투영 → ③단일 제스처로 수평·수직 동시 복원 → ④식별자 연계 영속. ※ "depth map 없이 거리보존" 명시(US9188444B2 구별).
  • +
+

10.4 선행기술 (조사 완료분)

+
    +
  • E1·E2 큰 틀은 공지: Esri ArcGIS Pro FMV, US9996976B2, Mandli Roadview, Remote GeoSystems LineVision, ENSCO Virtual Track Walk.
  • +
  • B·C는 글로벌+한국 신규성 확인. 단, KIPRIS 전문(專門)검색 미수행 — 출원 전 필수.
  • +
+

10.5 신규성 후보 종합 (구현 기반)

+
    +
  1. 드론 SRT pose 기반 실시간 GIS-영상 정합 파이프라인(타원체고/정표고 datum 자동 정합 포함).
  2. +
  3. 단일 대응점 sensorH 역산 인터랙티브 FOV 보정(가로 정합 보존, 중심부 발산 거부).
  4. +
  5. 깊이 모호성 분리형 역투영 3종(슬랜트유지/지면교차/표고 1차역산)을 보정 의도별 선택.
  6. +
  7. 에지(회전경계) 보존 적응형 평활 + 속도적응 EMA(연속거부 한계).
  8. +
  9. 월드좌표 precompute + 연속포즈 재투영 2단 분리로 단일스레드 60fps AR.
  10. +
  11. 누적 이동거리/측점 비례 축 + 방향색 직교 분리 진행바, 노선좌표 고정(회차 정렬).
  12. +
  13. 좌표/측점 이중 탐지 + 합집합 보강 + kmExists 신뢰도 플래그("위치 표시"와 "측점 단정" 분리).
  14. +
+
+

11. 검증 결과표

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
항목개선 전개선 후근거
부각 오차(측점 157K900, 91.3m)42.7°11.0°§4.3
지면고도 정확도(측점 vs 구글 DEM)0.2m(~6px@2160p)§4.3
역투영 왕복오차(z=42→py→z)≈0 (42.000)§7.2
화면 EMA 지연1.65s(α=0.01)~25ms(α=0.4)§5.3 / history 06-22
렌더끊김60fps(precompute+RAF 분리)§5.5
역 재진입 마커누락좌표근접 합집합 보강으로 표출§6.5
+

지오이드고 N: 본 명세 25.8m(KNGeoid 검증) 통일. 초기 문서값 25.449m는 각주 처리.

+
+

12. 후임자 인계

+

12.1 데이터 흐름 한눈에

+

geoStore(frames/stations/structures/routeMeta) → useEffect precompute(viewedRef=프레임별 km/chain/time, chainRef=time/frac/chain) → pxAtTime/pxAtChain·barSegments·structureMarks → Timeline 렌더. 모든 px는 단일 매핑(pxAtTime/pxAtChain)을 통과, 역변환은 timeAtFrac/chainAtTime만 사용.

+

12.2 튜닝 포인트

+
    +
  • 폴더 route.json: stationTolerance(통과/검색 허용오차, 20m), direction/directionChanges(방향 변형 필터), endStationGapMeters/lengthKm(미도착 폭), startStationName/endStationName.
  • +
  • 카메라(패널, 데이터셋별 저장): focalLen, sensorW, sensorH(FOV), yawOffset/pitch/roll, geoidOffset(지역 N), poiZOffset.
  • +
  • 코드 상수: 평활 W=8, YAW_EDGE=8°, HYST=10m, 좌표통과 TH=max(100,gmin·3), 역사 합집합 24px, 검색 중복 0.3s, EMA β=0.25/REJECT_DIST=0.12/MAX_REJECT_FRAMES=8.
  • +
+

12.3 빌드/배포 (★ 필수 숙지)

+

55000은 PM2 ghiVideoclient/dist(빌드 결과)를 정적 서빙한다. 소스만 고치면 화면에 반영 안 됨.

+
export PATH="$HOME/.nvm/versions/node/v20.20.2/bin:$PATH"   # 시스템 node v12tsc/vite 실패
+npm run build -w client        # = tsc && vite build → client/dist 갱신
+# 브라우저 Ctrl+Shift+R, 폴더 데이터는 런타임 로드라 폴더 재선택 필요
+

외부(LAN) 접속: netsh portproxy(Windows :55000 → WSL IP:55000) + 방화벽. WSL IP는 재부팅 시 변동.

+

12.4 업그레이드 포인트

+
    +
  1. 렌즈 왜곡(Brown-Conrady) 추가 → 가장자리 정합.
  2. +
  3. 다점 대응 자동 캘리브레이션(focal·주점·왜곡 번들조정).
  4. +
  5. 지오이드 격자(KNGeoid18) 보간으로 geoidOffset 자동화.
  6. +
  7. POI 표고 DEM 자동조회(현 부분지원).
  8. +
  9. 중심선 per-frame 투영 공간색인, precompute Web Worker화.
  10. +
  11. frame.ts fps 고정(30)→getVideoFps, HLS jobs DB 큐, 멀티 Range 지원.
  12. +
  13. 코드 통합: 측점 파서/포매터(chainage.ts ↔︎ StationBar.tsx), 투영함수(projectToChain ↔︎ projectChainage) 단일화.
  14. +
+
+

13. 한계 / 출원 전 확인사항

+
    +
  • KIPRIS 전문검색 미수행 — 출원 전 필수.
  • +
  • 지오코딩 수평오차는 데이터 한계(코드 보정은 드래그 수동).
  • +
  • geoidOffset 지역 상수(대전 25.8m) — 타 지역 재튜닝.
  • +
  • VFR/abs_alt 노이즈, 먼 거리 역투영 민감.
  • +
  • 미확정 항목(특허문서 [확인 필요] 유지): 다중회차 동기화 구현 범위, 위치앵커 주석 데이터모델, 권리귀속, 공개이력.
  • +
  • 렌즈 왜곡 미보정(핀홀만) — 가장자리 정확도 트레이드오프.
  • +
+
+

본 문서는 5개 서브시스템(카메라 투영/자세 평활/측점·스테이션바/백엔드·데이터/기존 특허문서) 정밀 조사 결과를 종합한 것이다. 각 소스 참조는 클릭 가능한 파일:라인으로 추적 가능하며, 특허 출원 시 본 문서의 §3~§7·§10과 history 원문을 함께 변리사에게 제공할 것을 권장한다.

+ + diff --git a/docs/기술명세_GhiVideo_종합기술문서.md b/docs/기술명세_GhiVideo_종합기술문서.md new file mode 100644 index 0000000..76a688a --- /dev/null +++ b/docs/기술명세_GhiVideo_종합기술문서.md @@ -0,0 +1,541 @@ +# GhiVideo 종합 기술 명세서 + +> **목적**: 본 문서는 (1) **특허 출원/발명 신고**의 기초 자료와 (2) **후임 개발자 인계·업그레이드** 자료를 겸한다. +> **대상 시스템**: GhiVideo — 시간축이 아니라 **노선 위치(측점·체이니지)** 를 1차 좌표로 삼아 드론 주행영상을 색인·탐색하고, 프레임별 카메라 자세로 측점·POI·선로중심선을 영상 위에 실시간 정합(geo-registration)하는 철도 주행영상 분석 플레이어. +> **작성일**: 2026-06-29 · 모든 소스 참조는 `[경로](경로#Lxx)` 형식(클릭 가능). + +--- + +## 0. 문서 구성과 읽는 법 + +| 장 | 내용 | 주 독자 | +|----|------|---------| +| 1 | 시스템 개요·아키텍처·데이터 흐름 | 전체 | +| 2 | **좌표·투영 정본 파이프라인** (모든 정합의 기반) | 전체 | +| 3 | 카메라 투영 & 화각(FOV) 정합 | 개발/특허 | +| 4 | 측점 표고 정합 (DEM-free) — 발명 B | 특허 | +| 5 | 드론 자세 평활/보간 + 오버레이 렌더링 | 개발 | +| 6 | 측점(체이니지) 시스템 & 스테이션바 | 개발/특허 | +| 7 | 위치보정 역투영 — 발명 C | 특허 | +| 8 | 백엔드 스트리밍/FFmpeg·데이터·상태관리 | 개발 | +| 9 | 개발 타임라인(주요 의사결정) | 전체 | +| 10 | **특허 관점 종합**(인벤토리·후보·청구포인트·선행기술) | 특허 | +| 11 | 검증 결과표 | 전체 | +| 12 | 후임자 인계(데이터흐름·튜닝상수·빌드·업그레이드) | 개발 | +| 13 | 한계 / 출원 전 확인사항 | 전체 | + +--- + +## 1. 시스템 개요 & 아키텍처 + +### 1.1 핵심 아이디어 + +드론으로 촬영한 철도 선로 주행영상의 각 프레임에는 SRT 메타데이터로 **드론 위치(lat/lon/고도)·자세(yaw/pitch/roll)·초점거리**가 기록된다. 한편 철도 GIS에는 측점·POI·선로 중심선의 **WGS84 좌표(+표고)** 가 있다. 이 둘을 **핀홀 카메라 투영**으로 결합하면: + +1. **순방향 투영** — "이 GIS 지점이 지금 화면 어느 픽셀에 보이는가" → 라벨/중심선/궤적 오버레이. +2. **역투영** — "사용자가 화면에서 끈 픽셀은 어느 지리좌표인가" → POI 위치/표고 보정. +3. **측점 색인** — 드론 GPS를 측점 폴리라인에 투영해 영상 매 시점을 **측점값(체이니지)** 으로 환산 → 측점 기반 탐색/스크러빙. + +### 1.2 데이터 흐름 + +``` +[폴더 선택: base.csv(드론프레임) + building/측점·교량·터널·역사 CSV + route.json + KMZ] + │ geoData.loadFolderGeoData (인코딩 자동감지·병렬 파싱·KMZ 우선) + ▼ +[geoStore] frames / stations / pois / structures / centerline / origin / routeMeta + │ + ├─▶ StationOverlay (영상 위 AR 오버레이) + │ precompute(가시성/겹침, requestIdleCallback) → RAF(연속포즈 재투영) → Canvas + │ geoProjection: WGS84→TM→ENU→카메라→핀홀 + │ + ├─▶ StationBar (하단 측점 스크러버) + │ chainage: 드론GPS→측점폴리라인 수직투영→연속 체이니지 + │ 이동거리/측점축, 방향색, 구조물 마커, 측점검색 순환 + │ + └─▶ VideoPlayer (Video.js + hls.js, 이중 재생경로, 프레임 정확도) +``` + +### 1.3 기술 스택 + +- **프론트엔드**: React 18 + TypeScript, Video.js 8.23 + @videojs/http-streaming, hls.js 1.6, Zustand(상태), Vite, Canvas 2D(오버레이), proj4(좌표변환), fflate(KMZ 압축해제). +- **백엔드**: Node.js 20 + Express, `child_process.spawn` FFmpeg 래퍼, Range Request 스트리밍, HLS 변환 + SSE 진행률. +- **배포**: PM2(`ghiVideo`, 포트 55000)가 `client/dist` 정적 서빙(빌드 결과물). 소스 변경 시 `npm run build -w client`(Node 20) 후 반영. + +--- + +## 2. 좌표·투영 정본 파이프라인 (★ 모든 정합의 기반) + +> 본 절을 **정본**으로 하고, 3~7장은 이를 참조한다. Python 레퍼런스 `advanced_tuner_v2.py`와 회전행렬·축정렬·정규화 공식이 1:1 동치로 이식되어 있다([geoProjection.ts:1-12](client/src/utils/geoProjection.ts#L1)). + +### 2.1 5단계 변환 + +``` +[1] WGS84 (lat, lon, EL정표고) + │ proj4 EPSG:4326 → EPSG:5186 (한국 중부원점 TM) + ▼ +[2] 월드 ENU [East(m), North(m), Up(m)] (East=X, North=Y) + │ rel = target_ENU − (drone_ENU + offset) + ▼ +[3] 카메라좌표 [Xc, Yc, Zc] (R_w2c = R_align · R_b2w(yaw,pitch,roll)ᵀ) + │ 핀홀 + focal/sensor + ▼ +[4] 정규 이미지 좌표 [px, py] ∈ (0~1) (0.5 = 영상 중심) + │ object-fit:cover 변환 + ▼ +[5] 화면 px +``` + +### 2.2 단계별 수식·코드 + +**단계 1 — WGS84 → TM** ([geoProjection.ts:75-85](client/src/utils/geoProjection.ts#L75)) +```ts +proj4.defs('EPSG:5186', '+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'); +function latLonToTM(lat, lon) { const [e, n] = _toTM.forward([lon, lat]); return [e, n]; } +``` + +**단계 2 — TM → ENU + 표고 datum 정합** ([geoProjection.ts:91-97](client/src/utils/geoProjection.ts#L91), [263-271](client/src/utils/geoProjection.ts#L263)). X/Y는 절대 TM, Z만 기준고도 대비 상대(상대벡터를 빼므로 무관). **핵심: GIS 정표고(EL) + 지오이드고(N) → 타원체고**로 변환해 드론 GPS 타원체고와 datum을 맞춘다. +```ts +const stEnu = geoToEnu(targetLat, targetLon, targetAlt + (params.geoidOffset ?? 0), ...); // EL + N +const drEnuAdj = [drEnu[0]+offX, drEnu[1]+offY, drEnu[2]+offZ]; +const relEnu = [stEnu[0]-drEnuAdj[0], stEnu[1]-drEnuAdj[1], stEnu[2]-drEnuAdj[2]]; +``` +> **지오이드고 N**: 대전지역 ≈ **25.8m**(= 84.244 − 16.853 − 41.59, KNGeoid 검증값). 초기 문서엔 25.449m로 기록된 바 있으나 본 명세는 25.8m로 통일(§11). 지역 상수이므로 타 지역 데이터셋은 재튜닝 필요. + +**단계 3 — ENU → 카메라좌표** ([geoProjection.ts:275-295](client/src/utils/geoProjection.ts#L275)). `R_b2w = Rz(−yaw)·Rx(pitch)·Ry(roll)`, `R_w2c = R_align·R_b2wᵀ`, `R_align=[[1,0,0],[0,0,−1],[0,1,0]]`. 행렬을 명시 생성하지 않고 전치(행=열)·부호치환으로 직접 계산: +```ts +function applyRw2c(b2w, rel) { + return { + Xc: b2w[0][0]*rel[0] + b2w[1][0]*rel[1] + b2w[2][0]*rel[2], // col0 + Yc: -(b2w[0][2]*rel[0] + b2w[1][2]*rel[1] + b2w[2][2]*rel[2]), // -col2 + Zc: b2w[0][1]*rel[0] + b2w[1][1]*rel[1] + b2w[2][1]*rel[2], // col1 + }; +} +``` +규약: **Zc>0 = 카메라 정면**(영상 깊이축), Xc=우, Yc=하. Zc≤0은 카메라 뒤 → null. + +**단계 4 — 카메라좌표 → 정규 픽셀(핀홀)** ([geoProjection.ts:126-137](client/src/utils/geoProjection.ts#L126)). 영상 중심 = 0.5, 해상도 독립: +```ts +pxRaw = (0.5 + cx0) + (Xc/Zc) * (f / sensorW); // 가로 +pyRaw = (0.5 + cy0) + (Yc/Zc) * (f / sensorH); // 세로 +``` + +**단계 5 — 정규 → 화면 px (object-fit:cover)** ([StationOverlay.tsx:923-933](client/src/components/overlay/StationOverlay.tsx#L923)). 영상 크롭 사각형에 맞춰 정합: +```ts +const s = Math.max(W / vW, H / vH); // cover: 더 큰 배율(넘침=크롭) +dispW = vW * s; dispH = vH * s; offX = (W - dispW)/2; offY = (H - dispH)/2; +const vx = (nx) => offX + nx * dispW; const vy = (ny) => offY + ny * dispH; +``` + +### 2.3 두 투영 경로의 의도적 분리 + +- **렌더(정밀)**: [geoProjection.ts](client/src/utils/geoProjection.ts) — proj4 EPSG:5186 TM + 회전행렬 + geoid/포커스 보정. (3~5장) +- **검색(경량)**: [geoSearch.ts](client/src/utils/geoSearch.ts) — cos-lat 근사 ENU. "현재 시야 내 POI" 또는 "특정 POI가 보이는 프레임 구간" 산출용. + +ENU 월드원점은 **첫 측점**(없으면 첫 드론프레임, 없으면 중심선 첫 점)([geoSearch.ts:117-128](client/src/utils/geoSearch.ts#L117)). + +--- + +## 3. 카메라 투영 & 화각(FOV) 정합 + +> 좌표 변환 파이프라인은 §2 참조. 본 절은 화각·내부표정 보정과 역투영을 다룬다. + +### 3.1 focal ↔ FOV 관계 + +화각은 focal과 센서 치수로 결정된다(핀홀): `FOV = 2·atan(sensor / (2·focal))` ([StationOverlay.tsx:864](client/src/components/overlay/StationOverlay.tsx#L864)). 기본값 `focal=24mm, sensorW=36mm(풀프레임 환산), sensorH=20.25mm(=36×9/16, 16:9)` ([geoProjection.ts:48-66](client/src/utils/geoProjection.ts#L48)). 이 hFOV는 나침반/미니맵 시야부채꼴에도 사용. + +### 3.2 fovMode — 세로 화각(sensorH) 역산 보정 (★) + +라벨이 멀리/가까이에서 **상하로 어긋날 때**, 사용자가 POI 하나를 영상 속 실제 위치로 드래그하면 그 한 점의 대응으로 세로 화각을 역산한다. **설계 핵심: 가로 focal(f/sensorW)은 건드리지 않고 sensorH만 역산** → 빨간 중심선이 좌우로 틀어지지 않으면서 상하 배율만 교정. + +투영식 `py = 0.5 + cy0 + (Yc/Zc)·(f/sH)` 를 sH에 대해 풀면 ([StationOverlay.tsx:1326-1344](client/src/components/overlay/StationOverlay.tsx#L1326)): +```ts +const cc = toCameraCoords(drone, drag.lat, drag.lon, poiZ, p, wo); +const b = cc.Yc / cc.Zc; +const v = pos.y - 0.5 - (p.cy0 ?? 0); +const sHNew = (b * p.focalLen) / v; // sH = (Yc/Zc)·f / (py − 0.5 − cy0) +setParam('sensorH', Math.max(6, Math.min(36, sHNew))); +``` +영상 세로 중심(v≈0)은 발산 → `|v|<0.03`이면 거부하고 안내. + +### 3.3 핵심 함수 + +- **`toCameraCoords`** ([geoProjection.ts:301-317](client/src/utils/geoProjection.ts#L301)) — GIS점→카메라좌표. 부산물 `distH`(수평 직선거리), `fwd/side`(진행방향 앞/옆 성분, 비등방 거리필터용). `pixelFromCamera`와 분리한 이유: 중심선처럼 선분 단위 근거리 Zc 클리핑·보간 후 투영해야 하기 때문. +- **`pixelFromCamera`** ([geoProjection.ts:126-137](client/src/utils/geoProjection.ts#L126)) — 카메라좌표→정규픽셀(클램프 없음). +- **`groundPointFromPixel`** ([geoProjection.ts:218-251](client/src/utils/geoProjection.ts#L218)) — 화면픽셀→지면 lat/lon. 광선-수평면 교차: + ```ts + const t = relUpTarget / dir[2]; // dir = R_w2cᵀ·((px−0.5−cx0)·sW/f, (py−0.5−cy0)·sH/f, 1) + if (t <= 0) return null; // 카메라 뒤/위 + const E = drEnu[0]+offX + t*dir[0]; const N = drEnu[1]+offY + t*dir[1]; + const [lon, lat] = _toTM.inverse([E, N]); + ``` +- 관련 역투영 2종: `worldFromPixel`(슬랜트거리 유지 lat/lon/z 동시복원, [149-183](client/src/utils/geoProjection.ts#L149)), `solveZForPixelY`(lat/lon 고정, py에 맞는 z만 1차식 역산, [194-211](client/src/utils/geoProjection.ts#L194)). → 발명 C(§7). + +### 3.4 보정값 영속화 (데이터셋별) + +- 카메라/내부표정·표시설정: `ghivideo:calib:{baseName}` (로드 1회, 저장 800ms debounce, [StationOverlay.tsx:473-500](client/src/components/overlay/StationOverlay.tsx#L473)). +- POI 위치/표고 override(DEM·드래그): `ghivideo:poiov:{baseName}` (폴더 파일 override 위에 localStorage 병합, [502-524](client/src/components/overlay/StationOverlay.tsx#L502)). + +### 3.5 설계 트레이드오프·한계 + +- **렌즈 왜곡 미보정**: 핀홀만 사용(방사/접선 왜곡 계수 없음). 분석 보조 라벨 정합엔 표준 정확도로 충분(가장자리 수 px 오차 가능). +- **클라이언트 순수 계산**: proj4만 의존, 서버 왕복 없이 60fps RAF에서 직접 투영. +- **focal 분리 보정**: 한 점 대응으로 sensorH 한 축만 역산(가로 정합 보존). +- **POI 표고 가정**: 실측 표고 없으면 최근접 선로 지면표고(또는 "드론고도−이격") → §4·드래그·DEM 보정. + +--- + +## 4. 측점 표고 정합 (DEM-free 영상정합) — 발명 B + +### 4.1 무엇을 / 왜 + +표시할 시설물(POI)은 주소 기반이라 **표고값이 없는** 경우가 대부분이다. 표고가 없으면 부각(수직각) 산출 불가 → 종래는 지면을 평면으로 가정. 그러나 노선은 실제 기복(정표고 41.6m→57.5m)이 있어 평면 가정 시 라벨이 화면 밖/지면 아래로 이탈한다. 실측에서 잘못된 고도(≈0) 사용 시 **부각오차 42.7°**. + +→ 별도 DEM 취득 없이, **노선을 따라 실측된 측점의 정표고**를 지면고도원으로 삼아 정합한다. + +### 4.2 구성(독립항 필수요소) + +1. **측점 표고 수열 보유** — 노선 따라 실측된 복수 측점의 (lat/lon + 정표고 EL). (실데이터: 측점 47개, 41.6→57.5m) +2. **최근접 측점 지면고도 채택** — 표고 미상 대상에 가장 가까운 측점의 정표고를 지면고도로. 프레임 진행에 따라 최근접 측점이 바뀌어 **지면고도가 노선 기복을 따라 갱신**. +3. **지오이드 datum 정합** — 정표고(EL) + 지오이드고(N) → 타원체고 → 드론 GPS와 동일 기준. `rel_up = (targetZ + N) − droneAlt`. +4. **정합 투영** — §2 파이프라인 수행. +5. (종속) **대상별 표고 오프셋**(poiZOffset) — 건물높이/지반차 보정, 측점·중심선엔 미적용. + +### 4.3 검증값 + +``` +타원체고 = 정표고(EL) + 지오이드고(N≈25.8m, 대전) +# 측점 157K900, frame0, 수평거리 91.3m +정표고≈0 → 부각 42.7° ❌ +정표고 41.5 → 부각 25.1° +정표고+N≈67 → 부각 11.0° ✅ (드론 rel_alt 16.85m와 일치) +``` +- **데이터 함정(핵심)**: `building/01)측점.csv`에 표고 컬럼이 둘 — `Z좌표`(로컬 ≈0) / `Z좌표_한국`(EPSG:5186 정표고 ≈41.5m). 기존 코드가 `Z좌표`(≈0)를 읽어 42.7° 오차 발생 → `Z좌표_한국` 우선으로 교정([geoData.ts 측점 파서](client/src/utils/geoData.ts)). +- **효과**: 부각오차 42.7°→**11.0°**, 구글어스 DEM 대비 **0.2m 일치**(2160p에서 ~6px). DEM 취득비용 불필요. 공개 DEM(SRTM 30m)보다 측량 측점이 더 정밀. + +--- + +## 5. 드론 자세 평활/보간 + 영상 오버레이 렌더링 + +> 두 본질 과제: **(a) 자세/GPS 노이즈**(단순평균은 회전 뭉갬→지연), **(b) 렌더 부드러움 vs 계산비용**(가시성 판정은 비싸고 60fps는 매 프레임 재투영 요구). + +### 5.1 에지(회전경계) 보존 적응형 이동평균 — `smoothFrame` + +[StationOverlay.tsx:577-604](client/src/components/overlay/StationOverlay.tsx#L577). 중심 `i`에서 좌우 최대 `halfWin`(기본 60fr≈2초)까지 평균하되, **중심 yaw와 차이가 `YAW_EDGE=8°` 이내인 인접까지만 창 확장**. +```ts +const YAW_EDGE = 8; +const near = (idx) => Math.abs(((frames[idx].yaw - yc + 540) % 360) - 180) <= YAW_EDGE; +while (lo > loMin && near(lo - 1)) lo--; // 회전경계에서 멈춤 +while (hi < hiMax && near(hi + 1)) hi++; +// yaw는 원형평균: atan2(Σsin, Σcos) +yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI, +``` +- 직선: 창 풀폭 → 노이즈 강하게 억제. 회전 중: 창 좁아져 **즉시 추종**(지연 없음). 회전 후: **한 프레임씩** 점진 재확장(튐 없음). 과거 '변화율 기반 축소'는 회전 종료 시 한꺼번에 재확장돼 회전 프레임이 다시 섞여 1회 튀던 부작용 → 경계보존으로 교체. + +### 5.2 연속 프레임번호 보간 포즈 — `poseAt` + +[StationOverlay.tsx:670-690](client/src/components/overlay/StationOverlay.tsx#L670). 실수 프레임번호 `estFrame`에 대해 양 이웃의 평활포즈를 선형보간(이산 30fps 격자→60fps 연속). yaw는 **최단각** 보간: +```ts +let dy = b.yaw - a.yaw; dy = ((dy + 540) % 360) - 180; // 최단각 +return { ...a, frame: estFrame, lat:L(a.lat,b.lat), ..., yaw: a.yaw + dy*frac }; +``` +O(log n) 이진탐색으로 매 RAF 호출 가능. + +### 5.3 속도적응 EMA + 이상치 거부 — `smoothStep` (One-Euro 사상) + +[StationOverlay.tsx:48-61](client/src/components/overlay/StationOverlay.tsx#L48). **느릴 때(떨림)는 강하게 평활, 빠를 때(실제 팬)는 즉시 추종.** +```ts +if (d > REJECT_DIST && prev.rej < MAX_REJECT_FRAMES) // 0.12 초과 점프 = 이상치 → 위치 유지 + return { x: prev.x, y: prev.y, rej: prev.rej + 1, ... }; +const vx = prev.vx + (dx - prev.vx) * SMOOTH_VEL_BETA; // β=0.25 속도평활(떨림 왕복은 상쇄) +const a = Math.min(maxAlpha, minAlpha + (maxAlpha - minAlpha) * Math.min(1, speed/speedRef)); +return { x: prev.x + dx*a, y: prev.y + dy*a, ... }; +``` +- 이상치 거부에 **연속거부 한계(MAX_REJECT_FRAMES=8)** → 시크/재등장 같은 진짜 큰 이동에 영구 고착 방지. +- 추가로 **픽셀 히스테리시스**(0.75px 이상일 때만 정수좌표 갱신)로 저속 반올림 깜빡임 제거([1048](client/src/components/overlay/StationOverlay.tsx#L1048)). + +### 5.4 라벨 사전계산 + 겹침 억제 + +- **`startLabelPrecompute`** ([StationOverlay.tsx:692-746](client/src/components/overlay/StationOverlay.tsx#L692)): 전 프레임의 **가시집합·겹침해소 결과**를 `Map`에 사전계산. **화면좌표가 아니라 월드좌표(lat/lon/z)를 저장** → RAF가 매 프레임 연속포즈로 재투영. `requestIdleCallback` 200프레임 청크, 취소토큰(`precomputeIdRef`), **현재 프레임부터 순환 처리**(재생 즉시 라벨), 첫 청크 즉시 노출, 재계산 시 옛 맵 복사. +- **겹침 억제** ([755-784](client/src/components/overlay/StationOverlay.tsx#L755)): 임계 가로 `POI_MERGE_X=0.10`/세로 `0.035`(비등방, 텍스트가 가로로 긴 특성). 우선순위 ①철도역/역사 최우선 ②근접거리. **진행방향 변형 우선**: 상/하행 형제 겹침 시 노선 방향((상)/(하)) 변형을 남김. + +### 5.5 RAF 렌더 루프 & 성능 설계 + +매 프레임 **계산 없이 조회+투영+드로잉만**. 라이브 시간은 VideoPlayer `smoothTimeRef`(단조 벽시계 보간, 일시정지·시크·배속 보정)에서 받음. + +| 작업 | 비용 | 배치 | 빈도 | +|------|------|------|------| +| 가시성/겹침 판정 | 높음 | precompute | 1회(+설정변경 debounce), idle 청크 | +| 좌표 투영 | 중간 | RAF | 매 프레임 | +| 평활/EMA/히스테리시스 | 낮음 | RAF | 매 프레임 | +| 팝업 가시집합 setState | 리렌더 | setInterval 150ms | 변화 시만 | + +**핵심 = "무엇을 그릴지"(precompute) ↔ "어디에 그릴지"(RAF) 분리**로 단일스레드 60fps 달성. 중심선/드론궤적은 `buildLines`로 매 프레임 직접 투영(근거리 Zc 클리핑·outcode 컬링). 나침반은 별도 ±3fr 평활 yaw + 360° 누적 언랩. + +--- + +## 6. 측점(체이니지) 시스템 & 스테이션바 + +### 6.1 연속 체이니지 수직투영 (핵심 ①) + +드론 한 프레임의 위경도를 **측점 폴리라인에 수직 투영**해 100m 양자화가 아닌 **10m 해상도 연속 측점값**을 얻는다(부산물: 선로 수직 이격 offsetM). +- **폴리라인 구축** `buildChainLine` ([chainage.ts:22-32](client/src/utils/chainage.ts#L22)): 측점값 오름차순 정렬, 평면투영 `x=lon·k, y=lat·111000` (k=cos(lat0)·111000). +- **점-선분 투영** `projectToChain` ([chainage.ts:35-50](client/src/utils/chainage.ts#L35)): + ```ts + const t = L2===0?0:Math.max(0,Math.min(1, ((px-a.x)*dx + (py-a.y)*dy)/L2)); + const d = (px-cx)**2 + (py-cy)**2; + if (d < bestD) { bestD = d; bestKm = a.km + (b.km-a.km)*t; } // 최근접 선분 위 km 보간 + ``` +- **왜 연속 투영인가**: 최근접 측점점(100m 양자화)은 값이 계단처럼 튀고 두 측점 중간에서 전진/후진 전환 시점이 어긋난다. 수직투영은 선분 보간으로 매 프레임 부드럽게 변해 **방향 전환점(극값) 타이밍이 실제 주행과 일치**. + +### 6.2 이중 측점값: 배지용 `km` vs 방향용 `chain` + +프레임별 precompute([StationBar.tsx:233-246](client/src/stationbar/StationBar.tsx#L233)): `km`=최근접 측점점(100m, 배지/폴백), `chain`=연속투영(10m, 방향/축/탐지). `videoFps`는 실데이터(마지막 프레임/재생시간)에서 유도, 미비 시 `30000/1001` 폴백. + +### 6.3 축 설계: 시간축이 아닌 위치축 (핵심 ②) + +선로 점검 영상은 **호버(정지비행)** 와 **전·후진 반복** 특성이 있어 시간축이 부적합하다(호버 시 커서가 위치와 어긋남). +- **이동거리축** ([StationBar.tsx:250-271](client/src/stationbar/StationBar.tsx#L250)): 연속 측점값을 ±8fr 평활 후 프레임간 **절대 변화량 누적**(`cum += |sm[i]−sm[i−1]|`)을 정규화(`frac`, 단조 비감소). 호버=frac 정체(커서 정지), 이동=frac 증가. 방향은 색 리본으로 직교 분리. +- **측점축**(후속 채택안, [history 2026-06-26_1520](docs/history/2026-06-26_1520_스테이션바-측점축-전환.md)): x축을 **측점값 비례**로 두고, 축 범위를 역 POI 첫/끝으로 **노선 고정**(영상 무관 절대 측점) → 어떤 데이터로 재생하든 같은 구조물이 같은 위치/측점에 표시(회차 간 정렬). +- 시간↔축 양방향 변환: `cumFracAtTime`/`timeAtFrac`(또는 `chainAtTime`/`pxAtChain`)를 이진탐색+보간으로. 커서·구간색·구조물 마커가 **동일 매핑 공유**. 라이브 커서는 React 리렌더 없이 RAF에서 CSS 변수(`--pos-px`)만 갱신([771-786](client/src/stationbar/StationBar.tsx#L771)). + +### 6.4 방향 판정 & 방향색 트랙 + +평활 측점값 추세를 극값추적 + **히스테리시스 HYST=10m**로 방향 전환점 확정([395-458](client/src/stationbar/StationBar.tsx#L395)). `forwardDir = arrChain≥depChain?1:−1`. 정방향=주황 음영, 역방향=청록. 이동거리/측점축은 구간 px가 단조라 `linear-gradient` 1장으로 경량 처리([686-726](client/src/stationbar/StationBar.tsx#L686)). + +### 6.5 구조물 마커 — 좌표/측점 이중 탐지 & `kmExists` (핵심 ③) + +두 통과 탐지기: +- **`pxPassesTo(lat,lon,off)`** ([489-534](client/src/stationbar/StationBar.tsx#L489)) — 좌표 근접. 임계 `TH=max(100, gmin·3)`. +- **`pxPassesAtMileage(targetM)`** ([537-559](client/src/stationbar/StationBar.tsx#L537)) — 연속 체이니지가 `TH=stationTolerance(기본 20m)` 이내. + +탐지 우선순위(단락평가): ①명시 측점값 ②좌표를 측점선 투영→측점기준(통과순서 정확) ③이름매칭 POI 투영 ④좌표근접 폴백 … ([612-639](client/src/stationbar/StationBar.tsx#L612)). + +**역사 합집합 보강**: 종점/조차장은 측점선 투영이 불안정해 재진입을 놓침(예 대전조차장 162k080). 단락평가 탓에 한 통과를 찾으면 좌표근접이 안 돌아 누락 → **역사에 한해 좌표근접 통과를 합집합으로 합치되 24px 임계 중복제거**([629-634](client/src/stationbar/StationBar.tsx#L629)). + +**`kmExists` (★)**: 좌표 반경만으로 잡힌 통과는 실제 측점값이 그 위치에 없을 수 있다. +```ts +const kmExists = stationKmVal == null || Math.abs(p.km - stationKmVal) <= existTol; // existTol = stationTolerance +``` +([StationBar.tsx:648](client/src/stationbar/StationBar.tsx#L648)) `false`면 **측점값 라벨·측점검색에서 제외하되 마커(동그라미)는 유지** — "위치 표시"와 "측점 단정"의 분리. + +**종점 미도착**: `endGapPx=min(폭·0.15, (gapM/lenM)·폭)`, `placeUnreachedTerminal`이 최우측 역사를 트랙 끝에 미도착(속 빈 링)으로 고정([303-308](client/src/stationbar/StationBar.tsx#L303), [574-586](client/src/stationbar/StationBar.tsx#L574)). + +### 6.6 측점검색 순환 — `handleJumpToMileage` + +[StationBar.tsx:850-916](client/src/stationbar/StationBar.tsx#L850). 측점값 입력 시 여러 통과를 **통과방향 순환**: +1. 후보 **1순위**: 화면 마커(`structureMarks`) 중 `kmExists!==false` 이고 표시 10m값 일치 → px→`timeAtFrac` 역변환. (좌표근접 통과 포함 → 모든 동그라미 도달) +2. **2순위**: 마커 없으면 chain 연속구간, 최종 전역 최근접 폴백. +3. 정렬 + 0.3s 중복제거 → baseT 기준 다음 통과(끝→처음 순환). `jumpRef`로 연속검색 판정. Enter 후 입력값 유지, blur 시 삭제([PlaybackControls.tsx](client/src/stationbar/components/PlaybackControls/PlaybackControls.tsx)). + +### 6.7 타임라인 보조 기술 ([Timeline.tsx](client/src/stationbar/components/Timeline/Timeline.tsx)) + +- **동일항목 브래킷 클러스터**: 동명 다통과를 거리 무관 한 묶음으로(드롭+수평 점선) 중앙 라벨 1개([143-172](client/src/stationbar/components/Timeline/Timeline.tsx#L143)). +- **라벨 2줄 균등분할** `splitStructLabel`: CJK 1.0/기타 0.55폭, 괄호그룹은 통째로 아랫줄([30-49](client/src/stationbar/components/Timeline/Timeline.tsx#L30)). +- **측점값 라벨 겹침 숨김** `showMileage`: 직전 표시와 `(lastW+w)/2` 미만이면 숨김, `kmExists===false`는 애초 생략([117-137](client/src/stationbar/components/Timeline/Timeline.tsx#L117)). +- **아이콘 passed 전환**: upcoming/passed 두 PNG를 opacity로 전환(무깜빡임), 전 상태 사전로드. + +--- + +## 7. 위치보정 역투영 (단일 제스처) — 발명 C + +### 7.1 무엇을 / 왜 + +지오코딩 POI는 ±수십 m 오차. 2D 화면 이동만으로는 카메라로부터의 거리(깊이)를 결정 못함(깊이 모호성). 종래는 수평/높이를 따로 조정하거나 깊이맵(LiDAR) 요구. → **단일 드래그로 수평·수직(3D) 동시 보정, 깊이맵 불요.** + +### 7.2 구성(독립항 필수요소) + +1. **슬랜트거리 보존** — 드래그 개시 시점의 카메라–객체 슬랜트거리(range)를 고정 → 깊이 모호성 해소 핵심 구속. +2. **역투영** — 드롭 픽셀을 카메라 방향벡터로 환산 후 보존 거리만큼 투사. +3. **월드좌표 복원** — 자세 회전·평행이동 역변환으로 lat/lon/z 복원(지오이드 역환원). 한 번의 드래그로 수평·수직 동시. +4. **영속·재적용** — 복원 좌표를 객체 식별자(title)에 연계해 `_poi_overrides.json` + `ghivideo:poiov:` 저장. +5. (종속) **가시영역 한정** — 전방(Zc>0)만 보정. + +```ts +dir_cam ∝ ((px−0.5−cx0)·sW/f, (py−0.5−cy0)·sH/f, 1) +cam = dir_cam/|dir_cam| · range // ★거리 보존(드래그 시작값) +rel = R_w2cᵀ · cam // 회전 역변환 +z = droneAlt + offZ + rel.up − N_geoid // 정표고 복원 +(lat,lon) = TM⁻¹(droneE+offX+rel.x, droneN+offY+rel.y) +``` +**변형 — z 전용 역산** `solveZForPixelY`: lat/lon 고정, 드롭 세로위치로 z만 1차식 역산. 왕복검증 z=42→py→42.000. + +### 7.3 효과 + +2D 드래그 1회로 3D 보정(단계 절반), 역투영 왕복오차 ≈0, 깊이맵 불요, 파일+localStorage 재현성. **"depth map 없이 거리보존"** 을 명시해 US9188444B2와 구별(§10). + +--- + +## 8. 백엔드 스트리밍/FFmpeg · 데이터 파이프라인 · 상태관리 · 플레이어 + +### 8.1 대용량(2GB+) 재생 — 이중 경로 + +- **로컬**([useVideoPlayer.ts:80-88](client/src/hooks/useVideoPlayer.ts#L80)): `URL.createObjectURL` Blob URL 직접 재생(업로드/변환 없음), 교체 시 `revokeObjectURL`(누수 방지). **현 운영 주 경로.** +- **서버**: Range Request 즉시 재생, 비표준 코덱(HEVC 등)이면 HLS 자동 전환([15-17](client/src/hooks/useVideoPlayer.ts#L15)). (서버/HLS 경로는 보존되나 UI는 `SHOW_TOOLBAR=false`로 숨김.) + +### 8.2 Range Request ([streaming.ts:52-77](server/src/services/streaming.ts#L52)) + +핵심: 한 응답에 전체를 싣지 않음. 끝값 없으면 `start+10MB−1`. `pipeline()`(백프레셔/에러 자동), `highWaterMark:1MB`, `Accept-Ranges:bytes`, path traversal 방어(`resolveVideoPath`). JS Number(2^53−1)로 4GB+ 안전(BigInt 불요). 멀티레인지 미지원. + +### 8.3 HLS 변환 + SSE ([hls.ts](server/src/routes/hls.ts)) + +멱등(디스크에 `index.m3u8` 있으면 즉시 done), 코덱 분기(H.264 `-c copy` 수 초 / 그 외 libx264), `/progress` SSE로 stderr `time=` 파싱 푸시. hls.js 필수: `backBufferLength:30`(기본 Infinity면 장시간 재생 메모리 누수→크래시), maxBufferLength 30, enableWorker([useVideoPlayer.ts:7-13](client/src/hooks/useVideoPlayer.ts#L7)). + +### 8.4 FFmpeg spawn 래퍼 ([ffmpeg.ts](server/src/services/ffmpeg.ts)) + +`-y` auto, stderr `time=HH:MM:SS.cc` 정규식 진행률, exit≠0 시 stderr 끝 500자 reject. 프레임 추출은 `-accurate_seek -ss {t} -i {f} -frames:v 1`(플래그 순서 = 입력단 정확 시킹, [frame.ts:37-44](server/src/routes/frame.ts#L37)). + +### 8.5 데이터 파이프라인 ([geoData.ts](client/src/utils/geoData.ts)) + +``/드래그로 받은 `File[]`을 병렬 파싱(`Promise.all`). **인코딩 자동감지**(BOM→UTF-8/EUC-KR), **헤더명 우선+위치 폴백**(`makeFieldIndexer`), 좌표 유효성 게이트(lat 33~39/lon 124~132), KMZ 우선(있으면 CSV POI/구조물 미사용, fflate unzipSync). + +| 소스 | 파서 | 대상 | 매핑 | +|------|------|------|------| +| `.csv` | parseDroneFrames | DroneFrame[] | frame_cnt, lat/lon/alt, yaw/pitch/roll, focal | +| `building/01)측점.csv` | parseStations | station + DirectionChange | 측점→title, **Z좌표_한국(정표고) 우선**, 비고→방향전환 | +| `_POI.csv` / `02)지장물.csv` | parsePois | poi | category_clean/명칭, lat/lon | +| `02)지장물_역사.csv` | parseHistoricStations | 역사 구조물 | 명칭→name(스테이션바 마커) | +| `03)교량/04)터널/06)구교.csv` | parseStructures | bridge/tunnel | 구분/시설물명, 연장→lengthM, 시설종별→grade | +| `05)출입문번호.csv` | parseAccessDoors | 출입문 poi | 출입문번호→title | +| `*.kmz/kml` | parseKmz | poi+구조물 | Placemark + description표→props | +| `route.json` | mergeStructures | 보정 | station/offset/이정 override | + +중심선은 v2.0에 별도 파일이 없어 측점을 측점값 순 정렬한 폴리라인으로 합성. + +### 8.6 상태관리 + +- **geoStore**(메모리, persist 없음): frames/stations/pois/structures/centerline/origin/routeMeta. `basePois`(원본) vs `pois`(override 적용) 분리로 원본 불변성([geoStore.ts:96-110](client/src/store/geoStore.ts#L96)). +- **settingsStore**(localStorage `ghivideo.settings`): 시설종별 필터, 표시 토글들. 커스텀 `merge`로 스키마 진화 대비. `isGradeVisible` 공유 규칙. + +### 8.7 플레이어 통합 & 프레임 정확도 + +- Video.js `data-vjs-player` + Strict Mode 가드, `controls:false`(측점바로 대체), `fill:true`. +- **FPS 감지**([useFrameStep.ts:21-49](client/src/hooks/useFrameStep.ts#L21)): `requestVideoFrameCallback`, 첫 콜백은 시작점만 기록(31fps 오감지 방지), 연속 2회 일치 확정. 스텝은 `1/fps`(누적 금지=드리프트 방지). +- **이원 fps**: 스텝 이동=실측 fps, 프레임번호 표시=29.97 고정(SRT 기준). 의도된 분리. +- **매끄러운 커서**: rAF 단조 벽시계 보간 `smoothTimeRef`(매 프레임, transform) + `smoothTime`(throttle, 배지) 분리. + +--- + +## 9. 개발 타임라인 (주요 기술 의사결정) + +| 시기 | 핵심 결정/구현 | +|------|----------------| +| 2026-04-01 | labelMap 사전계산 + requestIdleCallback + RAF, ±10fr 이동평균(yaw sin/cos), 30→60fps 보간 | +| 2026-04-02 | **투영 파이프라인 정립**: EPSG:5186 TM, 핀홀(Rz·Rx·Ry), 타원체고=정표고+지오이드, Cohen-Sutherland 클리핑 | +| 2026-06-19 | **v1→v2 데이터 형상 전환**(실측 폴더), 인코딩 자동감지, 측점 비고→방향전환 추출, 시설종별 필터 | +| 2026-06-22 | **측점 표고 컬럼 교정(Z좌표_한국) → 부각 42.7°→11°**(발명 B). 역투영 드래그 보정+override(발명 C 최초). title-키 보간+이상치 거부. 비등방 거리필터 | +| 2026-06-23 | 에지보존 평활(YAW_EDGE=8°). **글로벌+KIPRIS 선행기술 조사** → B·C 신규성 확인 | +| 2026-06-24 | **1클릭 FOV 보정(→sensorH 역산)**, object-fit:cover 정렬, 라벨 월드좌표 저장+RAF 재투영, One-Euro EMA, KMZ 직접 로딩 | +| 2026-06-25~26 | 종점역 누락 수정, 양끝 역 고정, 라벨 2줄분할, **동일항목 브래킷**, **거리축→측점축 전환** | +| 2026-06-29 | **좌표/측점 이중탐지+합집합 보강**, 측점값 라벨 겹침숨김, **측점검색 순환+마커기준 전환+kmExists**, UI 통합(토글 재생바 이동, 활성색 amber), 보고서 3종 생성, 55000 외부접속 | + +상세 근거: [docs/history/2026-06-22_POI투영오차-개선.md](docs/history/2026-06-22_POI투영오차-개선.md)(발명 B·C 1차 출처), [2026-06-26_1520_측점축-전환](docs/history/2026-06-26_1520_스테이션바-측점축-전환.md), [2026-06-29_1029_역사-통과-좌표근접-보강](docs/history/2026-06-29_1029_역사-통과-좌표근접-보강.md) 등. + +--- + +## 10. 특허 관점 종합 + +> 출처: [docs/특허_스테이션기반_주행영상_플레이어.html](docs/특허_스테이션기반_주행영상_플레이어.html)(기술발굴), [docs/발명신고서_측점표고정합_역투영보정.html](docs/발명신고서_측점표고정합_역투영보정.html)(변리사 제출용). + +### 10.1 기술요소 인벤토리 (E1~E9) + +| 코드 | 요소 | 특허 적합성 | +|------|------|------------| +| E1 | 측점(체이니지) 기반 영상 색인·탐색 | 후보 A(좁게) | +| E2 | 지리객체 실시간 영상 정합(labelMap 사전계산) | 공지(큰 틀) | +| E3 | **측점 표고 기반 DEM-free 지면고도 추정** | **후보 B(상)** | +| E4 | **역투영+슬랜트거리 보존 단일드래그 위치보정** | **후보 C(상)** | +| E5 | 비등방 거리필터+소실점 클러터 억제 | 후보 D(종속) | +| E6 | 노이즈 강건 오버레이(이중평활·title짝·이상치거부) | 후보 E(종속) | +| E7 | 측점 폴리라인→중심선 | 제외(통상 처리) | +| E8 | 이종 CSV 융합(인코딩 자동감지) | 제외 | +| E9 | 비고 텍스트 방향전환 추출 | 제외 | + +### 10.2 출원 권고 우선순위 + +- **1순위: 발명 B + C 결합** — 신규성/진보성 우수, 글로벌+한국 신규성 확인. +- **2순위: 후보 A(좁게)** — LRS 비선형 재좌표화 + 회차간 위치앵커 + 다중회차 동기. 한국 선행기술(KR102268318B1·KR100411587B1) 위험 → 좁게 청구. **본 명세의 측점축 노선좌표 고정(§6.3)·측점검색 순환(§6.6)·이중탐지 kmExists(§6.5)가 A의 구현 근거.** +- **3순위: D·E 종속항 흡수**. **제외: E7·E8·E9**. + +### 10.3 청구 포인트 핵심 (B·C) + +- **발명 B 독립항**: ①측점 실측 정표고 수열 → ②표고미상 대상에 최근접 측점 정표고를 지면고도 채택(노선기복 반영) → ③지오이드고 가산으로 타원체고 변환·datum 정합 → ④정합 투영. **※ ②+③ 결합을 한 청구항에**(개별요소는 공지). +- **발명 C 독립항**: ①드래그 개시 시 슬랜트거리 보존 → ②드롭픽셀 역투영 → ③단일 제스처로 수평·수직 동시 복원 → ④식별자 연계 영속. **※ "depth map 없이 거리보존"** 명시(US9188444B2 구별). + +### 10.4 선행기술 (조사 완료분) + +- E1·E2 큰 틀은 공지: Esri ArcGIS Pro FMV, US9996976B2, Mandli Roadview, Remote GeoSystems LineVision, ENSCO Virtual Track Walk. +- B·C는 글로벌+한국 신규성 확인. **단, KIPRIS 전문(專門)검색 미수행 — 출원 전 필수.** + +### 10.5 신규성 후보 종합 (구현 기반) + +1. 드론 SRT pose 기반 **실시간 GIS-영상 정합 파이프라인**(타원체고/정표고 datum 자동 정합 포함). +2. **단일 대응점 sensorH 역산** 인터랙티브 FOV 보정(가로 정합 보존, 중심부 발산 거부). +3. **깊이 모호성 분리형 역투영 3종**(슬랜트유지/지면교차/표고 1차역산)을 보정 의도별 선택. +4. **에지(회전경계) 보존 적응형 평활** + 속도적응 EMA(연속거부 한계). +5. **월드좌표 precompute + 연속포즈 재투영 2단 분리**로 단일스레드 60fps AR. +6. **누적 이동거리/측점 비례 축 + 방향색 직교 분리** 진행바, 노선좌표 고정(회차 정렬). +7. **좌표/측점 이중 탐지 + 합집합 보강 + kmExists 신뢰도 플래그**("위치 표시"와 "측점 단정" 분리). + +--- + +## 11. 검증 결과표 + +| 항목 | 개선 전 | 개선 후 | 근거 | +|------|---------|---------|------| +| 부각 오차(측점 157K900, 91.3m) | 42.7° | **11.0°** | §4.3 | +| 지면고도 정확도(측점 vs 구글 DEM) | — | **0.2m(~6px@2160p)** | §4.3 | +| 역투영 왕복오차(z=42→py→z) | — | **≈0 (42.000)** | §7.2 | +| 화면 EMA 지연 | 1.65s(α=0.01) | ~25ms(α=0.4) | §5.3 / history 06-22 | +| 렌더 | 끊김 | **60fps**(precompute+RAF 분리) | §5.5 | +| 역 재진입 마커 | 누락 | 좌표근접 합집합 보강으로 표출 | §6.5 | + +지오이드고 N: 본 명세 **25.8m**(KNGeoid 검증) 통일. 초기 문서값 25.449m는 각주 처리. + +--- + +## 12. 후임자 인계 + +### 12.1 데이터 흐름 한눈에 + +geoStore(frames/stations/structures/routeMeta) → `useEffect` precompute(`viewedRef`=프레임별 km/chain/time, `chainRef`=time/frac/chain) → `pxAtTime`/`pxAtChain`·`barSegments`·`structureMarks` → Timeline 렌더. **모든 px는 단일 매핑(`pxAtTime`/`pxAtChain`)을 통과**, 역변환은 `timeAtFrac`/`chainAtTime`만 사용. + +### 12.2 튜닝 포인트 + +- **폴더 route.json**: `stationTolerance`(통과/검색 허용오차, 20m), `direction`/`directionChanges`(방향 변형 필터), `endStationGapMeters`/`lengthKm`(미도착 폭), `startStationName`/`endStationName`. +- **카메라(패널, 데이터셋별 저장)**: `focalLen`, `sensorW`, `sensorH`(FOV), `yawOffset`/`pitch`/`roll`, `geoidOffset`(지역 N), `poiZOffset`. +- **코드 상수**: 평활 `W=8`, `YAW_EDGE=8°`, `HYST=10m`, 좌표통과 `TH=max(100,gmin·3)`, 역사 합집합 `24px`, 검색 중복 `0.3s`, EMA `β=0.25`/`REJECT_DIST=0.12`/`MAX_REJECT_FRAMES=8`. + +### 12.3 빌드/배포 (★ 필수 숙지) + +55000은 **PM2 `ghiVideo`가 `client/dist`(빌드 결과)를 정적 서빙**한다. 소스만 고치면 화면에 반영 안 됨. +```bash +export PATH="$HOME/.nvm/versions/node/v20.20.2/bin:$PATH" # 시스템 node v12 → tsc/vite 실패 +npm run build -w client # = tsc && vite build → client/dist 갱신 +# 브라우저 Ctrl+Shift+R, 폴더 데이터는 런타임 로드라 폴더 재선택 필요 +``` +외부(LAN) 접속: netsh portproxy(Windows :55000 → WSL IP:55000) + 방화벽. WSL IP는 재부팅 시 변동. + +### 12.4 업그레이드 포인트 + +1. 렌즈 왜곡(Brown-Conrady) 추가 → 가장자리 정합. +2. 다점 대응 자동 캘리브레이션(focal·주점·왜곡 번들조정). +3. 지오이드 격자(KNGeoid18) 보간으로 `geoidOffset` 자동화. +4. POI 표고 DEM 자동조회(현 부분지원). +5. 중심선 per-frame 투영 공간색인, precompute Web Worker화. +6. frame.ts fps 고정(30)→`getVideoFps`, HLS jobs DB 큐, 멀티 Range 지원. +7. **코드 통합**: 측점 파서/포매터(chainage.ts ↔ StationBar.tsx), 투영함수(`projectToChain` ↔ `projectChainage`) 단일화. + +--- + +## 13. 한계 / 출원 전 확인사항 + +- **KIPRIS 전문검색 미수행** — 출원 전 필수. +- 지오코딩 수평오차는 데이터 한계(코드 보정은 드래그 수동). +- `geoidOffset` 지역 상수(대전 25.8m) — 타 지역 재튜닝. +- VFR/abs_alt 노이즈, 먼 거리 역투영 민감. +- 미확정 항목(특허문서 `[확인 필요]` 유지): 다중회차 동기화 구현 범위, 위치앵커 주석 데이터모델, 권리귀속, 공개이력. +- 렌즈 왜곡 미보정(핀홀만) — 가장자리 정확도 트레이드오프. + +--- + +*본 문서는 5개 서브시스템(카메라 투영/자세 평활/측점·스테이션바/백엔드·데이터/기존 특허문서) 정밀 조사 결과를 종합한 것이다. 각 소스 참조는 클릭 가능한 파일:라인으로 추적 가능하며, 특허 출원 시 본 문서의 §3~§7·§10과 history 원문을 함께 변리사에게 제공할 것을 권장한다.* diff --git a/docs/기술명세_GhiVideo_종합기술문서.pdf b/docs/기술명세_GhiVideo_종합기술문서.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c845cea6a60b3a54b9d0025fabf80b0686171dd7 GIT binary patch literal 235691 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�Y1VUf7te@#CrHa_=Ep;^qPkxFj4TlKVmL6gESu8`~5I z&800KKDMaP5~?WE^~n%Lln|^AEAWPE7hdKA3D`|heiAr{D%r(bjH}0v$M*boa(WJ3 z8bki2+H&S7-o2tGiQnp!?LY95U&9t`2w%E%l#nJWg$vcCZa+qsn z))vOPght+6i_*~fqWS5xHOl&k`~tN>TaI&}D@b)U+nuZ{T2g}J(T63@p~3p}>ybzE zQoqKn^!(LZwSgX_Xi?4t-K`@}gs!{jR@gIu&Ejej&oc({NLzC|zi&L-kE{#eY(Xwp zdu~lP#5_n$#(tY8$xwve)tsuUx~Mh2h&7(>+y3Y#z7JY``efC#D7l%OH;8lKAE{Wt z`LlMfG>lql!U#O7(uu1k@UAAajITx3C+z3nCY4jJ_UWoQx(C8xfA~^INeEAsXd%~W zE6t{ZXCKh5pjZ{WN!ZEE6B-5Vt)NA-eD}?FSmnMK18B|_@};s}5A^j4q9kbY6jTRY zLkl0RO)m1B&fdg`6w7Hz%%{f*C|o%lo(b`4JI1o@Cpd3}cK!HG{dil)1CXz)8q9rF zcCIiCoDHUWQEG@jh#-tuWRA42yNNbqUhf!x^hU%tanlUQv4SGRD3VyWaMK+&0x6;xuaiGX!b zi9*S}2juaBlNzS)HA6dCr*YOu6?2Gj_l@30Ugd8=o7$(G2BfZw%OsW+BlAAH1;0F* zbzMc|*dmT;wlo|jeCD98bTt2IL_o=sWM0OrLcxO(Q|>H6uM1RbXepEK@wNJ@Ti^^e zd!H(BwWcVy8YJ4XO;)(5fz58r^?(>=aEx}YzBc9ldor!13V5|2JOrxL!n=mqpmJt? zIfufWUX_1V!L2%dht1~qdYRSpaj>Q$=&CoJZBGpp$WJnXtlC_GJ84bb$kJD#hlIDeYa_>5E?!?T))BiDKP-0S?sancon zcQ+d)ct#tBudLi%mO>0)^}B_7&wZ(&<5>t=#e%3i^@c?=$A{weQK8@mm>hJamhTcV z9KCHkJNZRvPzFyEMklA+RO^$L)UJCUiX@*~=XNr7b#IR+z?!Y z+cyi%w8k>@3n$c;=G+>KJF0i8$TbR~D=mZqy#l=U@`zsskPV2BOvp3a-edW~_f8Ku z8=Qf^g=O5N9o()&SU}$3c`WGi)(P7A#|!kT^YYtHQdCtTO!l2v*sx~%Eabhk+ubZ} zt?YaGzn$fA=^r2Z)_oK3Jp>Wz7zeoBKh}a~zQz2370-qn2Z4d(ow$8Qe?kWw2F+B` zEjQry(^D+?yGZhME9;pDC1w)CJfO$AZq1;p)UTG_tFXXuFTzplt+#Y9L&j+ZcG&sU zBj9Z@*wC~QJRO6H*4_32Zf=Nyr7`Ta%LFNgMg4{YuP~?1vQ$ff)QSA8TV}90iqQE&cQn>e#mq z*m#-d&|<~BN^|)fh?O(U8*zs^&f3KhdDYnue?t(t$`z`{=_&*(h5sm0e~bGQ zChYgd@?mY00_s_tpQ;-F{2h}!f561MHhWa$?!9eUj9Pi$UK`kex4_PF?{5r#f z6a}!|0E)1ZkksopV}$8_?k3UuyJ@mCS*;x~Nq(OTJNVNCV5R1_3v7Si-*&Q+a0YL# z{;qa6DM47Sh1(6?@D+i9e@hI(ifZ_sA8rlST;Csciv zHIl$XW8`czOZ^%2;0#$(*BsOUGsX&RU_3@Hp| zRM_MDj=6D_aHFQ5D32KeC!iqwtPhYbC;*v;PZQRat;xLkcDMNV4zsn7&e4w- z7zqZ$?=LSz@;4)uxM4&zpO>M7xFRTO9`33#3j`=&EI+g7lkUvCCW$5dNXP^Jiqx@g z?6gx5D=Rul&WvJFH3uR^a`?V~UQgILi6#L3@adsH8zf(hu7$)yeK^>IJ?>|IEELg& z;VU()cHHTLA26-&asKEa;Q%=USv#6iM?a|ucrpsXc3f(g!QM>nM}tQkARtC{V3)9v zQ3KtUju!vMHseUBNuoT^w&hK@|O`@1%_N1jR_Z2C_OSdxHE=ZhUcjO!aKU zTK_&>Jod9ArP+Dd#Igiwk6RL(mu_n(<`kK=?dtu#k4vW9>?I_fzYj?Mnc&AXID(3$ zt;qVY)V{kwzjmaB0yQ(DA@(dhy(=WWbPa@Fz5;oX$j0~RO_1nIkUcd=N@r{xv-2Wc z{d;)*^iItwQ|s8(`-giL`^4wn7`}XIY8#rz9%Qd}p>hj(<_S zHm|9*pS&*a%E6q1gvS3~m>BlsP{)S2jhNB?WCq!8TtC=VzAcpo4gbDO!z3r~Kbiif znqDGHO_|)VvbQ_{Dp63Kmb6!17by95NP<^3QgC9!6X0{I~XxSOpnofBnt&ihf>3WZ`%Ia zYUFVlF0P}=o;-ma8)J68gDhDl9$AtaF3QXye0Ead*efn2TQU`KpsVwv$fXg?7PJd`sXEj zCT&K$hb)KFD-%Zq%o4mOxsqV*nA3=y<$Y6T^W5X)V8N5u=x)Bb=gPY9+G7L28#110Q9gvac-A!yA{p?5~l+cXDpx)MtC)ve-X`o9o`-XQg+pd zZXeJJ%kC+(3L0D8n0VW|o+nJ}a7dgg}+ z6h*5dkUAWg3MI!QeQnL!y2iK2n-3Ugl3p6q`MBx{TsAZ}l`;;$OAM|5CfK=+^^TEs z6aE#g8k|&SRK_9jck*5){x?jBdU;U^hVv6A3sIt11GTh;cu0nLru8K@RPH@q<+1#M z(izj_oiB$zGpYDYMBahh2FZG>j-3^o(ve^RGCiZuVgxdz!xsP0bU5fXxE zBaXttBE+lr+bY0-|3w&1vJUv5>g5)B+HLx7s5B~=&s!)@8u%bK3~-|PGNBE_-n zj2)wYXH`yXR0^6lz$VcCGSZmz*4IjM#FQy7>hzK6e0pwii$3z;54R~;Q*B*%7M(MH z>&52LcvPIGcJ>lIp|tkd9mnT#H*aNkQwpqc59f0UJ;vQOhsR+WOvy%Ti4`qgi+Am< z=jt`Mb(UrjDw+r%+V~#31rmD|u&3%MiR9v(Q=jD8tMG4tB~M=cS8>9vxutKPAz;@+ z?-E*5sMZTRGVhcRsVLONH*oJ3^~2IbYATwjo2ykLE+**lmDb7 zpOV=XEs)`qCWctdgYqYBU|QZ8qZBIISr<`49>RUoue#mdYs}r&paen?{g(cunK?Dx z*<7|OLS;0@sYvf*`82=xVRrBpJo#*wP7c3S`z$xC5C=hC| z)Q!ir9c=PiSdV#ciNF&PSLU!p4mcB75(+c)rG?BP`l9QN)oVU9ZA4$&CaicWG92Odo;lxaV0NVUFNZlhqH2&3&M_KfMqc7G+c=$j{z+CtIXL1b$K&5uEH zwaC{Nfc)&Q6c3kTZV2GXx{1`LemWDAs+IK$cdn@V?{+Bu<|`)>BBUkfr^39p7 zm&#v2(zJ3-i;?2_sVa?Y`O$@?a-i;L{7{OcwCYsF(7WpG1WyFVqg4)*Gu1qWrR(ZE z3~PL}r%;eLqM-@GzkFAbBH#sU5Q>P4hb*Np0b-4LaOseS6%rF0%Y5a-OM4Ic_Cqi_ zuU%6xY+&%`A12dBQ=PkPk9Mo(2Fwki7bhKDLmMAFefDc#L~c05pA_hFDL8rxzqzyg zDJPK>@|Qs3-Je16NNpfg`{d9wOnhdf0gWVC(5Mv!$l6;3P<+Mrl|ti@(CismuBoLo2u_;03iz2>-@Q-hj*3^b4pT=oFG1Vi zVp#9bXf&~TI@&g12uh2O)j{c97?By`P@5eWv~T+P*CftuqK~|Cuzx9!m=|nWUW@&5 zjoQ}O`W5J;na$d4)peFjz}ttQaH{_<_|`=d$PQf1nO)>;uRkniSVw}I=U7XF1FW6|j5WAHB-dZ=)&U1FLB+$aWPSS1RQ9HZK zXdbHuQ_SQRSv{z4YKfr2NHt(|>2+C({e=R3Bb@sb>AAJ}R(#3@W7MTh+BXh)BJikk z>trUR(6RdBHuH)GffS5gXMoz);t>^)j&y=P3UOyk&LekVPAy71Xu3vGt_PhJpW@?t zW#vr zmPfw%$bN$_=+@)ws-k!4en1Fo_f)9f>f@a4;R!roibD7N$!4yO_CtEX zrubfkQ;N|#YMW-BohlD1i{O1mDK8F>lNCM5FBQKSLvC!38U3uos~bdx3U9|g+4lAI zpMV37%(LGh*TSN128gSJO^$9uYm8~JXWFX=x{+|F@ufPrUAaTGbBQ{07^^=PvfTxv z8-`flJT4jPwS7n5?0*Xg)Z>wQi-sk>{rq8T#1kdH%6)(wij#8wKyL;~VuOCqu*$Aa zhk?o?V&{yLJU8t5%a0?U{b$!#VaP6xEHN$)sQT0jMpsVzwk37#>M!Hg)R9TQTw>C! zC@Ic^d8t;SbTXIBFF+fCq6nrfSo1$((bD&d%DRX3ssA z`mh@Ja~=lMaOAaKi(5=~Pf|!-4*T;^0BGzwNbG*ELi6|1@#70J`*V1E8pv48K}p%D zG5czb5ZSXx92CRRC{H?LQ$-k$hxzUMg%C{+~%Q;Tm`(JPO5>6B9|r)~6< z3gNt7j0??XBH|@eo#7MYJZ<9ODJIGqr%?xv-Kqy>3RQZ%CF4u&OgnzVT!uJ?*Xbu{ zSI4nGXJ<-FKm*NWs|9-x5nFU8z0u1lfb7j)sUuU)>heA9T;b%L?*^5B2XMX}^7nL> z94qK8l!{iT#j<$hT{qs{Q#9SXa!lkF#^BABOL@Rp-ASAIYKLqtdWJ5FaH%6_&r|2v zc5RL1kr6M?NkHV}mLfYZ2@(zT&}MK_azggO^QGd7UJTE^K==^_<`gY7el&~a%3b!O zdNI3YX04q^AgoS>%%r4}M@8i|-XI;$p)pn*kJ;II4$wXJEfxk1A)L68UdTN(>#;Z= zX~A4o%;m^^@2-?}<2}Bw;oNwE8h<-=-WtHYs+{wQW9a;iJkp|oMY0XYdGV-W*PTbR zrBwT-sehs|@-%-osVZPu*i)(dR39^M=_A*5S>0E@)uC#w8k=knylrBu9EMlHcF$dg z>H}I>IkQz(%nhx3svcCcc~;u6JRtl937)#x`M;eNtjzyU&I&eqM*9CVVS4N9bgTs5 zydJ|`P~pQX{9eOvTu?Y9-C!D522O6RVX5t0bQhr6UafwAD<~pVlFMu*8}~NC>KPu- zTTr4@-bV}*NF~1tF+V*aeIiGn3x;oI9POl)>g5R^*}Sg3O9dYJb%p1Ac`Tl)3NHZ- zM9)Whxq&CqYQy62ThPmh+VlO_Q$1d<_~rsovIW-*bbnuu=G@3vB98n%?Yb;WVVT!N zck*}sw$En`K}*m$e{9TcNi9Uj=%DmbXp{|aYk@!pMrmU3R-y$>|7218s>H1N&t*}N zsnG7ks?w)fZBuGpz9}lehMKKxe9(2VKZ*lF)wi2hP~F38aAruaiS<4tT+pofQrEE9 zpk{8mA`c^aZ4grT@ALYu5s>5Y>Fo1biy$Zt$@krt z=3T7E8}YPA1f@$}pzHnVu9T1O;|3XPd5T4{oa@Js&NonOP*?;-3;3;5doK2CXytR7 zM2XY~q;+_BZ8%-KI{NNZdQ63i={`EC>3Yu}sj=t12G0v*=|6~5Dj#u60>Jl!_&9Sy z7Y6+)44`HA_XKYXS6ARAPgl2Rc|1f*FdriZbe9?F?fJjIzff)${2f{tQMF=(lYxIWJ69S7*DU*X1TTF zWAB^O_amwCxEGWnN(%l*P~UcI{Bix<`grL+h5K4>m=uN{JNaKA{@>eyBR(FXp}Bl) z@UXPD1YG{TQ6zME1FWPz;%e|RHdqpai*ERSy*!EFm zPlQ1-(XY5-VFy%!ToEW;4ExmTSpZnd#Bo$_q+p;?Vi)3ZX8vfHV@$Vy=x0Jzz#9uu zz+(3cpD?FfT#%yE!y7Xf7tx>;%~7)6?3n3rZ;r>>M)pnUPdhXt|EK zeb`0AZ&^w5KI%9hLewfygFZYxk_9Q10ETHuDsT)STg) z9DAL>Q;ZWzhhzy(3OAd>yZhdT5|7}PbkJEG!*4IuE+7obRQ|BZO&#WaH=F!6QsVR+ zmsf4U0sw|5$?SMDYSGbA$`A1E`FVi{JANL3x=b#Y4Kw8HZ%cvi0co*Wu+3*E2&asP z0K5vC1s*7;CWh#SfbC*mkJ%a5;ge$?pq>Al>?moC!3}F#Grx4B=z_MZ`)=z1hn$Fo z(grM$gKkIrx092_Ca++aAb!7{f6xGnlymM@?!yG+j!m1)YMJ2gq>( z!>trAMbduz=rHT#!+uf?BH$RIMO@A-v0rxRf;=gqw_U`OG(w}&v*gY@@b0ao$BdOJ3RNI)} zvm^6+KOCt;^!%uE9#pe;a#ml~L;wX@nLCRb-qBNE08&T`T_ ze~Py;LS(Irx?spoS2&^)Uvv_#RuOWR(0|B3y_YmoEJK8;Os=n8z(?|rV6ydgTjO%2 zN{O#gPmli5i@|CXdkGs_7D(J8V5WGrU)FkPTPKo zJgtbNM`@x)<%La~n$q~BJa8cjmAJHj`SA}ZmdkLf%ZaRxA!GXNHrC*@|09_#c?^(2 zDymQ%vz#A8VX>#i&TcD^j8j5rUfL6tiy~a@tezJ z4LdRJ9G&P>n0R8DfG@jLpGgK#wJhqEO0{Ghd2_PpKD z+*VVhiN&XnwoY*&I28_DjB5|Qzn;{~4F2;nk5O$6a`gfAsDON#<$RfKfWogi(^T=; z>imCmunhoN>gcbx!n9HTnxjHC^Fj`rb7{mx@zjbnFL(Uzt&!`E1H2fR`Chs!JW9qr z+}Y#h%Z{=|b=W;FIe3a--RH3v6XGZ8<2D@jnPc;{nFxY)lY?3dj>6(oz=OV|GR!`; zN!krQ-Z+q+k=9jI%nmLZkjT(8LrdjAr^yenrFe zyZy$A#tCO52HMH-m0H+TK2|@`z);Fq?zNGU{oMJJcRTk6G52>SZSQJvLM(W$zyw~< z3(*}FvcVL(Y3-F#!HdHhvDBYVBVt zX)1L%ociG0dDFbblua6hJqd}r!$VFx#Apg2olS}5d22L`UEAM{E}=oHnOjFta!Kt; zGfQ%?81fX)qU?ET9tBoyj8FzK^&bebYzoObq$%CzBA27wQ^iC<4>uq$LM)mU`ECp+ zZ$ORIt%G}XXXCS<_u|X9kSh)oFD~42# zyp5#z@}n}|L&rfYgrR1J=)w}CuEMR*t!4ykb4)WHIuOEl8Hv=m6B0w~@qB(w=YP#RM;t`R`I6xg>K|J?O(~|q zpkoe5>)GSPdcV4>0y3}O%bWLAm{oi|R4YAk6E(~N)a3mZZ}`s{+Iu*ooGGC^)78rC zDT7(ceFYar*AuSn8~wEiCQOv*nScJdL^F9aVb`o-n3>xU!+LA~(eCm8soipva*G$6 zO;@TTqtnF&(gMba({ufWSV#If#Dne^FIe=Y$YZpM_;{iWMD3g@OaJuT>;T3LFXl`! zkt}?sigWB4dF(l!4Q%n(8~|BVIF=&#-6Ht^i>A+jHQj#g&gveWB~SGgf#xPh`0vB( z)ko+%#_+)bwMxVvUT7M2{NA7c+u7d#h1(=vHnt;NLbGDtS0(S;eU{BWX7y30i~8uquG7n*CRwmC_K-K%}gp zqc#)xpMrUsi!^cbvhvQbr`^|Rq;36SfR6aqSFUIWl2pc^QwdUmN2o!Py>X9knDT}C zO{%fp)#n}T&^9QJT2o0-?C9@5t2G&(&5RU2lX=1u`cT=?XIzYnNCIG-?Lj>59GH@s zM#%dEdtQ#l_#vxLavQW#MKUB(OkjJbSP4Nh2)=hgc^2W_WJ~5-C8CFCfJ;&&7ufDI zZ%0++ox99t0@~{3Rtgn*(I@7~VG-L>`Sg(rB2&}iLT1&bPWQM^_IULn%n6wsf2;iG zl>mN00HpshE%=rGB!!)Ky|Gn!K|0Md!itLi2+pqJNar15P@hGS+;B1~1gy zp~g-jPl_~G`FiV38!UiEn1S^Uei~i$uq{kq>392O00YKZFnlIU>SO))I+NpH=;k?8 zS{Or1@M5(Css(X`+IaWYAuQHGED@MP?p`4#B}a4G1_3z1Ai`j>Osr#!QAt;R)GNqG z2Go^VU=wx6SH@9-5$XreJEx=?m?`D7QRVW}YM^ize4-!jzV27QG+T|wd6OrRFoQqA z$CGbgkYcfcpL4H~ym|^$oT##69=#C{_g;+3!<(1z>urBOHto34OJ5^wdnE%Ep~F>- zAT|+|>>{@6nLpOPk%Kb$je6YC~-qF>q=R_W`!9#(%TuUdM+3Sp| z5!_3vgnP@Agy+NAvEX0P4Ri`SgeaOCHV_q?2-vEZr1bQ%`txeTnN7u#z3n9sRCKqG zEcbZnN=G;)mWr1ZU6p)h<{xH@{(EY4slVG~PLA2f|KJwTF}Un;5pA&W{zaid`18CO zNOF4}?jvWaEADhwsDZn9uvArXN!H?2k$(aLqgS~}M8^hk7!E2p-3d-H(kF1ylE)i} zoYmbwR@{Q9a9-gki=)}k;%j7g(bP^!KiirwmyDDj70N~@hb$qtp}y-)#+%MkK+WOM zGhi$~9WQ5uv~QtwbO7K=DgEqFx=g3dm}$nD#x2<@3$dQXiw=r|fSJ1m9tDrK>Y`k! z8$FUC@Sj9ypO4&is|*yV8qb-#L5b&lg%+retQ`m-!;kJigm2Iazzivk*{zM~aX@^g zs=h&FQnhm2S*lXzObMYv=)W0?$~|twx4$t9s>6x@;SLIhFM34h!}^03<2~$=HHHD3 zB2y$oF2(sz@{Tq3{>Mki{tZ%s*!13bP)iwg$|(9|(G&3m3-p}AGj<=E5Qb!pBQa6Z z5H97Cr3hZL2wv!Zs2+6`HNhgB2{)XHT;Bk8^?Xt_rYr`p4%(1CTIlY7O$+`IaqNx@ zXs6+Za29laMd1E69X+F}^PO=1vzjcfhZzZ!0snIANma5LIn+nwvW2J-wHVFtnqR%z zkJF*;Hwe*8;e?wuwI3S#w-}S{-?!&W0hW%d3mFf*crl1yP&t?+cj97u+%`0upCuSh zU3Z?59aFn5Fot8V4(EP3gTcG4zpn?b3-g}{NTieQx_j^h{z4~&qp#~_bH?>Us)q0L z==bQ%06@f)NmFcyYIkjBTq)G`jy&f|x2a|2kM{Td^3r}-d+m6vQWf!3bp>xih7&G7 z2{Yp|*{huyh`c#dG_oplp)KBUES?SVGGC;eOw-DAD>CZ{GwU+h+t|UEaHW7}ZrF(` zcMjdD6dpAxE?)!t>uuXZGn*;o0Z(7P?XUdq^5npwcGz#O$Fypri@jo*%uTUBWJXRhUzwbe+;c7l5(p zI0ev}Kn?TQ~c~6}!wBUcDquQbO22spZ1$l_{?m7lyc7 zpq=S=*XQ!co(p#i_r}xZy`UhQOp^k*dPXCyViM@&}z<;AfTY^g&15e;#2wT01D^P#<8 zJdLGsh8C*VJUvJo4c!;|_)eC?*xmnDRHIylHVZ|(rnnTgAC`RaHNe=N+nEJg?vBj5 z#tM&*5MH^>?1Z}5crV1PQ>vtSW(-Yk7jS?3su2@(GouYPQDA2i{yT$X;rUP~$l2Pa zGo_E84OJ#s6>aEKbI2=@MKuPBgpT z(0=HiwO+6QWQuX~95YL&=&0DbH<}j$+$Z0P&{CtVB3r^EG>jIwU9E~Ad#1`!`=2<| zz>SE!A^P}MNw@n_Ad=Us5;^8AWH#b>V2{M{8#yNT=5#mbl`aqmdFWY^f>eip;6M^~ z$ok7K^BM!+=ncf#`n)hj^&;p}j2Z1uY%ujJ@Nfn%o#ofd_o%V`=yN@H#>F%ldv$s| zFABHVuM^s;T~YcS$YhCdC&iTXwuZWO;goK8hPZ4pwwmr?W3d>IhC}ewUT5&rqY17Z zhhVpVr;2+}6i6w8c6PqXWK+p#>JE-|^<0T`Bt~cYkct@QvM&WDi zhCQa)clo{jRPMa0bozESPN=Sv+TfZ9!H3jcY*y6q!HMB1*cQ=y?g|vwnx{PlA!&U z91bCXmF_xT6R}pdBXOoyc19?8H6}IuiJiF+l~C@Pe(}WFA_I5c~xtq zi`Ggy1406BnFc$ZBPY}>>kNG8uFq^S^;Jjus6?)r220jkj+w}l9wGZk9;7!8mpmDT z=EJ-)TUR_810&}Q#1{2z_2iy5<-&VB>jQr^V(O!X4d*~jr`;Lc8=$7}mX6MSTZ(HN zVvCHn4bY$ zl?_t4z>k@5V!5b?Ry8a`zo4-+l_{GHB||o zh_-4G#|@w9;tFcMZv(A=qSsUmcD+#fuDGuanm#l6ZfLYn*+)+nEqaJ|iZANn2ph|_ zXnX~5cctm>tW*X&hp+u7P<42`jnuWc>*iRRajFrv-rK**Qi*b6<|8fV>Tu63;OO{l z5~gV-_WXQd@-OV-Y_(d3pfdwz{0QA`i)0s(&gW8JOXahZ&*CZ``uV!^jeGuIoVfqB z^lw|iBR0*OCI5p5lMes+dJOe-WGT#3FkwZ?@p6cyoysGrz1+kTYOnTU`AA&99ga_< zj5hx+#cY71GmJ`xS`qWWwMWw1%lxR*; z_;~GdG4I!xIcxuJ%Z>&`>%#+Gx2U1!fLz#UyFO(|=LAFy z;nEq)SxA~RR9m1}Shd1X^KZ5N-h{6*$6K}-Lp3G&(VKC+Eu4P`Wqow#O2C1CMprP5uZ^mc$3R`@%yF#l-)evg79b|4Z3{I|=>oDLdU*#%7RTj$z&O=efOq z-4`hP9gX-=7#Y4{P@uARviqfBew`9~_v2{>c6{%intbVg} ztFW^zEceEYw;F=h$?otsWnPJ9)-&Us&sW;<9akQ=lTETqf{H(^q|q~B%SA+j<{+a< zKgz+sJKIm-uTnWve&$0dlMePx>PaBCM%UlJ4kZp#zc3yAU$=dexNy6&-lP15vl65A z(PZ%ka~##8lb48XZmVT85Ra*C%=oUVqVkMYG8A>rJB5~xd>flmrmDEOT$B;-QT#>I zRyi2H>mxPXmJCphG3U%=#sbfr*3}VyNeaHmq-6XAM-pPL&dRhMP|Nd zzdz&X1cglFV_msowk0^kPCwxuE`R-E{%QDoLevj7{F?iLRxtlDs-3AR>*)U|9 z6$@Tf&-1luEX!+_rQ@3wxqM^gRi>eGRiNyO8yVS?dK@4(HjwF}$jB)cj&-3D`RFa## z&Bo!-A5d}!QI2n_g^ji#yKp6E6>XyZ;C&_MKBhSExUo{Ck2J{yKZarpl@oXfO~P~Y zW)V&>DI+kU4f`h3IgulMK+zB46n(36cC(6lL(MUY&cqG%j~~!j(_7uAVNt~=F$vwx z=|QR=Nyj&j&*_mj^Nzv~^@m`eptUhiRj+1+Y+GZ96u?KuW5>e}jwzP~cD?RWIuA`^ zt(7Ul1I2w_cgIgNKHe@!yIyY$w+aI;_Juzl`!DX74dL^YCH1~tQU@2^TKFtKGv2h9 zOn1L0!F^^*QpucDZ+VJ@GeE3hejdMeCmg)jwqyuqdk&{?>Q%kjE5R6h>Kn=Kl6yJ- z@O#~TD%pPjWX|NlNhxUt{YT#g!cbz*HA~`zZ*1)cQHs{v4eGZ4$0qph7xG7nUT@zk z8z}6U8(9=19eZO@ixv^6uuXICd%8VxJh*r8nY>a$f}R1H>izC4AQ_J%DEKB&R>~L| zSz_EmpB%G{r|SvSDMr}U_t^(~n4sRW%Ckbf?|do0m7uBP#O;Q?@PG3WV(9&#+x^$- z73MJ$HFP^*U9p9?IvP+!L@GcgcU!!YeF>|HNLdmbitl zM6W)ZmK~iv^eyMehlMEEsx80u-i~9F))tO_k{7DL44=B37jx>no+g$yLqul(5L_{> zJ-kK)`4YASiv+rt3KnkwL&P2z*5G2q9!?8aXbKL4Wf85pm@<+k)-kmN2r;~Ry!FGn zRJcBG>%Gi(W0_-IV{I7b{R7;MsO#_J_DFLaa*SBF>oa=c5Y*#F~7~lV?py3cr=L;JirCk^rMbi1` zO{{4C@hrowugg-(EO4LKO3U$@7GsY{?bgy*WNgg4z?iCv0w_bn;+9}6>qLpDqT#wW z{{4?d;jg9%>9hft%sx10nY=Kbl4{3bPn{0;yF4P9Bqmw(pVjzV7h{mli4ML-k283PfYX z8B!MU*_%^5Tpweh%D&X5jYLSZ&pK~f-FUo%0NcA0OuBT$`)UV~Pz=%-)L)>%<*(#T z`yT|6L*F{EMhs^0mSZUp$Hx{AAyAc%u-+cxH*`1>xUq+}$K_(C2{x#)S!IEJr! zn2fi0t66Fli+n96Frr&+{r6geH{$FN63zfE@o;k(Q*`u6a(9o|4K>en&&1CyrUttc zl6Vp^$&bpL(9CZeHa>gVs*LeEDek%egIPMxxlg#ky)BIT7Wv>Jd(!Qvf$Gc-;{gZS zm>O&;X2(NfW@615SgNtoRPHI3R0NL2=HtNiMtO7-H;&J6Ds_}6F2nG(gk+7OuMYNB!uXY^lD*-4v4@cNfA$du9V;pOlF(NXo!QP zB~{HNMIY6tJcO7tkVT^a$dU<|=iO7;hjqjhZ$gOY5B=2ate^np-D6dg8RsmvP-&hY z${YWvLoy9+G0b)5$fVnIKerm39Z-}CPEX!{N|1bn+RxfeKT>wGg3e=B&KiHDi6NY~ zqVbhiXU8i@p-i1;_hTF<3UHctr%mOU zx*c#i$6^Lb@A$l3L&qSYt6ZhxLAH?Z&;>h{WV&}wKO+^K*F`dGxNm38*zz>{zWRku zu0xMt^-Ud)|E_7>lR`yDOY(ST$ad%;0mOUou7B}#eZB$3HYRy zSJH7zz~AW;^~Xdnc9JmW(<9@=9sT)2DV1~P*;9{U;i7;Sn5FIT;lkEHMsLY98@c4B z2pfs^I6eyVOhVilP1FtPPAfdT8zyT7)dvg@)oT(4Z2%b;**sFf>LCS30#*;N|3y7o zhEjO#e;z9o(L@k2T@0+Pc}o$T*97dgh4fZ%*Hn&ZH5@#9Z9-s9_#R>hypIG zuwBfjOyjtPwvN{tm^Vb$9eq%!au-Lf6BhVy&JL5&CF***<9Zi$nh1x|Kl(&9rpU*`pGH7P2%(P8AGTp_oY;b2_t@xL-PA#R)0? zd=Yi}oE8L!0y&?8na{OW7?m5l@4l1A?iN#lCueovsh`i6a-l>d#ii^LP+1 zg_9VFQj;h);+V(vQjH)0Ty{aHxbiKt2V2S%n+_|Wnl5JZ8<8%9qSnsAt+rKNphm?0 z|6@gbJz2>~mwMQY9bPMn8-w=RT>$(!VObBv6n`zbyrk<hVvz*5{tP9a}W{1#ZmB4e{o8)w;@K&^x4#aTX+ON8g+ z(T`j*+o!aJljL{1hFxXzLctK3nM7@+A*Z=3yQWE;p>41KdRj<@ylEwWtK(|PC%WXK zB1;~ACkw(m4Ur&6H?OTTr$>ZzL!3E^AJIjy{i;omlvK6_F%SF!#coV}@T(FC{hL7u z!4J0DnX=E3fS_a!vlF4Q<1!G};F42_b&3uLziPborWU>gEWyY`_<`cxY}&uB!hORO zc__T}fgGB6PNM9od`*ebaELo*{ z`Udbdc`WJU0P{s!tjMUP!wrAVm7G;0cjyeA(7CTdp$^r(C%|Fx2&%=MFZ0z7>EDD= zb1+eAL@k(0kXx-vj^6|j&I_oYBxV!aH#LYuL}YO}E(WR|rqZn_Kt&!n=H!D!%_kjY z_Y08NV=l2^MksopIT*z{OB4svW#K8*W#J$ssZd_~e7d5@F6V{YHpHoin!dRTD?G29H*@)~@3KU+42o<^a|v(O`?)&v!Z)Ed^#0?>5pWYQU(}KQNkN4LVPRs?>gXH zsfTI6j^CEMZQKX8@3$lPoFvwN%fs*1P4mFg1bsGx)nG2?SR$iuNp<=}jTaU=4#``D ziPIkzXE2@7fRA|)Jl7c}la4Ec%-HF|>aw-hyY@p#HK4M4@sNJ+)zj5j+)!eX*~R!g zA7--MG@3{!!9_HB#Y{kHslJ`&tCM9G^WMBgES{W@e~@_Y>z~Xh6<-gP!Pr;w+uhEe zG=5*lQ`uf1=L@ZxPX}ZH$MzOL(^bc2MYo{k;2{n+a!@Z2PS3#piI5khUEn{hm4MUu^QT zl%{+*$5fiGNI#d)cqm6u+KUy0bgM)4zF#Fz@t0QAZir^axmllbM+M={Kq>_B!nz;r zK#Pi8eHG*@lAIT{0?0_A9;FhGJrR%8n+p%Ml@S$T&S-qsn-MN9iqZV2oa_Hb( z`Y8}xUSc*am=wqrSHRi1W;V`2k!jpQq03IhLjvd%|ENF#7c2=dxfVQh581&+^ir2_ zQAQUpV^3_0FO$|Xl9zZ3jbO>-3X2;1X8BTEP7V~45UnBy2I#JJCC`R~DU;~QHPywP zu;IPb;#d+6(|ulRLSO3fj=HYXxke5`)gfPxRCbmczr4)IZBe&3LTNq7BrxM&YJKD8 zY4iL(co`_3zmphbNW9M=r47kT&;+%^VW-x)6sfkGIX2t+dsnSPepxh;{cpcJEI75$ zE2-~c&R28c0Is&gsekbi)4(>tZ`)&ZMMh3o;3Tgan`no@cQB7;W4~fTxT6jD9rYmp zJ>{wS1uFISP-CK;ApU1FT;YX}+%G`05+ECG*(G+_HhwZmmn4NlU=!Ev%aPF=EPQm@ z^RzYFehwSpte#akPwA@zR-HIvcy>pR`~Ej(;# zkMzCm0lA1Pb;H04y6^eX#^G*wTxuoACcruZy})Wvzjh-)uld(~xcJv??QtF6?&&XA zs#}QtfP33**lwwUCGk&qQonw4duR;Xb*CU9>W)%AyCt;ERA8u2hg z80Hla>CxdcoLRY4WH1>yW}WPDmrW#-)UuYwq&CM+;<8l`3iDd$E>7v@=Tv3eQEnJo z3veH?^!;#Y$DOC|7q~Ftg3(|k&vnyUNIIZJ88F?`#sTe8jIgX^@a#1W-qGJ!p-KE; zDf$cF?7@v+XvB*$%-lAu(mV;a%kH;G8a_iGIAz%8u0qy`-hIa!3ja$Ho`vpm?y%65 zlx~?!OMDB)n-z99o?R1!&wMo8X2IAdDKjqC_vMBkZtzO#Tbs4Y{01 zG3m&Bp{5fk(b;{9G8bTI^nh~Fy~-#awg`E`#Tv<{?}Du(9!{4ysRUyNPe zy0}UvU8sq9VY1q73o@FNM_=$Wm)>}1F7N}Jwz%SL z!6N6HW6R4G}06!rQr zlNHW64SAgZg#fS8kk_gDhuW&QJ_`Q`k?%F=KEdEJsB-z@X%g0T$~qP0VGZD3>u@wX zC*oYTTV;5*iiYV5Ve&ntJ@maeU_!F(>zX9KfwidO7g{XVOE?5!-=;0Aww3SpN0vDI z4=0V*d~=-~iTt!4gO;SRI93{sbUeX9io;0qHk*4hlaATO$aKE^h>R|5_TRqJqz8%} zuz0A=@2p8iPRdUfyB9ACN$~#o>`YG;p*lBo{R-0sTZJLvn7I{RWT*cx<>hAf1IM*n4}oUaLW%ia0MC6c^BTTl)s_Gylklux>u)j z&+%gQ{4%?fWShrXeVRrvnm^oU8GYPJ2s=`+^@>f3H&3aK0Oru^>;9}}_9$$x`)!j| zE$ryI>Q+2Hc^*NZf0d6LN%F@72yz<_w9~SU;;!YIQn6KeE=_jioqyawwZXR!e6DTU zy|BGwPhUFr2_}jtPEmgL$y;oHNo^7~B(&sTR9ly7%$&y$iA{EB%#~YAfMs`?WIC&5 zU=H9zi{@JDI1dPljOO@NwP33?`I-*$A(NVa&7mIj{O#vD9*EM3R6Ht}qMkg&MU)N% zMgsV?Hm@q@LJa%;+BMuz>{{l@99nEB=a&f0$V${@n~!mv9$h5op&|n9klLRO$`nUG zkV>Wu$`y4)?DP|5yVSB)5Hu_JI9*|M=e&x9X1#ZYYps+VDnlIBD-)@zH0g1`=gW-o z&G>OC{W{g^R&G@*jD5Yn*_}K<+{-?>>H5wB)SE8n>0RobH;ItKmD3_%e%&dbfX%>eTj^kLG{*z2YHc`;3@T7>cvmC+% zRw`#{j-U%`b-EsIhI%?k6G-jf)BpB;jUzZE;UC_&6|$X|J~BE%kRzFHcX)DfP zn+K*yvWeCBti_Sw@Nq}n%~-`_$TYjJxj+){~V5F zRuds%?vjMA@D3ZoWUGp|5%%pDof~S)L6%wP7oOCK+$CSQ7pq_OJYtDq4{ZiDIcCbp zo2_wiN@t+*H_(rbEAl7)EVP&DFQkMo?SfcBkxCFsmn>MWn0@d#uE0vEOkGcKN*QiU zGhiylDq%vA32LNkemm^P9X|e)hw36Cq@Vqur*J6yTB0UrVJYg`y33O+Y$mS?=-Kd>AFO)QL+4ORS`U_JuDlQA?8MUdk42zNL2C&kcF) zagv8bx|;r;cq@|B7lp-<2?w*>lu?+)b_cZ5zqGwlfYISiCMqq!^rt7|>&c3UDi>4* z1(`HWY~4{yz%1uu`hJi!cdM?8Is3(V-hA!6;wdqs-$vvltibATsRDf>RuJUf-7Now zP$Nx7LFc`(0F9Wf=E{>;^SI(Gk0Doloo{*#(ddcbdKqe;uqkDMCO<)*X?q2_o=&v$ zj~IJVHN`da0{aXx7dVtm>|dKf#QaGE5aTD|tIxfJRSDaq(>H~r+eeJ2Oz5TSO>?8T zz87}}E*7MF+ILX=Z^gYncOcy}B?~}99#LuBU@Q$|G~5ph)s?41DAy4&lK!h6<@&UJ z911CbLJ9vSM$&(Rf+{RT7slo3y`fWnpN{SmiN|k;6uxXrXwJOSuSGnZ@3q$SU+nDu zZKy;soxUe$#J?y=%-;xx#~E>i{o(eY{G3erw4*DGP8Y z#aLr;u(CW-TRB=vs~eF03dXrS8dlvO*Uiz)>3PLDTVBKGG-nhaYTe>bt_V8^l1`4g-+h~Y3kG-%~|0y=@@;=y_gAZ>SVV?s7^|1RC6g+%9mVK z#>Sf*(ET{<-?8~S*nK!DF{GSfDK_1&`$x=oO!OpK)Q`d&?m?LYc7U!dwb0#d$XoMP-F_Z~WZG4!7jcVyn}vU=yg=K(6Sh|Zqwd_x86~Nzq|9c^e>jAbd6*pD z3BSMJPfDQdy6 zIG1@B+wK47=(QF9_QgNk$y}yYNog!xgqi>G;ncx`5=}i=>j7vJrEe``>oK^xcR(&^NoV(d0;PG5b9X-sd!1G>9y@A;Z9?cv; ze@(JnU4=xq+v>DRBtN?yt=(dr(2qC53EdeF>tDVG{#p~U%j*1lRgKC#^0TastF|}{ zl=E-L+;WQzgB!e1^2r2?cyDVxZG8$%NYmwmU+xz~tlYv-Fu>r^g7jpdBOe$%8l9H$ ztICTXdQ*?3j20&0BD{+;yAbi3Dp>Lls<7Z_2L^^64%(v8w|1^+VPCI5N5^0|#XhQk zKNMb<#E19$>HB@ocEl&8JNLhv{(>L(VLIWrW&_sNy||gp`$yD&FKO^ZtZQu9M6)gB&M+k;SC;hYc_JCdK3=chpZljjI#k}B685ZX zwtW$&)$R?kgJ&Q<((zjj60aZW0RXu3g0D0)SFcCb&|;xoQRL6H4mFG1!A+`&Y8%{Z zXSznHR$n+838PYQF{yqrmg@p*zjZZ+f#dFMmntL8%pWHd|_oF^m?`6-*p)7bv z;F8@6SiueN3IxaWbczS%epzBsJQ&dX%ls|67JK|cxH@WJni2fEnw^k&PAQgl#WLkH z4)tdpTBB9uq7F->xF7cS*s=m6_S z%wTbxmPj|g&o8fEgZE6GM0Hvpv*gBBkZ}j^@k405{=6HIZI=)miFMG%WV_JD(Qum2 z#m&x`^@4t$Dtuq?7p)eRWS{~qgJi8z2xrJ^c;66KPyqm=Dr33|7#fY-QL14Y7@LT% z))A*fZjND4?7+2SaEKF|h)|IJ{f~IJbo)Tu)54sDK@fp~`(Op2hRFb2rCbr+bRn=5 zqJL|sfY^SG8zu7mvyb`aSDb1W;lp=yLgYa>wp1+Bv9XspW_iT+pHi#_Mc9X*=$Gzm zwY!h4OYPc9lL1cfHUn{b8vSYvwRv(Y#d0BejWX&6o7HK;Mb_3i!W>Em&o^yAP%7R5q1(|81nA&xs06p?}@ zyy*K!aB!wJPOib15>=wS6ANI1F52Ii+zs{SE2{^cr$e6yz*;bwTtWjknmcwlp))n4 zN=i{RNVxsZo0A`Z-=20?CUd!|9H4eA4YU_#n_D2x8U3O9NO&dvjt&nU>yfCT9AlWE zKxs&)(uNUbXYVvQdwlPlIX^j$H2^H7OKm5b0a9f-C%#J0HWfQG^Z!v)noTz&7m97x z(H0xc%$;5Oee9U7g5h&pH@eGWa)=p4cZIbYvkx7e#>OO@Xxx(LA7@YQnjT-8&gdD2 zgeuwHl3I@)_iEB-_ouq#bm#AsTAiPU$YdSjs3F!FvO4)*xVjKWx>0_uny7gN%zbeR z=fj2yB`4OXvv{(Hh(8pFIpXK7f|uM!rzttOjYV0r!Mdh-5miJ~_P z>aeXChRrUWQ(XIl5@ce3YFTusLFCZo1SF3QbyzOUm)K5KgY)!6A1)q3GU|R(?lM+7 zkpL;zwfk~gnTpAQq;N6~tnDE6f}&F_1%OR~R3znQW>YDqm*b4ynPQ}3w5n%l>PTs@ zcb(Ph?z-&lCTL14@^@&ob2PU+ap-9*AJD_9FPB`W9qpIJ)6VLN8>}#uwlkwhFObre zLb5Z3qL-i+V|9R zmT{QDFxf?w^~cLh4-oHRJH}(cl?lo();qrYXcJZD>K|)lF?$27g&PnhQ{ccj%o^J8 zB^V~_=9osVPR{2Z$zvbJ6N4#*Y8;|^oIEFC9KDz>{gC<=3NdlJACkEJ5p6iuhl#mH%=1k#CpMPdE>!_&ZQy6~soE=go8 zqhROu1MeB7yJxA-bP=~(Is0?G?=wDNKiBSlsj!WqdS;y*j5$_m>d)Ar~1mfdZnh9x!5)C>M9R!~q?kfK26xz*xk5XF7fGyi_q; zWHCnrH`QpvHH__cS2q=q{$waVw6}8c*@Sp7q%H$d*HtSH=#4Blm4wM~GTG*N%>pY* z41ulkTR)1~tcd%rAAU4Y*RA9p`L1!CUQ8HSi(>HCq!oAe+pI@5m~zd@o*Os|O)snx zaF199YdR=o-?7aJPJU2bjAR?qr}tTonSO!u?1U}d$Ap^zHtAsoag zDPSq$SgTwr9XE^fXD71u#to8DXHP|uLYp|8b;h!Xu-Ql8;cCOeQ^a7^YK3MjC^xIG zg>k9eLeLa6(Cx6C98^cNTW@9eapG+p;-;P&?gBCTI7v*P0OjTII{O;$8ykx1l^Ghy zqPD4#f&`BTc3vqan}##L z77EfG?!#ZqN*@>+l$qH)Ce~Cyr`W9hJ!;?gg#Ik-Y4Pwky3C3psKF#-Q5xNfzwlk? z%%z+$^ec)w6B=-)&OQy#->|7z=Snfd$R+nOR4qyyn@;CEx+CEv(~%YN0$ElYB~@Q} z^3A&%oi^H;FrXjI7C1jJ77XVg1QH=wP<~;>~-G+Qaum9MVp) zXi^KBiP>Evr-CrTq6vyg(%MBQ5i#QeeZ4of+S+u8-N6MCLi%9Saqw#o_amp8`rQ_2 zYo;47<#B{5OBycJ6>-heEZ$)uKJpuL`D!GCR4HmJ7O|z&_iN+TGvA!}*i5Qxm*h{r z7>n&;@bbzi9UbeF7pnBa=87}CPA-_lLWabd4L`JC{;XSE{WXd!K*+&?q!bkWJ_O#n zm@vfK2dw%GWKHr3G%V3te>c!3fGjUgeL{0^2@tB|hD(wl4YO$w9-m786I zqUDI>ON%PRV`Tr?cA*;NsRj#-v?8vH&DPlA!vzA0s-!P-!W0ZCfCX<@FFDfn?tO${ z;=bTy7W0sKSzS5>ixEvruw;RSW&VpQoC$q&6I_8iTmdg|j&%ZBF-u2QGn~i{Zr=?~ z6;%8W?cQhw*0_B?vY3ows;q)er}*Yrg*%_mfs<&;uZ^VOT`ZgR&0u5OJ(@{lfEH}jZ%gQ6l7&dr*Au^JVhJhEXnC{Y6z%v z3TTdHk2vqgFCtdJPHu54PX=HRs@=9ROyLjSlnbV4F*Vve3BVl6?V|ek+$p_33z&evQGj>`Fy??dls#nNJqnP621$^jYoJV3?%$SA z*mpwQr`BOTO{F1cx9Oqf*Sq(9$#b=BPD_>?YN*E6CnYor#MCF>xre6#wwx+n6Lo&3&XxQ& zV^b}5xGk2r)tQK(yVz9!61!^lalvS^w)C#Lm5}eG#Kdh+03q1$8_KraVi08ar?{B? zYddI-b1M79$|8!uMTDYQRqZH3q-7T(WE}8@f>#homO4J~1{f#Q?5UeA>T$G*!UQlH zDfEj2GD0|WjUO~x&TiIr)NKIOI+p8GSpnJObld8QkuW0lEr%!zdCWwJPkPZUVksjh zfrc9hC5lV~XqMvyb|>Ubr{p~P2NO~BN*PRTsdnIo8o2Us$t-J01~r~ zGiXtC-HoKX?Y!o9br@H!OH`xpBwDAphOQR{gVk}@ zx4w|X8XZoakSH^Xcp^G~+MkZZ6DXSU@`A&?)3DYzGmcuQg7 z==YZUu;)XGL(;Ab@n%ts@Okabt1q6Q*EC6DIFHAf@pU(6Ce4hJhPNizy~vzq;gUGN zCFf9Q)#>j^mg%(Js|K<8Ic46}_aXvY2F94Sgr=0LQE5uKPT-mcIDOg@!IHml1D1T5 zwH*m>K%I{D`c_swCZNP;x!tALd4-46{RDB?oJJ41UJTryDacxb$JLHbUve47&XU62 zvj3WG!a1CioTCArzX6`fZUbT21&*QMpHI>N;%@+Y%x25|mq+5;jrJtSrCS%zT}v@l zSkX7+wJE_zqFEiNGijXu?kRkPXSK>2`z6Ztly-P;W$HXY6(?-w#|=)BcwpW@Pb@Y zxosV5O0qp56f6M!UyoOG^jfv#RDBs1#`WBM_IMT?bu6F>Jz(a%PDL$dwA8HqhAt4u!Pmm zcGYSvCzHYIn09NPZ)fG*)fFWCdkVuovg>EkorJ&UmLd(_N(WUX#`_~9FM-C7N-oXg zX0gPGQ3xm)Cua+#OF8y)4 zBAv_Gea}1Ls5Hs+yi-0t++;WvIgT_z<(qfq0Pe?(Kt3VChxp*SIF3tRY;s4yHz%0z z4#-C5-zB^3Ua9LV5=zZKKx80$CU}eJE1qXxwj$1!&6M#IKNY>sRG$((z;&+Tn|&KP zO<^y2k?@*<;@u~kWR6p5OF7DQ`w@>~!yVRo+T+^MBsG8fm2(pyi%X!t%kV~>TOrbU z+ZA^>i|t^|9TL@x+SMR9tI2`OGrOpDGgu4Qry2iy+(z<)WR55^PN(Z{6okI`k-^X5 z?!t^fluZP+LvjQo=?53!gyru%jW7hrmN3Q0fN z{-n6#=+8?GSx>FeUV|h96JM#~$-Q9yMjA~#!AJdlTi{dGl<%hoypyfB!q`Vhw$Bdp z&VRlo>J<;hu}ad~rL;ukCC8M4LI~o<$qCI`lJJ5$rIlc~T1e2=;Epk>M!^YAdcG!v zMd6wLd!VEB1FQ_{Qp&A_C8C^$@Q0ZCS>s=^MiGi80^8xsPF(xvD&}$86IUFZzulny zp}ZQR3`MTCU}BVzWHG0ZiWAj#?(A&7|BbP?jH;_!wnYOZxVsaAy98Ny2=494N|SDRzhs8Q9cdaq_WP-F3$Ro_f;s+S9iz3sp^eB*l1ZvTjwdC=y6v4mVAfLxNQXafzG%kf&#NA8Wv zI`fohv+{yd>U)Y%iq6Q@%0=+IMfAfypT@H?f_#cMY&_%fy4z&{#-iAHyE?0Gw7yV7 zbJl&V8xrnif94R8=M&NFOwycouzOZ3*EexrhaxEwt9$NqFI?{#;>PTVr5!W`u&g$sij_)LOn{7 zSX_Ygt*yLKG~F?a>~-C3qAZ!(EoPKzPD*uHcw~9DUIMM9W)YI^g$}4fFXSbYJIaKA zloO}devYsK=fK#Z5V3!?lz@<0(od@mO{`9k|{ah$Hi`>2h2S znL<7R>zeQZa69^8-6`Ba#+RUt5~-gSagW?S&g&Qimai#vXM4*GTM^PCaM+N3bc6=t z{pn_q&IL`6OmYS4FS7uUp=L0c7M>>Mu=);DtVq9fJC%}5_&cIa=`HK}uBBJs-a#^( zL{W)^7jl0d%b^rx7sQdNj2JUN9?5rTp=T>9Y8iM^fT)bGE7;9BYKXyhFZ*>=|LazH z_}pv)s8MRzd{SN;^;XwCljx(KhhXEj)pCN>_}%QQ<>fBwd(i<|<;e+hi~fcSX)DpO zqx2-L0JR=(x2RjrLPnEG1Bx|ev$73KvOKPJlR3WB;~P$qi!Tn^eV5r* z2Y)Vrl=U$|t5&DVy?y*#MWkF? zZ1&dbD>|O;HE{SaMb>UaDCq~N}5S(w)uW~<40RmJF~P9fy13Hf>y6fQDViK zwq&dJOpRb=u{NfG`2{$%){g{bWVbUzBrUi1gc$9E)?N2_?%31GJp*Mt44LYR(pDB> zXb%afo9wa~@jan1lCqOXf?LW-?u`%K`Kd83uVqCZfmTlhZebUAMskOR6|}cQJxaH= zYzZ!?lCn{iH01|Yq2w>)3s+M3(=SNsv!R*|YaBMr?I@i6edAzi^yFz04&;gZXQd{! zEfbdu1h3GX=ZnXVP<*}_8xX(Z9nQg=Ja!zk^?DHfevW)?@8Rjk?El&-tO1!!0EsjH z)ezRNMo?W0G}C5X@1Ni9FxO#|LFqp)`qx>LpkHk@Sy9Y`JU9&4wI1N&l(OGSvIKkLT7@L6w-hkI$g*Y#M(e;QmDW7l!r+GWze zr;CwDX4iCcKNz*sYO%6dwml+uVus%zUFp>HUlh=EImw=f}GaNQAKDYp1w1 zb(6}DJ8SdH@ffA{t`z8ABK#aX{92@YJ~wmit8lR(`&yR!HdAWGHZRJ1eCl*C68^3; z5&OW)-#As&mh~Bx?;vw+!RpJO4bhfpA$^kqI;xi9BnicUVS6jyDOazow-(g5g%60a z!AGzEw_e7<{=a1YeP{nq=AT3qX|rv|LgisC*unMNCrpS_SYLRtgsnO76DW_O;gae6 zfv#bw4^I)s*zl;Q!|{@87JCx}b&WSx8b6fRLFgF%0x!AS3nJDx9XmHss)0@I{)s{z z8MEMy*LqLN%Uc9(et@3;WqV0p*D3pjjN?6^ zo^6H?Eq8kGnkEKfD^0>|F2vr4dZ~3SpD52y6=GnU4PAX^oMS!F)uf-m03h(9WBcc8 zj{NNVj4y}abJR?Ya9bTzA6i}p4Z+xD{{@p(n@d#@Q%^UI?j^ENNBTnxm+R$^Z$8`j zcxl?BA9;?Tl?D2A$MNc>o$OM(Nr_CuZD(TV`tS`Ql{eoT%^Qj7aN#dei_jr8at-DL zmxX8qN1GLVTV1@wBU8Afm70V*KP^83ky$nobQ?h>P8Pk-RyL%T_N2rj*I9{-jrnm( z=2bhQ%lpAyIW^D_I~GgMuZJl*h34xYTj~5wc<%K9Eklbht==zVG9RxaaBq(1_BV?s zHS+5--UM&{ruYd#j&wGi4so#N zN!+G(+)Oh)N2!qy=jiz`y?t5EOsQ9kn&6S zYj&mDga4PdJAoWx+7~~}F5a_ktz;(89;V1s;yLDDsk%k!_2Ht_4HLsL7<8<8uI8H1 zfpNcWi3>Gw&{RDom=yS?xtXK|zXS06w1bz;Ke@M0W7FU!N7uEbWiobD>oM*MQ$}h8 zd)W_)ra(TK#}Gdp$2ALM-^SYAlM4*H&|#EGcQNlAboQ;px8zdp6zSC1170*1W{)J9 zt%bF2MLT}}N_9BW^l3JCvQ;b`oluKku{LtbUmTT8^KdLLHl|Z-3o|mdb4CYoJiFrp zbG~}BU(GlFw5|s%zqMe8LYag|xBLlzQtJoGuu$G}M>Wd6<=AXMI$rf1&bWXX0HXa0 z;^>WEe(n}_`46z;rdVcGf0d<-L-r=)R`U`nU3zl3BGyPfO_sse3eR=1iDJv7gE1sDiTJ0VC)1w0o`c z9b}fF+moj?gAEsxzy+!t=iuCEfT;~l8eU_3fu1ph(DtWw6ewHvJaLq0(TG+d{7wP< z4K#`}o1OAFD^#vJlE^h;#7!ypILdzz`6-{yXvEh^{K-duj5shaZ_Gg^>b}HN+A)3K zU#sYyF;9gq3ByfwmsDYF5Yg_4+IW{}Yza!x85d40_CCVMDFO1o*QZA14suOrz?}sH zlv{9G1bmEwNya$aSUJVfeRbdd7!@H+kM;v5XFYrrM9K357K8tOn`kX!kTf}JwSd*q zOH$_HZrz1Z2}}T@lL5|UHUsxA87J#4q9x08V0d*;v#G`ro3xXONNSw`Ue`)e1gfFO zg~^Vy$xbwzUl_T>GDAofn%@9Q0~|K3^C0+kX-ppbVF}aZ;xKI3U*J7$$g53C zMT}AT3%hGC+p<(5Z&?jSeYKLY%dr@hD&sBZg^-KBFfzmG5hp-HXp8mzQ7(d-rk-S= zVz~P8Q{6N=sHo2vF0$^}=1B1?3g>1)del>ZwDgPBwcmWVNxKsqlIIq2Z56CU^g$Q#9w0aP=K4JCo ze_I5h5<4Q!Z^LVM#0)QLuN^iV z$SbOuJMQ25xoUhUH+(;DgYZqYq$B$ggIFhXLx`N1-odcg#z6X_ES7jNP6at(nYqm9 zUu1s`*M9GGq1ohwr@*TdG_xR!+r;p?n@#UFl>zLQH04n3K2CK^TyHuYdH}s*%1QE zdEI3;R?EsQVyBalY_>sczyGgU?ub0R3AbDrz$LMq3w$wL-9Qi#SC3EWMg5*{RKLMc zN>^UtdHbR*9nm+YZd<*JOBJr+8n#nx$zmt_FvFBch+4qu!OM~IGKpO1C-S;9%Z6?z zD(9Kuy=tNG?y==iZ!A9}o9O4HO)CBjpfTI;)}E>~BctMwp8*Waf@WiV5$Hx2mPHR` z$CxQozj-CS(-$5ZY+g(-7rd1#W?0yxOTS6}mU#uU^CO-hnON@f$_V!SsbH>8Cjmz; zlm+8-n{v3>d8#TOI$}7kGhf0)eJvwg*_APik*SiV1Fc;9T1zN%mcyk4+Zk`HQ-D$! zzc!IdcgIF@%T+Gv0m?O^B!;+1i$H^p8sv3TJmi9-y)yMucQF`YRGDDd2*%Gx)tz>PrJ zeQXj2B2Zwy)>~XPR{<^Mu#u=7C^Hj9voez? zb108@f(C}n64;P1C{S6m5g9a9Mc2#SQr5`ioCx@ZCie6jDS`AKT`siPaCeGCIZlNdAt-fTv>Aq1jo zCv;;!RHS0m&5x03svvzJs^ZzP^;m%4Nm;&;g%l@kgIiWHiZiY7OIi$m*NC^{&VbC; z7Yv$@a>GaCc0o}E2p-Q&EPsG9yGE+SUDD?--245RiC5h6Sqgh=N;Km|kWo5_sQGwr z=ar~nK;YBy8VVR{^|Q9o%i?>{l!s_B5ODVu`Ij#PIvDt`&^Ml=lj9H{JK z&Y35|gl{XRblPbedm7jx2IOOV!88_KCUlv^PnxJ!wc~O^z(P`~P-HAXYhHOFD#UdiI2u2XRC zAHY1zwqgjnq6G@YDKf<^aEK;N?rY@Q53cQ9g6&_V<7GVJ=>ODB4!br)p}c7QQptao z`vUqeTGv{edRnY|&f~Pgxdgn{PkqB^=1#4YN3jR zD8K9r({JlF8BBb9JkL>G3;*7TCxn=Lz3}NE$C`R$KOY5@oj`Idtp|Slk zRd!BX+>=DnR^HL~K^TW8<^4&N>%=ggL@d7%X=urUeXX6W^d$|=Z*B7q)wx^3bl|M^ zrzhQr*nn?rJ@Z)RzA=RKoV=G?s3@($eYp7BX96zI&j(bnFrgBw^1uyo_7DQx%O&41 z-b$YJI*}0lVoP@-?(e_tmYvZqx+J+wY$u89As~Vz_?*-~VN`MXSBqjx#1@c^N5J~S z;F*YEHws_}`OC%Qev6)@>&YYNAti-B2xqXhQlu45v{|GV><8Y?)*d3hy$`NIlQ~KNi@mTeG%dT_WD} z%f1QyGIMtd%i2!X!o=bbqrv+ynvV{vx%Q6CWma*}VDAVPT%tOhQfXTee@ZFtxe}Tk za?_qT7(3pKox`*>GZeZCRo_mCZ8tLwMeD`AtEXjxwn-VoDHot$84bQ+*6xDek2rq&;DhSWWqw&oO2nv8(T;4y`VV^~;5UR5z12dI*8p)!)`nS zJn}pwR|ODYC!bziW3stOBbKz>BUMlK|?+KI;pS zyhLbZHO3V-k-Q}Dw?LG9eEDK2^a)1W-ONJT&|&=AL$mE7J8w95oAn61fRqAoO|^v9G}eR!Fe;+cQ&M@eC5Nlw;gNIHiM`|)%cQezfTLvbRZsb|VK3=~-} z>NhUwN7FH>GywC(*w47V9Of%m)QH^GzyU9IxRjT7n?VPmU};Wc78xpDC$P4T_pEOv zdBy?9w}ng{98m#`r3|mWKFyQb5xdk+F&m|9``H8{oU9KJJ{&Yl+UOk#x&{Mr*5%9@ z`sILwuM>D#F$+62u+D|wcxb-NR8;Mk8OYQhkeJV%)N>t-1XkoXQP-PSK19|=ceUWJ z5SW%4iZxh_7cf$o;_YO^r*f|CouS?oSQXz~)1hHy?)hGQ+eoDO;)DRSrNJE(DKRM+ z+;UrM`Rb)KM@`Pcs#oty?M{NWP&2Fg3@59cDg$1#QH&)fv_i=EU>Kt<0Mju{^N_2GN=35Iwx#$oPu85iU9p{1+!El11^IW zkpzXy_89?3Oaq%757yjL{Y$;28_=z*`cK3J{H{&v4ewiY#)4J{N)Z;2NxFUb@f_Bx zBZVxVtIgII61bAgIrbFWM)>1>|Fj&x$Di};VNi40-Kae*)T$sh(@teLX2VeXzBh5s z0zqAf=lR?Eg7;LTVW21<;K~$5I=($P1hc*MyOO7vc(DLr=0cy|ov2yb{-V;F+a;~= z)W#HlxSNjl7Ew;@3Xak%yMZN3ap*ZObnVFzq16)A5%*ie5Qk9>ZbaNx!yMY68~2^` z+eQUCwc@w&a&=1i)$8V`dN`z7`M%b|cVJw;)9^y!et;8nNac6!L)+7MXC(MB4mm9r zrGtPp=z0T`+b6+ZtV#xVG!$<)6G*Co*<#N^%ZYu4#S9@sM0I=3vhLT#2OeI>a|bm;WSGwt8*oj1O|HUi6A)EeX=5d` z8M^Ui81+ZDZknn4(Sn`*v;vp6?`u`Qzp>vaDz|jOyyaktB07_pAKZ7ZJt$>|50i^0_-o zofvfqHZk9N@vHB6)YNTGRfjY&T`U^+JT9-e{&kghwC2I0wmX-15PL*iZU~@gJkmdd z#4R8dIhq6%H3Co@IZ&fX=2apUUA3)k1+DD_Aw;%yF)14hY=HFDktBMlBwzsfI}9=1 z6cB_;q;+%RFgAdwjJH-YkI-eUWi0P>eBr3#z4g779Kc*K5;K9eP(tP<mo95rmO6E)Ch~1HNMyYh*?CtzXN&mnS#&0d@ch!=R%mK3qf%|JsMjapd+FZ&7 zZBENU_et=46wRH9Wi}ms&ZrpTDQ)Q>Gx5A<&F=uhD9js_2bHL_iffgZ;9cAie41D= ziYKkkS_ive)43sEAIs+k79ofjFB$+GY&iyYUkK42 z=#U{@PDMHk20t<@Mw;RPztlGAzfjS6qRaVBVB)t*1&z26quN?5)jcq=L)lf)7@?Y) z$m7g=omX3P?;cK09v)TcP0oD5-F}7r3mdHjZ)6kL_8{7RvaWz43r#!@-f;@vaohIC zI5&}>5f8H#sw0tJ1)+c22mIfFpHuG%`8Y4xr+tX*Jn8YuHo-Oq-t!4pDu!Gd)`J>c zE+m_cSfsqBg~j#-HUQ;Fv`F4}XU`pct~5ck8XbtV3-C1_9dUJ>Del9gDHphVrA=Q) zcGD^q<(6csUU|V6DK4F5#356V)>kYDVrK-ndr$m~E;PV);`Y7EQsM7ewWL=7E1az5!z-+_~eYdBehr5mQkAEW5ZWT@1q-E4a}N- zEX^lp9f1V>gsgU{i#j^jhY1Rtjinq(oix@@17?Pr{Uv9>0W2G!FJ6|J*4Zc zyLqIzqGfO(gU;X>*4Ugfp&El*HD{Tt*@3yt1CR+wy}SmSM;gE(qM|(|HM7hj$x2Fn z!d5{06lf^_H?c35kU~~!nf*M4CyxdMbyM+T_Df(!t+)tpp5a{7!dma6wSNQ{qlMZn zW=kEK8mPF4?Vv6DBh=+aEN4gjTwa_sF`y}7H_dG{&GliU8-UUsf&{nyX;`Hg%GxnV%IrN>R2n1f^D-%=MwuVULXjw-9!H5C1#tCIc)pGL=HD7VV9jW*5 zoU5Rv)e(!#a%8EHf_OSN>%N>?2D@vfTVhmtaif9a{ z8xC!_QBLP}7jmTR2S}x!-gcwY83n%+CXQmE_C_I8qC(r7n`7l4tF>-sHVXU_xiwq>iwCd6*FWb2VK`8cF4!1Y&ml>SO@M!!F_Np=)1-M?mxAR+-y04!LMIVoY zIzRSqwHUZ;-$y>IRnFr0Yc0_c5k6iVTh+?`ph15$|)~1|si0FQRbi%MQ z(|cU4$M%n$CE1IJ&od;>jdj-w`|VNUTd%$%y(IL|0N0|)=qTqfPh&3HQu7V+mF`lm zNcb^|NXD)}q@2%%d&mF!ASzR4!}ED&g74$;05s>1!@Dir>q#3L=Tbw`8}4n(9c4h< zAB^s}-X`(Wuby^9{VK0#+B|&JckhF!52xoh5qQ68*{nGF{@R6f=RcKgL<7g(w$Oh2-rUpPEQ(|{@EpfM`D!RSed=+eD8y-dn#67{|a!+W# zDYZs`?c|LcoMx-7_2#r8c^S2_Lg&sA+M!r2pE_0=Bx}wPeh6xRH=0P(pX;Jq$kAG6 z_18r)tYB-!m8)s?Ke2~60SNq4yO-T2$wn~G1bAN8_or!=})BJ0ak{Sf$G+4#73f>MiR{gw7Cj-5R+6FSH0 z{I|SJ8k+0-(aKCmH9syoKMs)iUKi9-Shi}q*%;gUq4o6YBL*(;yn0=D$ z<^cx)Q2Ci-DX6_=!$}7gZJ~*43QXdq}P)g#d3Bf?@6D`TYg7uWY!DJ zz=pi^h6XhfkN9CYzM7pcC)ZDI8aeI$9HJ?)i0r)I!t%XCJz}9y^8!P&Hbr~0Ij2qx zHm&Wl-I$4ZJqT+}N?50oZ_)qq(>R zoGw+m6h0`#u>A)@hYm^>Lv%k}mFk`wSYXi>9ugh?{H7tQLdl4D~PF&k+G3`(A+ zRGa7fn!vn(p`RYXNe$)hG6Gpj^CDG^ShqgdwKu`n^%}6N3nGO6VDnvw@l0%@DAv@T zgf5$qy~btPhy3bm&4;t8*cQr^^vsfdl(Kvb2gW&0!QYJMIN8K<7_0h6H}L{%$jxbtL;*|8h0 zm0ub+FFX>NC1CZ6H#sk+$Ot=h5djfKBqi|cdmQ1@08S8=!DvpwXaHM9T3|9+OOsGq zXaleqfKYLE3tQN22$aJ!5x}n%!0!z)Fb-7=QBqiWCfuO7WM}qK7(7mYzg61QBV+Fya3Axe9`{mjn|$S8*36ocd0| z1_0Zi2PyyrdQcJb2Zt^8I&#oXUkr$}bbNDfyV3p9>lrkq3Rz);t0|0M@52TR;R1$$ zR;EBSUX``J{Nlmu*_`W?Fz6>%qF!X zl^ymNHzI0=iHNz}s9n}zP4?Wnq{8GTCDn!n-Misr^Sv9FVOSc<+;h~t%_aFnvF;vA zA)V6^ANod{SzT2Ur5uXtr?KV_8k6rNhqy`T`20T^S;MKCU}?icwEobr3^vn(BM0X0 zO^I20l%#Hb4U_~VVvqfc-!PLIx*>B5_NKGRT%z70Y9TvxQsHto(cW$TU+beCk#f~NVJ-wAtDqx2(p%v}m^ z`m*I-Ig@=HW-;T|)x$8V=2@5wrMIVq^Tj1<+GB|@UqLr4NKFgY!#*nJLaZQSIu)z? z#At(=&f!2IO7OFqFbyn{Wl<{nu+9uxqbpxK5u<)bKeAup6kX=g%bch-$!KzluovI9 zZ2z~{6s{+@IZyuIOi@k93;0uqA^g*xgCh^F1a$Z}tt5!X`8p%+YT7Bw__gP;Us?R3~?gwQ0(aPxv|g!t<0#`uP|Z*~;lbbH7a;d-Ub} zl>uNi296N*JOl8dCGi^QTPFxx6ktiEW1)>&y^;f+6UnVhpH zvYi-9bnd*=y-F4ADWF6KxKkqRWaxW4c5t^$v^JB|r^)8N6dV)CnBR3thu1cqG6~U- zo*IbS!oMV!Ovb&kN`n$XaUuIqR_HseEVqm9uyzsCF?vy>L z$4E2Z!jZi??vt2C2)JO>`_q&u$N&w1m&4nFT^+vK>uK3ok}*AX>3t!#iFNkD7#K+e zCrPa|EfE;W#SPgDJVwxE_+=s=(=17m2Ic-fS}Yf1`S3>@h}kgX3t}7_3WHG-^(6Pc z0caXU@Yll~_ULso5;+#bM>1$zYGZW{O$EBV`d8nU^b4`hjd*DzFZ?@a^Dz_BC2A-7 z`&-A=jqcN1<|CHtc--H>(P+0;{p5&~eo+UEv0+z4FjvM$;CnlovC6x6-$TXTXPFW< zTeu#6@0VCsX##TDi@pvQbugBOPo_s&k5jA476uxL_SQd})0tH2qa)Jd?Ygu*aA4p! z_<1#d4~Fq0B3*_b32}sq2#Ue)>?JXveM1crE6XR-e`YN?sM!hG%JV%WqvZ!VALwMo zo@7D4yPx!9%L9|q4?$aTWU%u6WEF~K`C&lFxw7-n+V@qBwp-9l#gygwhwkui^o86n zhl-EQ6d9u^Ds3|>!02!Qy^~YG%x1zg>Yplw-S(h#CCD)= zt6OBH0WhpXdatHIqcu&IkF%!y?UT z!BX(?S1QkRun?jS&I<}LZeHMu-=ARVr~F#}7kt!q{MShYDC;t2h5iA>1LMy^o49d4 z#yIbGm%aIy2^UB~24a`!pCc;MzFbPC(apX3?9s} zT_#@$9?XRQO?&#M;H|2bsRS(!DBWmPHy1ZXkiHGDNXsv71=c&(v+P%B=Q`2Dy$sn5 zL&C~tN`VB$(k5XJP^T9E+wB*U*{N=a9%o2kg&>%L+RH27nj2k?K*X=Vx0fFna-u>; zJox)Gj$#|iC!RZ?Js9%E-^P-CAd{Br7E4o^!|D&b?L~rZ##Kiv(4`mLz4k3~gE;a4 zc$KaVdJbZwFM?>MtBAJv3wmi(@?^?VmM88WnOz%$pS|cm*~PH`;qVs$lU?S^EuVWo zYqeutG#v;HakI~)6z6=t8({&UD0WI$HbCPG<@JUJ2F^c9yZNVP=V!C}lnsN6P^w5I z`k2TH3q;!Evbhj{NmPXat_+wCzVw=!6j`+b5$1RjLA0K@sXseufe6}eAnqCH|6g?7 zSQG6BqqrY{_D6GVByK@~7P3h7Eoc_kqb47_ZN@fY#DOp+P%s2#61?LayrZ%Gh+$>2 zV=W9aPEg3>9E{eCXg~?Pq8YRL?~iFu`I)xd1>q$KfBHzzkmSf`2p+8?W{$7)5Pedu z!KDoKI#}oIU8~4U5PqphRat#FR&3lDz!2S&uoq*m)pe5)S&P0$C`j8vGO*PlwqO)O zV_;@Fs#`6*qluXf)=M=qGkoO9gb=BR3fe-yjDHcUpB1jSZ2Z8qw>_#=$=HfE9aY-y z57xKyA2jvnW*a(?$hqu6+*YX68c;-|+4B7&_@BMuS5W>ZqS*lL-63f2@Tb-#qp6vS zq_&F)wu=$~UI7x>|IoDmd2Aw$Zw*54#B2X6ddrk>#>`gSHaKXHI3B4u9$j!i)SBDc znGM~zVi2A$m-qJ33%!$_)QYcS_7FRcP_E**Wrj$RFf&6?o&z(#fxa;0c(=Qj!mukE znen>unC8lSo;EW1=z*lX7-72V?1wDE2wnhR#wW7;{fy#|Bz&8*KP+h~$Z7q!lBObW z7Y}u_9mY*L5}89xoEYJL0{EKmb>wlKWoIWL{Zh~e_#%Jw!m&7PI^CZr89uxerzUjp z$<%S|hOwZ9b)T7Pz@?x;P?WW3-#wg*NOYEga>jDPY{QVlSk9 zh(zSH3ynZ0+;X^VNn?nA`J&#|^}38NbD(RD%I6CJ+r8-f@_LRn`$cRf%&|!ibE+_o z=mJvsfZ3>sriG@nlbNUVbv#wi1Sfhr(L}INF;`f-O|v>YWJU_Z<3N|f%{_l^&PZLM z#XUFQsP+>QU22-S0GKIV6P->~PRAsh0 zi-C70IA}bjD*YHQVB73-PrcZwwEL#O1~!I~%cYojW`>_csL~8!VDL{r!gMX%Y&$-i zNCL@4Hc^8j$Plf*6uMC>H;<}Ydj9^`d<1dLAD$UY>0tG4DhNDUIyZJ_TRL0N!Dnw8 z_*?5Q@h4tj+jY@;RObM!aNX};mVFFh!i&zDCbD}FQX~rRhMsIn!w=!3mFj`7$38Pv zi-}6_mUd3<70nGi@JXpLgPjiOIdSQ+){)@8GBr&;KY=vXmtQuDRg8*n(`R}>(+PXB zqhY3e8#Ti?2WqCJP1#UMY?6Beknj?XQYH6}eh!p(B*=}kV_r)Iq7O|R=pTvam1o!^ zhQ-t!;+l%Z)YC9b*cYa|*#vwyD?1}gZCpKOG@mPgiFeVPhVqOY&5?tSr(4GQK3E*7 zxvFvV2-%y>H;TwE2rJEKvS053Vz~hV3h04E`bb24V-*tYPEMw+1lWZ@1rbq2Epb4!G#2`MRgB3kU_Is-ONT%L46P-AqJeyJyLF5;>3~b|8k8W-^cz}O5$%6^2iHnT!ndEZ8n9k_lba5 zS~*E*yjwv;4>)!;r$3X5eER+tc^7F4dp6LfgLBL}=#bo?ndpHG{&@pOaQo`2l=_-F zEh;QRvS^nG@j1c|7)23sc*NH!-c|#t874^Wm$6 zoAnhm4$Q&!p-uuaPPt|2;i5zS_5&IL-n99khUT#?1C%`c74D2OhwRw%S5K}{1q)dGdf z0);Hiq`MB$?fs#~X?{m!qupBBTK<{p z4NvI>oT$|L#cbjnuES}?u+ym(u|544r=j;My*N}b^)rL%!Z{bz{W8X@ip5O#ik(iQ z)IbbMr&%gu0ZQ9cfllH96p}zkix!}X>0|D!r`z%f!f8vzYM~lo!h!DEyy2uMQ&L55 z0U;GvB*Nz4>EWF#%!A@^j)71<_k!iL(R3|c-AFQj6}->t43XxF?+rAU!+-Yw52NMy zUtzTD01meQ#AyGUF!Jdj2AB9*!WdxANh1%`t(Cc$y(meZw*RQ=DjZGX$U~#Tps4SDqG}OfyQ&bSjeIzdv(#j z42k{X@k&gKA!!US%Vu>U4b|`1P?zHvODG|W^QuX;)VJFDb)YXBqTk;bD3YJ#zEI{v z5~t0c?Ml zri_LWl~q|)GI9f1MF3d>Kb*ceADg92E`COuA~Kf=IZbS3^N~g{O+t~paDT7#aQ0B) z3UAxa#w~<~%i`d3hp-hFNrps<%P`;RhCG#)T|HIVopWt`&` z_h*wX9We;ufy=4^y7eDaBXEPO-ALXay)qxqFsUY1iM3qF{o7HfKXtA;!9Gr^7doM{ zd&2SjyrF91qNr=kWbfJkg3#E*Vue7Z9e--gdNFK7!ww@!8=6}z za1AR)F66pkd=*mQ$_-Et3hoQe_b}X2$L2S!YxyRTHNSTlfMFl^ooQZ{AahE{wb*RA zITEE6nZgLp<7P(3`|(M}TL+5}?z`4kvgKLiBuQDy2`&q*cyr8D-2OX++zArh7JJ|~ zA3Lr|Co7`rrKRTd-2$Aen^jt4%W<>V^6Fm2@bX+p3jmg|tMZ%kH#zM$R6MSjMK{&m zP7EJvS1C?Ip3^dcBRJjuT;B80siK8GJ8I3zPpXZKFnrPJjGYv!aHwnkGtB;m)s57S zY?Zb7l#pV9#Btb4<*Hy_6{H*}(O1Tjd=>9NCz%Zw$BIxbGU`3p{A!A@yBxIi0-?-J zJH{`?)>%z7hiCJ}wzB$v80lH+cc4CPdB;- z&L16Q_Gl<#v=#%#8C}z|5vn!AYKz})a6Y1|P(Dmf2ih5SVz1NNv0ov-$Znj^fAGTY z2Bh3LRUR}tWGv&c?6;49Po|ugPz=RK>Yo#SR;{u>2IgQXsrr3$R7w`w3We2D@nfWJ z>r`edzQW+*|Bx(2H@h^UEYyMTXNb}u3q{$2m8m0zSp@rGLryDX*^pNq{B(yH>;1fS zJ9aTs^rPeTLTpt4B4=vuz&IW%2Dlp5>(jZzb^Y6s01z%GGko3)7|X3q`q~-;&sS32?t|Ykun| zpkigv5h?_UH|-4m1W1CgYMS}K2NO5rub52ZzooD|-7oMu*zJZJr?+LhRLjTQ9=%S( z1bQEYQ5L@<+HC9g)1*-+<|7i+5l60mMeHqBq5yRbt2Xm&Hz8~Tr17J8#!w4~>5BhJ zRCm~dcbX6c{$kN|bBzzTQ@=Z7AU#YT5g!eybZ1Z_Q?%A4P z`$=Fxc_@ePw^5Qt**Rca*nAbhU1=)qHCd|+{C(_*N<4@=zRcZJv@Qdq9$F=yi}nR~ z%fKPm^c=2gp9OIpeju9iD->1&Kv_ZRjLAVGm$*;}m3dWH^`NS~6|{b2%7VaV!IV4) znDYPft@=Mk=xy-P=Kd~VXbStle=OjQ7YM9AYaco>(7{h_+YbdTO;ZBEEC51iHXhLM zS0fA$D3=DdxY)E}&vs~^ov^&Jx!M7sC)(WbbapAbI2p!O$X@G(!HsSC=c{l4tmA0vEJZ2} zlB$Kz5jU*TN!8*houV^!rUe6&nx9gWoR}hAy)Zv>a00^9dF*=i9sqc^H$%RJ{=GjI z>|U4D{s<3unB|6uXBsgaKHQ*Ln8q!VH8j*Jnq!Vx@39I+rlL0<%KTRF&~i45vB#%g z0`kytYm&}W#eXc1zff-rN{wxcA*ou>5!_p>BW?OoSPOoy;eGlIzGDN!lRO2dYJjCV ztCbC1OGbj1?d;kTxb3c=0YB-&x7#IDY>iXo1jad2V#ojm znm@IDmQ?{;X3)u4LilqgQFK+^m?lg5%1zfb1t1WpPA}}VHlSPW-)}w2t@c{#cccAv zn}Ai}bhv^>?sR@cCHOg%`mmG!O>2zsw-J-c9C=`%T;T)^e9;qm-4 zR3)Wag3#g-ow(4P(fMK02uJNooKmGhee$vLzI?4jS{EsIs-7!# zb44#jQKk-4^T57-VLWxeHjxpOTuoyGhZJRh1a;)%Zaa#rN3s0Y@Xl!KGseKQuUut7QUxxIA^2}T!kSv8`#e%d|X$b5Gm5p41K zp2)0hJk;cOT;|H;22Bgm28JSiy`o&duj|BtBVF|i6bCC};z!u3)Pw601f z%Ks%gr?|1srIOo}c3qCHH@WC<%U!JY%urX5EkkcXtmS+&u&h8YH;8!{Y81oZ#;6?(Ps4cXwxT zm%F^*_nmWY-Sfw-TB>%cc88r`_jLDk_dHMAtew~Cd~6KBc1ylB$QWaX_F6-rj;)sZ z&z`M1fv2vF7Q$*)xG}*#o5i`pA-FyW!$@3+J0Cn9u1q7uc+sSsrl_KCuXNQ$2Q-`8 zsE=X(T_)so+CDDF1~$qNP~mNu2lc63*WILylSQPQrsNt+U2u&(c1!n2OA`auU<%#* z@jWz)x3%}dKttUH-HL82ZE>m2)_Vm^2!G5h*7Hm3(6ib6+O+u9i)0Fjlu^&H2y$hg z;u7$&Xb^U5-6&;;_?Ws0h;a3|uuHhNs9Vg`s-kCVNba#H=R=MY$7y99KT!9{jci*p zg=i1`!I4+UkzZj`7ij{uHw^J$$docIa+JD^pfJ5K#`N=jB5P8>NGC5^LPJ*I_LnFv9Ui_ z5W2V#I3X`P!T{KT0(_xDYAC{#jUNrkoimjpXcPjY5gM?FhKd-!hs04(&+%*L2lg!d4>#UbVnFr4Y;j6%k*GI_{_-}J;eZ-K?f~*8uUi>3K8=CJA!#urRi=x46vm)_YFtY<7 zP{`zm)-dp=EPoK^jK%rVd5OrHMCIe^Rfu4Zw+_i)ZBwZ^ zoT~urKv41fP}8_*74swv+r5MdnuQ34fBUny^c36S0%q{6ej9Lx7;pn>_RG+k6Hk}^ z9cl|SIN=!5Z!oM2S@>OS=<}S!CJt<|DVa1X2rLL|Odf2=T`)T}dkr05`XK)>C_3pF z2xxZWU?V;RT4=iB)@Gc!w_76neSoS0E_rCzt=0zI3O`8_1aSvi5mS6tA6DEstA;Au zc@7N6?p|=bAkCx1c8qaXIzJ-_;(X|xUi#`7}-}R(wr=5fWps;1lOAw!5-!iK1Ji*^-;N7>-s zotDp)ImlIc;A!f_n^cutsbC%p?BAY@9ZP`P8C- z^d$d8jomCN)_psHDCIHAoJ+hkrn-*JKIb3<{~0?{urojShpS;Mj&FOxDUZ=H@WRbh z$>qy+goZ;~xCOlII=_pq7`4X4_866fh=~{=h;=!kcQ5~t<x}iKvy#`Sed)YkLwBOLF4>pyec7lQ#0`8jWGh&f6SAZ z4cI(!eQylp05d{~<97<-uTN2y*?yOLKceCp=Cs#Y^Yz?OnY zmG!h&=3(^SBe9a6_m9?Tw8x-O@x`l*;_o0Q{_!U)BH@@^@y5EKjdC>V=as*Lee@2~ z9R9Mu;>CB?Sf|03T`|HyFGV^tPpRW3RLud;R>X2MI@c;GzBAtOVyS)(>Rk3g?sELL z=y0jBfSuj#y1hqRb$kMmrm8+fq)o{q+L+vgeJz0OF;wLe>+in`TJE5gqTby&8>==` zm8CVYbaG;X<)YqMAH=0^QuysXhpI5S5uhwYD{#REN896bqUy^J;rA7foB7`~_Y|8; zy-$f~rR&epWfoii4&&Hzn<0<_Qkn9IvYL_az_LwL$%%=THE>NYz}jo2OVr?WB|Nzk zc|LF$3Z~iT{kBj3@eg5IU=_FAX+6#av@5j<_fS?X;#uoI#y8OQ8z{-b1aSrX&s1Qzg7qE*SDHCK4HB<%`=GphR$4L#Wu1>LB7 z7+x#L0r{}S(S-5lHnVh8QP(KmW{o~Y_GR!Glkwq$2OsX~C5o}5cLk}U`7Tyko&naB z1RJ=0jG>J^l!fol8jLIBvBRgfGDAGNgkzF}MXCeZ5zqK_7|V1_Vr6l-^%a;L9#ZlYpWy`W2nGQ->B|mMns6G5g&N}6MZ;!*fI-`^f&uvm zWM-S7TorcIeQ>jBvFpDb#BuLacd<=@2Jss5*~-^ z`FSltG%hV!R+bGmi`;(qrC6n-{HobRtBoMMR<&IfiXaSVlms#t^OqMKKQGL5+*D{P z0KJvLx7yN!9QbdQFgg1waw>_!c-l_cKCBY@(Bl$W^UA;nIBR>CD7HtRnFB#mxXhtf zN{s~q%@CMRtlW$#`Y%Y<44ll=xKO9kcD{$XjQ2$Cw6G^aaAT7mn;P0#Z^Xc|i$5-3VF3XtLK0|RSM z!CrH}rxU2a_?(k^xZy?#OymxjgF^k6S z(qeg6vcFhBG?D~Y_C`%X>STWoEUo5l%%JHA;@@+p4PqPyYir*r>aVE1r>QP$z%76p=e!p!Uv;)v=109XUXI18ju4m!UCBmg|Q z#9*AeKNf~D;X{bP@FPDU zC7Vr2Tr9k6MW4$cm_o{_) zH(&O6YnLm>k5u7!-Ur{0ZVZT~bfL|+xDf2gGl^8f9GLun#8raNV!`36TRitEOB23l zm`8R?hkX|=>vS&o#L7EdtM_;S%9!w&*xHYFF8Xi)g4B@7p#_S_G4fI!QM-wYGTRxQM&SyK$PTR@ZP7?2RwT!XS^G#g+T36h7&U7^&%`J;&F9&_~eD_+{thSM4hs z-(j@Vc0o!R?}~~RYtxA$7w2sKaH@|wsrINyVs4kTywVgSikB*NFPJyRjNRF+#4@HC z0;_gW3KJbnp!}<0!HoL13TO9K-()XTV#$V>EV!yigWF7ltYE_Cs)mQ)!u>~lZp{dp zr`r+$dyLIuSz{QsG~b1Qd+t$14ZZP|cZBy50@Ugy{sC^cOQrylieIfRuE)E9`)#s8 z6C`dw%&`s!f`0gnI^H0=s1!%9w8>f3hcH^ZmabO>N|fGPuA^Tm zoEP7{lG^K;_Bzb1xrDNk3w2!#!H=~9tzq6Iz^)kudDk6;_jQ>kS$&e{IP~pE1*b6R zP$4(RE&4Z1!v42*MYy2>Ofruk>*8Tuz?nSuH?+Yt})8CFR12wq4r2 zAaO_uPrm}G)MuU)AvZNbxkC_*Z?MTqXdRR8ysnwx1_S^89m{^E3!aJexScKGVc^n( z$zlK2sD=xBLCCXd4aKT;Z0Se&Y@^#BU~5!XA2D{L4(0GX zssfdtjgeFjhDF_W8OwxM#k1{AMo&7zqw>7%K-OtQYDQ&h1Ho_rHm^d*@1o;6mdl;@=m{G2n*yU!+XgnLpV%HexrrY)7AY%b822M6J9BYt zOfLr}w?mOrpN;%;TT`9|aBZwFpZa#FnTW}TR1`|C;{pNhzjjEMWEU&P1EfX5UF}YM zLNAiO@l1V6T&1n1KIi{cBEJC<5xTs{Ndx8DVO|(+OeQwYUyg%tOYP?sqpZ8FJ+3p% z=Uv}O)1RoFkJ^pA{cFtf$ zD=;lqZrdUh`<`ykj#$q^>}7tu3gjqxKRm4#Mx0HH-Qm~~r#%|C+-$yVTu+*JT9$(0 zRevNFg3dL`c@d5y2U2;BG}m!#&d_7WEfvym<@7-CndP`?N^_^m`x5sami>!TEl&$H z^^MdUd)=(XX9Ju;ZO|~&VMi|&RgD9=y#4&xH&AY5-zH8|pZeH@o||m=>j!c{MR3$H z*h+ULc3<7Zsmk{`OP%)KG_PejrB(eMmdgh^)T?lqpR&3-ElfT;z`nN4wW<6a*udVY zfRx2L>m+Yx+nA8Sv80u&k2OoAz1BW!Q+}!_;o7-)zF%yuLtlDGZO~~q;bs1qUit^R zE-8a4A?7-T)amKzIIl5?X`hN&oO>dkegQKy@$jnG$H)N0m_Ew_9J9guyo<5o-41NL z5(jBgiZ?QH)Bl}h*(GIiQeu2Cks{%8`-a{6)WmTBRrL)qSW4;nuOP`p$YA6 z`whY@>Zif~QXgk!{r{_vGqJM&-}P}Y0a-Hue23|J37o?g^y&VPKsr>l5h(U49HBd` zG&8#XwqGp?V({R9Nq&8}G1SM?oEhdob`hQ}Dmt;3 z@hX~Xg-@j9Ers3>Z#+exLmlUNwf#K> zdIv>e%ERl0>+sbfo*fc7&n%-=;9qeILYJX9SNJQ2MtTwLoV%mwJ|az-enhgIclIV}J_^7>L`cd0O6WxJR120_L)Db8Ak$}-s1ja)%z$JUuE5v*|@B{H(j~HT? zV6PHj8O%@?MK|SwPyO*8Ale0wFVh7dWdRsGwhKM2bTt#*eSOn{~sV9SH^GOCpung9X z)Z9k^5{L|ERmAJ6raG(_pvCvG!h1qElRJQeYgitZC?a= zzoJJ!(b_Y$G_Y?+bW=^q)E~hVa$PUr@mnOF zU&WrJ|Kulf(X(j5!B6<8ABnL^8HOAW+Ws_aaw8Bt4jg0`a)!_Ej=&c4+unE4BL_bZvM%^y2Es;d>T>|2<;=Hd zbZN{+MC_UuuWk0s9vS7D@!S6+6Cg2-Win_(O3Fj@^Mt;AR z9;g_l2;+9XJ>-qzl4aZGz?LG1E?r(VPJ_H2rsV`*UKtsekk5Aaj}L7Gsq@FLa}TJd zI!~emsrSnzhl^`FCu~@qI*MRhCj`&j@&$t*KnMHJ+u;6@XHcQwGhe2Xf&hgueVtSo zrbeID@ZpVaJ*7P3Zv;Nhi{)Vj6^le_+z&1dy z!UBW7?QqIF!b~kkWch#-r}4;nj59=2>@6?hsigRt#)qiz5Ez7dPD&lCUoG=DvGUfWxX^Gw3^m#W|k0Z z^V_ST29q#h>KfAa{CdYsJ6d|^VDkHZlL>!4{ClX-fVX?LOO{@N@h^l*!HPokIAf%1 z&PZcw#8H=inu!3;x-akvHKetE{t@+-?CgGBOg=HXF!M;zdfyREacbaizPY(pEK~x< z*%|Y}M0H|i8n$_={wnWqM@2UKPanUNi|@gP8drDYW;T#+R#Oqd!9|dD$l*iqu5J$~% zv+rz;V9aqu_?KkhP*+=Nt(nV@+O7F>^905VUuQwC~H96K3SQQc$eD2f4^c z1B6jm9v7>Q9`p3dYlKI-e=h!tb^hvJ?aQ?p-+7OBgg(a6>xlG1a30%P@(fqph5ypt zBB1wkzZb=pC*pyHGP&R0 zUwTU|{}r6u_vD5ii7_z~;5HmV%vs+9_jz3W1p0NgK+6?eE{A*fCK?v6$h8IHN6%E$G>i4`g8mZBqRH0+WXzEPE=?wYG?qdr?F`tYV~96Y4w0@?6|YsiLmF$VU)6Sc8*Eje%h~mKrkbY%wdxD$Rumrg*1My zqFQ5ICOH#_mn__U;yhkzv6hO8ModkCSu4d=bIDemg6ivVt%xifHwE){&4ES5^^D7} zSN8eB=h;%G$Sd7i&4o&8qkHk&^AxjnwT3!woX@KEqC?mOHu`ci+qT^^ ziqk|V#>MStg*dsv8xG+G!^S5}sFh5Nf7)~9QXMZI%^ej-*Y{rb&u?4QIAffX=HZ;QyY(|JluX^r;|ht7nkgs8jE?Fvqt#wOvYxoY@&KotswnP#hUM%S5LV& zZ#1Pf8bsd`KumETmUH|hTl-6JlR73SHJ`2FU)YN;al(r}5IAm1eWXcQ+F)#*JE)3x zY#sS|;9Wi=b)G;93o3`$v5PbRVV?o;PaB`YYxWGJBMh%5>=34~vex!WWC2rpX+LNyc2nprVvX2iJr)N<`B&Q`m)B3mHR~J&x&9x<(=`4KCi@@0P63|00%D za&fKUVEEI7#~_)bABw#-SEkdmR3oW>U88`O(97*Am{zACDNq9^srWPPi%T?`Ub>i6 z%8r!9%pasc{OV#=x_E%lFsr1p$rvp63a9>>ZL25c6=l}5lv8PW&i&qe@b5nlIAOFF*1Gfw0vM~$;e7s%!6HPBNiU{Oamr_)( z7S(9DCv0#r3}a|R`OP)$AD$qtG9K6phc@w*ql@<;7XTm(&$uzEm4NXmG8e+Uqm-A@~%uNlR% z-L_<}cG}F>QHZ8MfDsYFU6}`BIWwk=bJ0M@fMJ$5P&|$Jme83*M}s!opV_Jwle)%3 z{fXW+y3kvG|AVZ5wi5>GT+^o2Y<}Hd@43cITn*dAM{DY%CYA{@Ywji)VOj~cj<|ta z*w9b(U0f3%zvg}k5uW(g{xOUegs6D~rKbiZph=26+6Za=4q%+5+d^@V^()) zHNXJ*M;M#R0B4ixDT?*SYV7;Snbl^4=CkkLc*$QQ^1i%X!8@>8__oD z$PhcfL+@<=F60_`oR-gqDr_aMGZIPFm!A1JxL9o6wLOCm?8p*Hlr*!<%tD<0Y@I~O zG$GPQdXW9T8H3DnIAPGy9Z#QaRX@bits##xq^9a^ZbN4v!h0y1CDJz-g)MD_f7JL6 zZME>nY2hOmF;H-;geHT+f7}w_*`t0!pA4|3Ce?6Q*Mw6Eg;G=`; z2Km*9xhF5}g6sA}uPamj{b5ZV)@{5tNp7JI$cvcAa1{f_bwh2`g+R}YgXCAACs zt4acHsPa!jwE>x3#?Dh{6$+3k!Pfv=V#Q74;wOpW2sFYL=Ri_-|#V+ZmXU z^Cf>!SQQGha=ve;<}ga2O*@=E*^2f6ku&gAf3~{uFfDu}vq9aU8=4MvckwE3u|g}-M9SMF zt$#^ssR$%HM^hO+sA5XC2xp-ZXgKYy`hp zKUiLVb5-Dl@oCX|62QyeloyCdEmVwaB)bMHG;X%qx`5Orl9XK{GI17N!h(R5WfTf$ zcLIe%d7q3BMHh?IHPoks00F5Fdt({D0-zjQZwWiouAaE|Yf+bYC1I{TQF%NFvd5Ms zP&Y5-HR)RiF1GQ;Uu702*E9?|awF!ATc{H5+(NjG#w(@Ic_DZX;t01uR9+_ZhwK%u zzFBd5{K51`lK;Sp1rD^Q7qmbi+pia3$KmMHM4Y16Z682_=$bV zgUKxpuG_KewO|nRAfw7lN5$g;z3c>s7xw7jPHOWchpoq)p(W;21~<;mXm#PG`yXNM zMoO*n12>-%>3x0fidur~i1VN*29V8a5>vUwK8@!2#h~3H24WJe<|nzsf_w$d=RC9J zh?EB}sleF&+CMbfYN7Ol5d~%kMgr?f$2x}V(J}W9L?bTwu*DH1`(J~z_V4CeVmrF< zPaAgP2N2lou38O!6faE*DqqTN(Z1xfzW~CP6cR2ayAX{QyoTve3##?CcOYCTzZz8o zF#FmRe*UF{#AHPK5u#SC_)~tf-M6%Zj)(4+CJ5A39VJBc{oJ~JHW(qZSafJZ9oM)hFS!&r z=f}@(G%pcr1$=2Fh9z9Tlb_lcQfBr@MBIog891a&#{`0O=cmjz@*E@f6cVO%yF4?B zdGfn`f{pmH0F}D!vL$t4%*l|t zg5K4{NAtgp+dmrO;E^|Ard3$xE-&5AAkMeGcgon2ug9ZY^W2d&Gs0T&f;WKc=UUYGYvCE1n%IpjwY|Y`^$p zp~RM9SmfUFsFlZ0qu{uA^GP4fyKIF!J{7MA1g);NAvMXoA{gcW&9viqiD!xs_=Qi(% zN}TA;k|EaNQ8OAJu34PO`2h)=ziJF@?VI$Sn?5#e;-i@;hlm_a5vf>a(qT0zfFV%P z5QM;v>wVjNtN8D*q0~!9#gijr!0gj?uZz0)!Iwed!u!XTziS&s##99rc_#xib~pZs zvkSS$9p$E8<>Qxh9+jM1GmpPhEB_oBb*P_((l&Q_M8~T*D?WeIw)GE<;HzK_qfhyD zROjxhqTre(&%M8_qNRh*99Icpf1d1546c`+YL?B`S$lZ;OI4Gvt1oGWrcb%@h$&UM zM9_A$&#foUtEG3{)cmN!)Y$(ez{N}$Xz z@!Ec=C<$vnv-y-g!1qQHuo{4$82g&_oP5W^u)=w&cSgf08;##f+A@L>x_c8;r_u(n za!38UWyBe^JRDDLXV~F+b_-MU7uoNmFb-f%`$>nT*j>bh;QXt1XY&E18y9-AK09b_ z$UHurwCwb#VN<8@;PBVqfeyn8J{oc1)y6}ph+%)!3>zJiacjMf!-BY7_65WPsUHFZ zih5(~*V!}%Nk?GLnGtCwnCa%Yz#v#I4mX%r0VBlWN&s199QeG-rn~AF#sH$%#sp(Y z=XRunq{8vJZ`3l5OmY?X_a1_YT&UTtXR16~RP(}!d+;)>&x;&>Yk;ZP52i(RU-2e; z!fC@F>x2`*uK)*T|K*L9{CbD@cQ*EGBrUcn3M~B)k!tyM;C5oGmx;6ISMI&B^QmtH z<<5+rTAel?SMBsC0R^|I*&OmoFu)lL1&f{mmgP#Nu~uB#rvwWQTjf$060@NJPLrPk z^*!|adHD|YZvG$FQN(-LU5q|RVl?PJMcWr4+X3 zIvi@mDkvP4m>zHD(6MME*8 zS*_Zp5M^7py- zb`t?n60)Q4qjtUfWjTgusSat%)~Uv;cFUJJt(uKXn_1|Q9K#vO9J2Sysfm1^H-4b~ zBmoH72#OE6Tb4^QyyQJz=HYmR z0cP&z=Pi~2V!zYVqv5mZgOy7FtX%c)K;<3nAgNnCuq0C45Y;JCdEr04pXOWUski=t z8dyLCs`$)IVI=W_kLzHx<)}XSV(@Vu%!Hmr5BR(flzH|I!~~|p=l_EK2aDj7%j`R2 z$Kx$gC&4}ERu^)4CXgg50fBHsRb@&D z=gNf;6`G~MG!z!}56^g+9ni`@$A)*oe5>0|rk}WE7T|?M$Q-SrIIM#?D)MDkHd5vx z;b_rNtxOG^O+Pi6BBOPK8wI0u&YtiA>(I!qUo5jC+1MZMmyN~_!)^qOmnjssW&d_V z2GX!_jbR55seK3~#Lhed0Xr`8M^S$176`|>Wop$_=VxvAlb1T4(?b$g-sZ6bInxrR zreG1_FhmGw7B=EhsO5+JPle%f1SEuFX|@3tD?23lrF~HBs*M{G-Qb2rFc|>2JKh=S z@C_+?$)&@qJ7rCJKbwju_0We5N4Uy>Dae+y1reCIJ3Hlw0ZtimfdMIe=1~bLMh@0d z22W82NdaidrV;l0bML94e=w#X+RUTS`V~CEt3!Im2W$*tHNbSg8 z8oW^7HCd)=^8$=#t%wJbcc;o!Ml+krCi_ZM;&on*lol$(aAf@0WU^V*qPz$@C)Zb; zX)P*IX*fv3f{QeItety;OIz<`9(k-%misSzr5+a1P077=NeyMUFo~g%=6ILaFn?nI zNwjet0ZYPZG34#)4PvV~_x^t?;c@+cmGIdApAz1WC<0!)j=Rc(s}I0IPl(SDUIV%a z$R0_Y9}RoYvsp^*80bIQc$}}_VM{Y~Yf9F4&k3QaYrU|Cp?1Rzh#CLrfPa30{td3O z2*XVBUI}=*KfTBxtQD-!VZ(ySRzmz0gz)bw@`(aqfx&k+Hr)sVbOv971$h?Ono7ZzbvD2tgdz8P{p)YHx1&Fhwq9)B+6C(sp)8lF32@$?j56stB&M<+kZruxe(!B> z)S)TdoEf<<_3M1UY3ff3z>lE3+RTm3& z+klApG%Im8ngwAJCf95Y$LwC^T(STq_s^XFu<0+hJ}U{$%A|HzpvE9@BjNtncVzjJ zjaE)oy&#hEi!u0j*3S3q*I;`t63gZiwzZGtqSL#prlF+H=t|Iq2jt3S3kOFg!QHy`eQEozGUZ_A7Z*U#WvWZ{ABO zTZ{rKZwe_kkJtnCDsFWDZfM7kIkMrr^2HQ5rbwL`+9P=T&NY)Dc#oM|ai7(_wtdlT zZ(;gdPqZnU<{mT0i-+KyAu-J}MzN}VjNq+(oc>=Q?Vy8wYR6DOmbAo6BzQXjOrLv^ z-G&V=)p$g1cYrU6HZs#e!M~ZCZpL9UZ?twreRnkUez~NSQcQh-kn~UM()WGV6KVAu z$ou)KQs?b~Fzo0cijl?i+?vwE)x(#ulmt0|#iY&YcVSOI$Y!dzTN1U*EisR%Jg2Ts ztB7iY!U=Iu(5Fq!7wg98_o61Mbubsb-u`GF7c{lSoi?yt#*tul59XZ9jJxt5PVPtHMu<(-2o# zU(Q~!#aY8e8nhtTz|i=+@q97VySo_^JY&k)G}#bQfx%nHO5m=`g-|(VA8M6(h24)swSVf+pFHKSI18dM}atC$8XcKY5O zv@v%?lgmgZmh6?`L>7k8pe&7t^{x6x4->@=OI&!j9l@qx*U1ZBA~n{i>DQD74W;;M z33?oB%()4(bOrdxRH2jr`_Q?B$+XTO*>pa{!Ih8bOYFd7QL&iZ(DBuC%5m{1&O=<$ z28GsRPXn3WgStB?-Y$9?8fVEA_cdD`(cV6vp;qJR^M1=dD5|)KNM2JDjGsH?96C(KaD|d7TzaMcnUkMKaU^W}P>)Q(U10IT5!F=@ zD4aa-6llQ$w_+Y3VkNcsi6rze#+bN7DgqBq+fMXZGS4z5$~wweuFyUzZi#QwGG+7+ z5@mZsNbiOr41BhYO-hYWp^*cDPRi(DEwQ6Ecl>f6m|&dd^Bnu0LM)tmwQl}rnf`KZ zywqWHJ6gY)??aj-QX7}3^yi{Y^6=B&J4S`;g5t#z&8@x$2;JiFQ4B#zVWvkBh#`d` zo{YgY49rvyQLRFS8-4k+4ds7KohHvRrm;x^ZGrwb1oGDa%K#piqaEAxCoY8UG606N zl|%mEgOG>*2Vie_}zQ@U$l1* zv0Fw|81!Z}>Y6aQt?RO};*gef55b+`g!3`sEaFQqY!n&Xn^udu`~QZ9>OH6BDqNT| z^-eF$#J_!W?Olrpn8pFah4%8ISRyf`sI8O!=zRtFZw;?boN%WiQa>ed3sW20Ulo8IPx+)Z1E&VH+61I6P%nuO=eGM2Y6;Bb}l;sZ7 z$^EeV<*vB*jWE=%1#p8ef1k}<$(i_@0(!nj~DJ!E$(60$z1=bSOHydT70t(w(~m=4_M zQAbp^kyjj$wLvJvbXZf{G_{W8%@ct&!H7RMV%v_&Mo$dhoWbsyv$I$^m=NO(C1AHa z3u1K@aBKUqPJQ{Vi5jT`di(7~+EpgLpqp@zORp!r0YeZE6Mp{~)a+j6Da z&eWhg<6c16H5a}_zC_6?E55TXN934{$r zoqrkYr-aVIwQ{nB6+{a&Mp=pUOkGae@Y+01l|!wx>;Bvhaw_q@Kt>JD3C&n>>7G`W zGOt0L$p`!P1qX<`A+8=#E{c9r)gTtS5s3s`dqe?xRDMsv_>LhA$vU}5=~+HUnL3+? zFqm4)loGyP*|c1s#B@o1hW_)jpIB7SkzW+ZbS9{F0>UOnLS4Ejyl?dD%I~MG5Og zTrOcQ!9hS1iTJ*v1;JTigp)tA23uUUtqnr(0V#WNai62Q#IYs?(l@-ie%ay}P(AZtw`@T1Ld-YsQxCs6; z>wnC#&Zk~=Ac_5JFd{-D8<_%DwjH^LW)BrRqj@YJCy%t|zE(TXI)sx(Hdn$1(|z=l zz*VGZD~|D{5kw*@Z2Y)`$SKwgozYY9?FhLcd^zGO<&SzOM-{R`!8K|sCqTsL!IJ5b zk(Im_SZ@iC9U|80IS+Z$1fWSb)AVis@!YG$#J#PGf&%cPqBx?F<^skT;~OZ(W-nKYjAITo*?^{Vf!CBHuys zp`3T3Jq0g*J89aOVSY@(;i7q;NOc8Jgz8H5sBYx4d++w8ExoU5%sR)USAt(zUoY zb8V$01wyTG>zuu~LO=Y<Xbe{T^< z04?7}x2Beu4~rqT&f-!L7;G%ecZ%rf_Ik6F8ufSQingIe9+G~*f0J^{1B17AVd>xV$%|Dx)ZV#va+3BZ~pGB4|2|xCNtIxA$*3C9Kw$jhiJo4@) z<6~RJoeuey(m3MXpM@SKl!n5%qyAB0;#Bw+FM?$=rWD7kF(v{AYWCfF;UHMw>L(KZ6VqE5c*eZTgB>A*HwIuYg zgh|Twi{2x*td125oARH;HN)N5yX1Z`>P6=px%^tp{D!P7Oj_b!*q*rcU4c zi#5=E^Lz=_=ckjqv9IQd2r|lu6HJ_J5lT}%#vtWhv`^mXNk1GlNrHVWMunnj#F+go z^yNse^Algf2Yl3c{WSN;TEVnOK8?_w*4;DQ9ttXSB+1=I9_p%ZA!o|aR$S;NOQx$$ z{f%BG8WW58?d}>)bW&{@ljDgPPE!h*6t#A+-b^)Bob-zcP#`gSh+hvBNd@!zuz3#( zvaBIbjnpoLmIz`&n4RJpO8#-PLR|INOMm*QMO@hT@JA>+8H%$~Nr_a~F^D9L&sx z-^9Hn7(#==40<;x!%ADSyz4iTBf0EY=O3yv_Mmammaz!cT2&J#$spQz84A}cqYt|W z9l=g+^uTO4`=d-2=2pI%w9q2W$BCl^6NssC6J-G=;>0UdLj!fE?m7K&?%B|TZ{XfL z=WQUlQNq{6YO=J7do4LvNtJ=0zw_s*$~W_PI<>E4(urd=p7(NC@YRuX_ty23tINV$ zz9unH-AcrCTaAvC5h>=_8v@Te{NtQc?X%nijs9NLSQ;0W1({b#I9@)4_)SQ7-*OPxDX))Nd@OpUf<$LQ4H+g4XFZgQ3rdvr4{}(#bjnBxF-C30=ncT!Y5wfR9-o2hiJ!15RkmCo zR|SlypFA*6guOJTxVco@0TA=?iaAe*UArZyi_;GQT2V8~!~0GJ;&aCBdaPRCK*d$# z0kr_z0aMIMLZP2R+k443yO{YuGX+oib{z*dH{g~iSM?wyx7_Uv&(is@ULzv)v&(n#YwjOFuE3Dl;=V_& z&-nqKfnq#lm5RfCN;%vKsS3@n8tS|u7)O6+YB6Mnw|D;!W8WBEX|uE)Yhv5BlZkEH z&cwEpiJeSrb7I?=*tRjToo~;)=Q-bb*Lr`Pwf4%7^xl1USJmBJ*HztB$fP?X1)*0G zq0yDgHd@-)rnje0505-W1BH$XeC5YupJI3OM`RFNnj<$v>B9|G(p{H75C8>ViQL<+ zg`Pj@MT(#CI^26n`@ErcFSLWn4^>(K|Gz@A^< zQOWGQL8Iz_T^^}$j8u_}>guu?nm}}@Q^#+(l*=s@XtUhVUIB`Z@PCij1rUBtT^(6n z*&;_aFU}tE1GoANMkOlKLs>Lcg;*5(*od2>*5Smrhb_GUKRrc-`8Tb)y~j~jp=g89oB zV{xMdI;U?DI3FE9FihY54tI|1HBj+1%va#%>f&Fnf85OdR$2NTTD0r){DaY61W-=_ zLh}tV>nb4btA;*U2cmzuPc+l|f_BBdFZR}MD5u4tNr`M5?*P37=GRt^p1a)z>^*Kt$A4o= z|K`?XxPb_z9WWJWc|)|aVXG=zh#H8TeohmDg-+oh*?BvV;PlIgx6g0WG@R70WP|t` z(d`XSZkJPdl`=GIIi^p}@869Jl|W!zSviE}w-&F$V=9r_fd{>37aVlmj0a6oo{m=a z^t%+|YgqRJuFfV8^d7^|> zeNSsKK`0C3FzRn?$h6%5@kpiX|KgG5sei`Go9Tq=_x#UTm39C>d;|J$B-HT@Yiz<^ zdb1Hzx=tk8x%oz#*^LW*WXFOmr@}!q{%lor(ZT;8;!*PbZ^a`$=>U1%oE|uqL1Fkt zR^Q6~Mt9@-q4tkXp4XxZcd3N?4kiG~_=-Ha1$c6)p<2tXT}@WA_;t!u@4-X@;|By6 zbVvUd@n40c5Euaz!Uj;?ABD7Z0CJKti1z_H`YRo$53XRY&41+DCs)ao+~#CCxDR=i z0%~|;T+z4y=$rdTL;w``8YUKS( z6VxT~8POA7=jS)qmxr-f{?E5}1~V+KtdC57z#SZ4ilF%xRQdq^mpQlm*g(wrF!kq7 z>}aS!{Y}UYg8lQk?#Y}_hnF1|l}w2BF8>%EHmZ5YFgqKwpcHbN$0-8ew~w){safZk zk0;4i&K@qJ>3K()xv2zSAA>Zw(CIk|2Ay1E!=*u<^R*Gq4b7hwQ99XlpAPNn^*o?* z{$Kn0ZiaT6Nf=0f>ksCF3$ovBt_j}Jhaq-c0;q(&|kiuQu zI4$kVQq7fs^5v!(%HKv{(A4vStH7qFqCR>C-3XRu$U3)ew{@PW|1>sBQo3U!yimum zsMe;Th`q968~g1`w(JVk$4xO$2IWS>7lZL+cP#3sT#b}j`IC_QD~`}g2Z0@IO1F$X zdt_{u*AgNa3oOZT8H{fDK(A@rDUOdXA=}sO5|#0J=T*Z^%Jl=K{B?Wj9g0*&Wi%=D zq>7x1Vp<_o&8f+)tu}(_q~cwzuNtYL#*C+Wq{T~;2tB=ZC!*8pxr(Cvm@{RRp4GYS zV~#q!ol1PMKsN_V{UuS2$99xxD=dSwW;{Mf;-o?qz#{EHxu>HN>SfjKzv`hmX=O5# zoebqD^)n0tw?D8{efZHQ9Ow17x+i6U^N?GiewLXn7lt??H$6*>{Qy++e5kb;?eOel zcQOr@Ed`QJ_p$bl(@#--xA>+R#Vl%EkQlbx1SY@>K1@|^rbCuq@@R{DWj#P+A-S4C zGxBiWGz(aKdL6ZgP)236f@feBptL@kC9f2yOYN8!3>kg9A929g zj^KtPKDm`v0t%+?Nfp!&Efo|@Xsk*eJR%a$%e4mf{%!i)ZBx-+zW6oUzPaI)8pW{c z-88bZH62CShP?Tehvgi_VK-3mh*H(F-sABB7exD$_kTT7pJ5d;sr>tcWSXGO|6n z`e<}4by>O6H#}aS>yj5xr`WtEFfKk&#ie`~o)6vxo36vR;1tzEFzZCK0!AMu=`i+SPc*3^ zul>Cw_ynQ{LM|jEelBu^Y~p1C z9Mj`A_0gTQRX|;n2Fp&dZ!Z&7kfVsIn!>YXsYW3N^fkOH&uvjkW-@|{ik%bb4eQo5 ziy^c}=WXQbMk)yB8p7Zj>Z;*)#og;lqCwq|0LCl#6>v6kB!+IZmZF{@Ru43ffnwnCDBhxgU zp5rP~X|~=>c4Yw1k*JCvhaoVlMr-RY7a-`a8^QPT(DXL|Vd)pmrb@!W5*gZs;f=tLqG#ej9TjMZ#OEpCh-^uGpCdVIPB5V5llV_IZC!V#h)z2VJX!h3(4FZ}M zLnjbOk`A&SZbKkjnP8TnCqQ7L4Rltn!psLft&({pb?H?>El3i<87XEp1xIYqj51jB zE&wH0TSk>4R_ng-J*Jnp^{G^Z`UA2g)xG(0LHwc{m?taoVPwgY4DzP0T}h~5a~v~! zZJNntYo2h%pG^!rxkJjrJ%5oP3UgRq%|S%q<5v)YX&@Hn^H!uR(b~j`7g2f4yKm~&3yo;=>1R)+m$|a~$kkg2%bR()(tyyE&jCYV$u0S}G_l2QQzQ=TkaoYnC~=IZ#SYqBOVu z74Ih0yJVpko+f!+nxFGo{o}}0?g0@(zt-?&3(u9p%ld4?>FG5C{F8u-SH1qDjEh$@ z;~nr9YwGe?rOW$p^0tv9?;%yRos##k*bo1*fvqiFF^RXREHG>I{ccS~cbo?has;(e zf<^zoAlHAFNSuY4ljA=jSEr6_EJ-s`=Tvp7lmFe#D<~uEKu@2bcU@=C5R~Jw+E1Sq zXoI7;?i_W}c1`OtHZ4UB2G#+cy0PfxGsJyF0J3;AXa z$&Qa(Vt6TVoAsaJhGAS1CHh->-?5_9ha~7fX2XopIO+jm(C&^mNbI0(R65`5!>KKO zz9>h;>h90Smw7p?QUJ_Sc$>O)X8-i9cRp13xmvd+1O56MgX4go_YtYTc9E2!y1=p6{G%H&g~tZv)ISMh4C z--t-{TERUz3PSLk;uL+xT7DZ9kFk1+# zY0B81h$XlOkxTl%-p^Of`61r#1SQg0Z*UiEfgSdjDJl8{nt?_Kj&;o#c!RkN9oO5$ z`>SIkn+AMrzrNh^wm?1a0@}#?T^Raqfgx(o3&ZfdFIB6>*&piIQak+Ov9(xJlv45r z_wn?$peFnopPedP${bL--zBiqSpYC5HYmPOuuoz(b|6x~@*6Yb-+wXH3ihQg#|9}+ zR>E+|+#yC%Fx6@Xj*2~TmX95kl;iCpoall(o9EJ)l`<50$9f#O__ZwMvqV8q;S?0*c2I)8w$pF00ubH z_^q@T5)K&E5YvAS;crj-?-H0OIHXa4X3m50a{K-Ps9G;%lnl0(4lkr3k#VvOG;%c$ zeiR%+oexwiUCVXmS{%uK&XAz7_IP`8N7BsjjlccsY(@vM#FOmEoQd^Om8b`QMF|XE z=7|jVh9ke!FJz8C{BigrT;>Ce+>ub`1NToyjI{)RI~sY7XC!9_jNwg5^Kj2FWw}**Lekl%+ETN&ynfNSY z43XiDFJM}5$U}hUOh-r67Q_6|Crt|W32dFsPAFsI!yoE$)~Ovi;c$rbei4oIY_|W0 zbpP~Ae<{|N_6smuJ>fpS6rtZl>5;z;=s#yiWs{mwY7y|;&kmh%w_KeTs`T6`=ozo-JMwR-aGfknNaoFDWb<@i1ygzD=T;f?<;IXKYYf+k|q zgJB3r=|^~fck825lCeb9g4nr-g5GzZX}9Nsc8kyy^ZN;9L*8~PU3c>8*X{c|mX9#gma6EuI{W4$pq(BlGkLDj4HIBUl8l}sTLoUjPD#ahE_ zn-Iq~v&#SaXtA`Sf$Uu6N^_w_DTVFHj&R_~Xu%Xh15fB9!s$u8K4~7!sT_^ZYaG9& z?r>(>e#o5o9Vd-4jh4tT$+$vvsIza3C(cY|2Xt@fKZB*9Z8S^-5mmvIkEE(blCBy7 z?=UnanH`{JW8C4SU;rU~D9heUK}4!1k!BJFE~O=Y2Hx$>C}IrteXU+Vn$(ew6xpNT zN^A}9OVnts@1_x!Vr8+lmd)sE5|$xI-B%}69Yqe z?nLEhS|fT?6E;YhVl5E#0jCk<@0vSe@a@@ciG3PQZWlKhZmiCDJ6W}-`*HOe-=OLP zvZWK2=AUVSB1J)H6VG2X0JTTM`w4I5oZ=pPK{ zQ_vgecFOnVh?4-x1sIx4ESIy$J%##&GZKkO-vmo@bfB1=93&M;v1FOOjt6UxYH+0UfwW_Y3n4*d}PCAT?k3 z@nl9v9vPZ1>ta?wzg<`EK7NPQ`_~0$Y>>PDp>yYGhvptYfxYvG3>IPgOR_0bkfFIY zaAR#`3vIQzG|O?^pBW$9$0y^vsuVGfelaXfv^ea$l(^oZLR+xbV z%-O%kK`H@fb>R7@h&d%6Hx_c8l+;2*8&fOVY4RI@vLUHug4SL1F=vgY(UHCWI4LdH zkv?wNNtV?Z$4SY>gNeJAkrh2K3L66-KAvmTFkbgI84IkBnm;aeOGS7aV#!u)n29mi z1~ZR=iIEkyl)DMRkj~q&q!C&3tPYKW4fLsQnU*f(G|L=Bu@!U7N$6yj?|q+JK2x34 zrquO8q-acXy-jYG|Mgf>JP`e)lC{V32EvzTvxK=u+}Tf5b+<{>7p(h{<*OasfF?R^3OK4_4ar!`u>n|3lCN%ImmTrVzLE zdnYE%cqUf4x2->uS(Ty`NjDqFq5{}LSUO|qpnQWJg8Pak{}Z8LSe8wU8xM8%FBrLS z&LyZKyovTVw-=oSVf2g&PUV^LRxcerYz#;oOOMu&u#LV|l8U{BnjVT}KxvMY3z=!8 ztgjcN6{?I0kL^{oxlTo&>5DQ)ixu;di4<1sN+{{wnnlZHr>XZkvVC0HJVZZ2ynOOE zx;QCm-zanoqVb8BAnw^tBUEZE_eAb zRQjlqfD$h8BBqVwH`M<$ie;mBd+3paJ*`=lnAFBkdzeXqUkxf0ZX#7Ya*R{up?!!S zaqhJxJAh!||8@fnoyAscd+OXz2P^HldBjqG$74=_f8yTyyQLD|s3hEk%Nptw_|raT zYlm<@@>0EHL@ktjC=A$T-7>%O`;c4!Oy#+&^}5Mi5^h2a+PNoK1-eH^=`84vd-SH+ z8szCE=Bs2ow+Lyh7w?fH?uP?UjfQLnB~5Ba=Mu|UcrR6GCEa|zr2fsasmti^@4n3r zxmIy(2PCpF0k-#>2oT4QQWwbm=kGI^LHVeNW z=`$|-kNiF!u7_hWhPWrl@YbFE_%6quDOy{nJ$+o-)2v=%GKP1*c^zl?N2|6>#F#&X zZ9>AO(9^M|R~b*8eXDrpM8|IVfnTUE1$)eFWMfLtJH^wOvo-Rg{`tQ5{`q~OHO=PH z34f%&{x9K9(|jk0)2~#e zsuiMV4%3E+FiV!Njmw=o$BQqo3NWFzF+lXF@*QkDG1QIDMDyrF`MP0|T=0P#-kx83 z$zLGmH(pvr!Q6uL6ZL8G!Szoh7zgWIG5uqY=`Tknr_yb? zUCti+&g;XyI5%^h50Yp3ZJE}9v=wt?wOd?_kkZ*p?2M0j*|s2Jt~StC!@*Ta<#=WY z+%TKr)zG{p=QmQkooMY}RPV5eYv!6T`$pv#!#)(Zr7;P0Pbq^`pSQ4_eH00whF+gv ziHEH=9%dIVwQzM=_{)FbL@5#)#Y*XW+_#_sZ^=}?R{1RIU9Oe6Eb)p_<8_y5x#7)J z>-IM*|IS7^rjz$(X}sX}*L5qgD_PW~2(gl{QU2+hHrZKKUBJec#J(ze5}%Pz&@hMY zg{iN~HV|vGk!dnFc*FlmP<>eV_J6=5Hm3h7BE`h`mGyrolJe2EBOi(1;Pw;cXV_NS zWbkL~F|XAyR!^?E!8ESk3B9EG@hXHh{qhMKQz|_aUS1KaYHgT~Fzg${gJ$$@#rvap*MTs`Dim+l}#)&pu% z6{qZ3Bq0>Fyb|r2qpvJ)o=#2^>xQmD%%0>KsU>Qso$~f(~6axlMtXqK#qH%o2OadH6e>fj}F6@(Y7Zg+*R(X^PI2qZ4IKtV7^iDw^TU)2!wYH)yTa7br24W%$v zTy<6u#qkor>CL;#{d<}1X~COc^0pI||A%C1FUobnWI>dLNKKR;h=)_R=WAOyK?b!a zzb|LKq#%UqWKDPOQxYdQa#sdM1YNk83j_J{x|job$2f2(Bpa@OpAUYA3@B+ktm`81 z9CHs-bp~*BWcZo5Ys{9lOJF5WDXEy(srrgSrc+oqlANkqbIYSx;k!V{a-KsHUJlUF zY#rpoIBc*PwwpYsx%0fFZXuDMe~box&lVJdFst^uD8sC5fe=aY`JkGjb^evwUs0^R z>If)#t~RGF>tWlPbShE>uOC8@%8oV>3TBFZ37_}u z8)8wFRRSzp<6=lAuAMbqh0B8}n9z7fhJtX+0*lRH;|q=;Vu`|?e-Z|R@B*vfskoDT zF$Xge{6OW+6&*F2~t*E|8iJb}$-7rvybSOUhx@0nrri-IDQt#Ho2_~-X!M+PV} zW(8u1L_?p}P+}3Ywsv`Z?Pg7{Tw@niUA=`eg^a_fIJt}9I5pQ~I4$pdlZM`)xr`jZ ze7&gS*H_U)n6gU&NiEtyq!l+&*GJ(-|D+Si`2todGRvL)&_?V>1#5tH1FX*M7_57Y ze)IxXl?Y$nq6FUp!r3~Vgq9y)-Y|@|bQA$vRoHuJz^tEObBZWZHNdA%>}X2GLHQ@C z1dlJ<0>PRJ!`F99Eo@e7`a%2a>L>5!--~u{d};w|4%?45wA+t#qD49 z$b*WI!kNwOc)4?KUjf5salJ}xUB&{^dD;GPG_14_+GH|;-rTSI@GE&%5Ka3HSR2T$ z3X?5jNzK~9r-^eSxgnVDMB9{X8mM%6_tpBkCz)+GoEa*HmpfKJ`hJ*wQ966;u&&r) z@;1~ZM>X@&Sk_5`W7*9#rBm4g1-OQq4-pu*I;v1fcfKOOA}*^SQm>vmj%36k5^_(p zO;bIuhk77h6~=BM3Og7nmEQy{8PejQMd%FO?Q10K6{dm#`mO|Buy6))^z=vD17GW` zzh_WY%&}B~!4e>q?%iNxBLuAkAEDeLAgK`#X6OJFo(j@19wOw`VYXs{yQ3(sn*eU; zKpCoX6Q{auLdcUceAu5)948%mgjiU{pKE|{PYJBsR#tF>%_4F!uySWbSr23MOeR_( zn?o=aIm)#liwI393U{~ER&aCF?A3nab@EWb>V1`}zu`^3m>KVGETUHbs=sQo8oRKy z!W$)Q?Uo|2t8cw%-ZWv^l(dMhgW^AbX}cb>qFL(>+@ss6Qz00uZ(VcjI3$piWnD=z zo!Cmn5t)>FhZfHYy_x_gZh4&V&4AdQpQ!i6j)$HMJdjZB8uZXuFMKx`#fSU5@k9kW zC3dnF!MPJR^X`-~C97iJQXGd3B0Zg#{b--kZc&K`7+lm3`W~f34)A8jK;}Owg(!~3 zEsy3Pttiy@8Nj7!1G%>&gsZ?#CF~rk+ZN)mrsq6<4Is7ikSken*dIU#h%;KzDh8A zSs?Z5KIp1RVYFK``%kg!jR^K&@W6$8A5B0l#`U7Efi9@UV8gLO=#gscf4%vigz~f2DEnZxduT# zg|7uQTZKs{gj)(4NIJPksF&UaTKW@w zxj8a|IdVMt2uJ#U7TGHx<4F1uf@O9}@hML3llfJC9j`nF@C+>c_V{W)RVNb!8<1lwX&N<I;UwU|DNEI zvKE z?vIrMkEEh1^zFoRT&2RgPKyrsE;&w1*l&L~mhu~@kB0&2 zVcQjnNB_C`EpIz4zT)q28&eCr#YBm?u-i`0pMP9GU*%~Qj6*O)NrdS?ix%XViNrrtPJ z%Ih&1h#!Hm>kx|W6C5XC#)=3OiJ3=mvjtVgFib*of%^7mYcdMbZWT=XlW@a{2r8*T zo`BXtH@+Lyw+0rl4kHv2Pg3MDofZm4AKt=@ihW=!*+xeyL zLvNrQn!6iu?j*XcW~=Zg|Dwm6WjTLSnH~(gw~F$OEAe6wt#Ja7_9y9~HAB~os?#^z z@Act}1MGbOzjX4oXNgLd$;|4- z4Zugy7br2<$~r|%e&1utdA@h^UMID&+dAOtM~DS;0o+-tsf;1*6U71zKpBCA3|s+( z!Y2b*$8&%padQb(j_Con^hb*-&g`pn;;Q~^Qv`^R_WXrO#QE{m&ElE%0-3qt32vPF zt?PvuV!_nAPoTT*4Tdh!oE~N?U7f?#5O0#$SB)rux{VNJOc2WFZXw{Ew}#lh70rB? za!7`3c@Lo^aU#Yc?Flp$C7pn(tVxS2>$_t+P=RZb+tsY{1qDqRln>++%9|MYXXvr? zY!+oy!65QT;@Q+4wGeDD{fvIUlqun76XDYLAjqZJR*K;TLr>T-sUrSa_Prf5zEcAC zm9RcZytm4Ex8C(O+Q2BmMA2*DVCv@YCGFBpvcb$Y22wW-fHwVc>nsyna+i@`tf7CQ zb3Dpd)9?7x1E2Is*J@|}Y>Ol zG2KyhL^yekg!V(?y2++kxx2!7X416aAHl>36Ko!0KA3Bn-g7qZ_a+V4@tio=QVHJ! z4DYArAHzF!sM-d6nKMQ~v_OcZn64#JJl3h`uJmu(#`|z;iR*olg;#xV_k8M5&|NUn z7#SSs7-gBcb6>B&7B@`d@Fs;cqhT4#`Q36Cs*0iz zll^)~^PB-U$u?b>F@pw^D2z*0awP^dPifNoMs!faBk>jE>zS>CxNf8>wnU8iPy@L5 zD3uO2u6fYVO5@{7#=Y#(TIl13&dcuZ#*0GS-xmVKm2>S&SOQlmu^bPfoXGCeY*@V| zs$J}Je`>%%Z|Y8|I<_mPIz8gB8(;=r0=SU_nk3}rLC33&|C&LUd$aEk*z5=_GqXbc zAsAKuBctH=F1nEbrNSv04|jc8pRNpdY1YDYNG@;XiE}<|{>k=Qy@#ogbRHX8_?y&| zjWnekzn4zWv9mtZydCL8>a}kPCvzD|yy0;rr0T8fjRyq>KTC@19p+!5pt@y;2ZdI9 z09<{tSdRl_i0Y52Ahu<>CGup8Z)ghT7d@1vbsq6SI&9V|rsj-fQb7$Kpk7v@O=Uui zf4~OTCM!KSgo{B&m5+SYT5k5kd7wor026CB_ZqeE$>PoiO?|W8KB3$KxxrK8R+u4m zvmicp?lw-#60(_JnIpZo_<<^WgMJ9QAq(gd(P9_g9Ha#mb3VYP?H^BINzIb-HICoD zKQz9Uel2p&G46N)bLsG~2}xmL1e&5d!zAP4WQ;ArPmva81OYY6$}wGxs5YtDT|%-j zL-()GR$92-_+3%I0OT9|a*(MY`vS89oyGDJZC$i16i5b;nzfM5GT3R45CAbbKa`gE zWVoAo*Os}Z^duovWl;_qwScy9%3`$lk9-YtcJhN!bq@Dw1L}A&oL}N;%9lQ!1JCrW z3|#5cDU)sb>H`)}f8bB=A2|o_<1sq@Ry~2UkH0pWV2+ z*jCdc^fA|S+|g9pqjHPvV}P49H?`c#9f3QF*S&V8n+(UvH|1cE1Vl)3D)&mA`6YAz zG+}Z{MSltJpQ3Wua6I9466}*8=!6p?#Nmj^$t3CiLcFip9U?%9nb(g1r@0F{Ij_Md zhI0a`yT7FJfoAnBTL?{DCx-;68XohWO}&!yM;oqW50X(Y#KEc#nYIov>{sMhTqKSB z408~Ut@_KS1y(EB4P*Us>dB{vSMT?z5>g*tu1ocE#G^*~!SVsS zMcMXw)G{T#NBDgCk**PT6cVT?5KO4xW392=@>*a=aO*y1pe&=Z-;UIXKZe9)n-nnw z4N?0fsDh<&KwPFb+a7pZ$6)Ro>rIZY5#U~Y@s?qHH7Rbd@j#l4H>KUR3xdS< zv30;HkT~jX=haeMxrTPohr~`-;n!7$;k?KE#ljSWT_)cwVf+E6s!b21u1(K~rcnkQ z?JxB)pZtXqu(KBZi{UxoRlcIbBK~S@o!uj}UyouQs#kpdubpz)l1q(t$Vux7m!I@<&^i#(*<2r+*eY$AGAkZ6H|s!n+^ z;Q6^OxP8zO_(ql$r;|V{MWD!dc=Lof_Wf(XYDPC*44}0#>;bgaAVq=L^;QJ?gFiba zIP(3B8>hNWLNX-~U{Y5-bB&S#~&wq%Z2{tH* zd~IVyWNlz3oPah969x-{2_00^fUgJvS7~-s?*<$4UJ^_HrR%PD5C@_N^~9eJdPV5{ z8V|7hTLS=iEg5c&sQvXosbWWyQRI33pk>oM-S{>&O`e#*MDntkG+0vwlYF9}``HS> zS*zf0s@3a|pKj>Ly16^ogXegBEzzwuBM($T%Zdc?o~=@%r4*~$XWPWrpH-yQ2O9Ts zL%S8)Jz{!h7M$Q2II#e0Gv#OK!q6>#!U-tQ2>yRRse5?hZaF8wvA^0p{Zp` zv@rO$Kk+M~_zRWJDxP@|J79+xi537XPp`*{@iYWF-YQ@b040E46Bmv$AllkjY30hE zjy`iFb92v_j8T%8A9RVkdnrV}I)S^T#pHR|Tw z7X5aDke|Y=oayJ!iNY_>o}3`|xqOD|g8HR_L7=}P_o;ZjuUBT)b;akiod~MWlc-Z9 zx2Sf{U4r|x-^%vrzQtR9&?2rZw7mg2gV1#)JUN%AN^L5`L_Tm7STtkpfRQgDUyXS- z-7tESr`n66PmmU!VQVOs*<2>eX=y@`f}wIlaa=k|!s(XwtNNAhLI%xZCd~YM12BM$ zw$PD6!2Xj2DF>G?FZ-cV{)}JN4EP}8ZLl|?ALLOIC5|=wOH*&FQHC?F4y0-!G-M$# zPeQ2HF%hPk@QAk3D6Hx+clcVN}$V!|WC*LTh@T*@DP7qDmEV z5qBksf}3GPqm`ah`9+B+WAhar&|!!oP(gGQp&arM&B0&*)W8Fn$FcXT z{m0oX28Mi`8Le{;Cvf;)XhVOiBLG5thfy7X2B4-650iPyO>LwSTtrA?Ak&_Ut7o_p z@FYzCzupD>HZrIH$$iJGelM2=Wkt zD&#;g;+oBzwQY6V8%{R?U5O-}5kPYo!K6SyVAqCj)<3S`*!mo;iBl9+d)r@!tObc) z%#oX~rs7SSC)n~SzD z1~J3hAszm+{W~=PezF>_eka*L-TYUuBZns}z|+LLCpOI!yRdyyGD=!shdUww9H5N= z_jqJ^SG#N0iEak-+f;yxnEtBBZk{q*?~jT~2px6}T-`I90PS-C2Ni&jaC`p2SJyv* z{QyPl&r3r9Gh-o%;2DOEL7DSW_O{;R%c;`dgZQl3%d!2%b~A2Qb=N*1eb&B*TaM2# zh?el|uywv@pkY(CPg!@b?0I*RC8m(%DCl?b9r1FlYK$7UTa>_O(%YlH(osHf+b(Fh zH=WMb3fbZ`KNU~wjGCK|NKTUs!)oHL=F4t27c9Utx6=3r5>8Q|bZqPEtC8VXl)e=YWiRhHK+#C?- z0Bym!`4h=WJDnN&OZq3`O%7wHH<*j4%nad8L)cUOPk;X~;x^DRSS-sr_l98Un>G_; zmY)|gVO8*w=NkfbK3|(U&=bC|F!-3k>#4rp-X|J394N%Y(({cWQb)e$4LX3ra8m#! z;7SH_vQkY$O;lT5)=T46-P-InCkDbbHP1Wj1V&2ZAw{m2$SyQobMtUgfLZ7%=0*Go zq4{$lL}L$T(5yazuDTb(fluR2s;(81!4Q?3@BLAdh{!b5Oug0R_0MPDABSEJWd#Nh zThY*GEk_H{Z0uK@Mqb)4#af%OaHkR;S236x$}7(FzwqFAl!v&h;v|?Bh!*JZ>(NfW zNr}WugptIfZkD65!GHroWu$;m8I`;5F91cZE{a|Hg%T0kffRXnI!b_qwH4q!=n;f@ znn=8EGZkCu>$D3Yeo;aQQ)Y8GgOBCu?Nlm!76Nbwsi3<+n{UjPs03AEdkA5BY!0XJ zW&qH7CWsmi+CmJ!iwr49GBE!CmpW+(p8?n|ey%HrmG`@jx7G?DdG11YALDlXE-_fW zt+2I7D3^QPC`|iiDb|>rOj--icbcMJISytJ8ItM&z|A9rVOghmI8eCBCIcNZ3keEF zFPaRSEPO5C)YL?B%*2(pQK2C)6}M%es*4(6++;I>93hlH@by4;uX4Lo(R#l5_JF38 zaS-y-PycndAVooO>=b3ls;`Ca?3Xr_;V8aJ5S33KRA#6-j-igRVkqN*KcgrL`K5ge zJIo!G3epe(;t+{pgjLW8cjUt#D85XvZouz(f@=4Tl2y+wHb+OR#O=)o9uh%VRM(dv z`EnGO{Uww>w@*b*E{i!zWwWOQc4IdS_I>VblshLC)+8RY?3^^;Mb!*;4{;t93zC%u zA^1=@q_rXxo-QXu{%~+zFIhNjj6`-X5?8828OEE^;oXgVO@+nxNsydU;J^lpebw#4 zg?21f-_QMo-0m?&m$L**0-WJ%vJE=?q=j6G%=0-?dlp(lusl<6B4ru#Mx6qvzS=1F z0I2>d7Ls4aL(N*vAl}kGHE*YWlkgbznAZY$^&7jE>zs8>kH|C;dWfJtvuk|tI5ek` zCRj1L?vunKMAJNub=^;9ATs03sve~nj_78kK&~2vtw{=F?mw%~@{>2f;f%)vIGo2n z;^^P9*U^upY-T;K0Q@-Fw0J@VyuAK?%$rRgBQjj95UcDjs(yI({6*QJNHU$!yh4{211ugqeQtHxJny&gs>QqkopAjz1?zjicO>vj} zy@V|Z&e{02ER&Tu?O#OiQH@zHSt&Dy4jrr4tWUX%D&c~-fCo3J2tZnVaEW--SM+IcD-<))GlpQQQZo+u~GuKv=1*Z_;fBB zjT1XSzB!Fw2Qy{g!|vn#?ibaqrHW3KTX`H0{VmIvgqF&~_|v&mC6bw@?-?F?2z+Ph z=;$#7z;_A@`vl9PlDBsZ#U{(N(w@oGIk)#pnhaAYN=&6`39WUllC9hbEvp)wMJY?7 zb2oN1%Odn|8&)^MG#yb&iG@WeL1_$cqi4TuH1?lro?d7e1m$O~m*U3W zx|pdMy`ggk6naf2Tja_r73G0z0+9aB9{)s>539a(2h|7gmUR3A5T6hL()&N-ab^s$ ztgrF}aFBn(<8GLVeRws47EM8Tlg(=Z&gZ!ibPxV?{=-DUL&=1=r?q7FD?eDZN6{Y& z?UDe&Jr(O(pC*kOz36B)oO#K_Q#G0yMCP6M9LwtYCC_pQ$=Lsc}Xg`Gpgu-J8O zFNTLjPW~l}6azLF;1aeFw(%<_&(o-}!NIxgU{kv~rD4IFZ0|4Tk3b|*zc?xmzGBjE z96o|jMPHket`P1>UTcu^pB;td)(dV+1Ni?<;FCsh*$G0j{XW@ zlVkRMe`I#m@}hZc{n>W6T{cw=kC8-lWMK6!HW4|*K_7bJEL7YZ!$9C^l==$u_J z!qJEwlj@6_AzB;Vmw_fgrBPw`Sg#ybdwae|H9(`U!}Cq0X4(hDLzb|)eT^Lfv&AldMBjgW82(AqDmy?fH6P6SHD}xmTKAfW=)=`t zUr;;z_9y|D67Ahji)0y9yvw%wI) z$;%v7z=ke*eBZG6mWPzi2@`N;0OKrw1as-ZcG>B0%!~98~AbyP|1`Nv0J$D zF-4(_8pwF|TKSByhLJDAB5A1YR)C#j;bU1%S~sY>^LOiv;q8}0X)dPQ_pOGL2Y53c(2X;6iNeIgYMdUd2bVZq0c0wz6u(sq({-APd_%UJBLp__E1? zo=PSYK%%acr$={>!kEZ!Ep ztIk;>=?d+A{54zaiJtywSJGx8EsAsM8DB86bwht46=(; zvJkYN*@r}WW}l2U;!L-$fs%&?+KD&eojER!y|iuG1=ACV{ok`({~vSj6r5SuMu~QJ z)Uk~(wr$($*tX41$F}jswr#Ux+s+p|(|^rO&D1$(s-{lO#jMMB?|QEGe(S+{*7|KT zV}CnXQZ|pkZkkgNt+SAIdJHBP3m}mhg2bFr3$k)sZ_3qxL@;Z9q}lI%js&*X11>)v zr(+3^leTQH)6FTDki}?u6B&bzd$)5uz<@2lip4ZB=pc~8O-`A>w}`rjHIC2=Fnc(N** zrS2Y@z5f*-|EZ5~Cab`aqeY7eY?XoRWeS-K#-qmnJF6YgxI=>f+S=9LKdwAyD?L$G%o0RZ-kuEEu5c4^73dnt!oEXDqO zCSF;OirihEPeNHb>v3#kCt0loXO($!Unv-5yM3$n(evS-y*{w=|HXu5|KDQ5GPC|a zh_G66u^X+(UH_3|{xOTuBlS<51px!s1R31TY!^HdkJQzg;qqI@%a@>@5{jm(%)TXH ziGOAP>eMe^LqE2XI%XmL=8ht`#o`yHG0axZNNmZr`~pO3O$s8mp8t;``E!d(0h$dd za-98XfWoXByM=XfkUe}Q&Bi404dQn8CoH_teMa!M@AJp<{1acXz}IWfM_E_1;yT%_ z;``Hix0X@dp3uB|iF=i0hVXIIEkAoP^z%}0cmEXa4}CeAcv+)Jmsl)orFq&Gc)x!f(7!%N zXs1P)_sHYC{}tF52Mp_>ncA+&*}*v5WQ)aq-t_8(?4;EriC398Ca4MH0c}Eb^3F(W zg;IEMNA~&)=pW$T+UE$)BwDoWRA&#G)&<40Rz-*_C|Tk(<{QlAZ=AJm%hQ)n8`X(A zBR~ROlsk(I411G3)PI;gp3=`jBA85Q{3Y24@WbcI0gw7d3L*TD!VqkH>=P1Po3>OZ zTn7^~7X|Y#1Sw@>fNq$n4Kle9!%>w3+x}ATkO@&GxA}6<{wbIm&Gl1wYWR2EMAsx@ ze?>{C08g6WB<*B3UB;~w4>d(f?DzfTQB;1HfuQ6GYmWFxr91I}WNXzee0U6{2Dc(~ zHy~NFY4}!ufGqYow=*I;Ez=t<(;u=z7#WxE?iwghnkW!5#6o%Sle3k^rvx?{36Gk} zyGHd$YS|6v+o1Kmcjib`&C3VuA;)~$_Qj;z{hrU;&uhT+c%`+S6xH>rS|@2K6UGOv$vi`jkU&=tmL-lHpkGNP}(9t!?!}1Hn-Ln z$IzqTs*bhM=v}r+b>c99t8rX!s|;NxfN_Lz^0MreQUMBq6HMF)xtJC3t9dRXJG@rQTCaR8ZycRT{C^H> zY8p?_1=)u=u`Nj^Eg_LtoSO81|N3yy>0Ly{yV1BYJh3!VGL;70ODf0+4Ej!QM8K!y z&ljOb2GRkcT!bt<#$v;H2v!NZ|K_uQpKy(a?aOHjUbbPR?pnx95RYU1^Cv$h?(=x2 znp)z@Nb2bXlcA3Cj6B+HQe^CtrCs9^U8kHfq$(jakIlh=mH_T17nJ~RF_5~_H^04t zJILPWL|JD@SXg)jdzSvm7aGl9hiiCMiWx_mpXf>)Md8zTWctU8(wAXo^3J%!CWHD7SmrL3@&YGPu z>|rB(R?BrO;t)Bt>m-C>TUA3MBNM;^YEq+XO>vnIYHs#NRXlB7`i+*>D5K<+T={8T zkkw4pP~?NM(ZfP_kG|%+B$tu+B6jSiZ3(R3Ma;-Jz%q=VFvc#t;M!rz)`m=eLkRaj zEt{Ws;Sn&7E=BVMj{V;qg=MlFLTQ*?UE)`A;TwO-4aEj zQ#b>cqiP&|v8;y`)NxvWb_YF#b#XQPm3OBv28Yat_;#**&@=-~tmCKtzWq%iYO8m+ zkel5l%Z1%QOFV`GdI0Zw@*N5@oaX>zycG?KFLhSAB_bx83VkGJFD`BpV|hcSvLI44 zT%Kv5e=CREOWc zA7_!FLlq!BgGHKLvHoc2#lGyRPA}!kLTG=_L>Cxirup@C%qWG)E3=+e$d; zSqC20eT=5NHY~%1j7=pU4{6^TT~$S)j%}r(AbkYRO@(9gVcjG#nZGR8KjbcrDYN^h z*#jjMdl_0v!B+bKQ`trk-ZoIqN~+GHI9?uVBONDvv@~l8w63Dq-B!#1dHS9q16ER^ zP~`e=S{5r@JYScDwh9yYGE6!S(ZYU3HibAoU|mXk%)D1^G54K7f(j+kdIFjCYm)Z- zJWmiv|JRUTc4y!lzYW$&yLG5XHPHbMpH-$O=zafRD`#Y#M=#gB(ruDVif?e|N1SVP zGP2Jml-Ou6+J<0PO_5wXAv+&4<|3DvW%Bl_o`BrLE4od_S1qt+nF3k$XoHr-Y+b38 zsNI!5Hb3rz3y?dx&k;)RiO0gJmvm!J)fdw>QY8V}Vz8I4Vvi7PlLEH`ZyKqiPc#Gv zakl<)2nvL%dO?$1C}?oh_~R@HD`W+atr!`4cp9o>3YB;`Heb9Y2vk%kPBT!8j;8gi zBpGf)IbOxr#;S`pNq8o4}|D(JU1 zMR=tG*UR^in&SRC)$HlXSHB_h;}b9dPXF=x%T$~z-;z)6+Yvqy7t3sidQh_U)Q{ft zxSNvuNw}xH>U{k5^d)}C2~sl?d{CH>&|%wWu|C*8N@AZTuhw^8N_W2T}(wyjqOcL8UE`lge**4 zOq~C-8pMSUln$E2+ZTU>HxxB7K5`TxA&wF4?=TA+3)<4K2+`;|gK+q0lncqIE`wOQ zGwUcKaAas|VhmMP;xG?lB8xF(}(lR(YH6aMz}O2ey8UoT$)&>zjh=8 zHcpwGKCcJ8wv*Dg9~w?wcY{jvAw77n@pM%Bof&;l|N_9ax46k zT22IG(+oX>z61BfBfp#`Nk(v-JmU9hoT5lZ7@dORjemP|=`~YI#3M8fEt7jrdJXlY zt-(N<)%1r$tGy3j9KOaCka_xtK&!EvJ?3l5{&>Tl7n}|9k*{Xq8ol@xLnuK*p5{Gx zk!V!-&$@%xYkTa*eU6RUeDYJ#)nnz1G;(oIVL!v33&Yk$yHd zY;snXagKSaXjwgtt(1%fzljcUzgV(HJtK_qyX&+1B8&45U5k`BsUFYUtq`s`-4qq#(Y)`O&BN4g?4J`(q7% z?)l??rJf0cz7UfNu)q9GrpFBTqTl^PK46-oFT6)|SJ}4|XFYl4KDS;E=LhjK`9u43rVf$N&)!}a^wYh97Jna^?FCvT_2$7rAEDmg>S&oM5-3%bXh;j!a9f1#N_%RbfH66-hS zhhIwTl{n|0JkIR`I})i~WnEEQGtZe5XrJtd&lI=oUww6`A1S`gn*7`4M;-5HOXOQ_ zUAMMR=!0w+`wxvRn%ZwHZw&s&ogiTdIivKITQy7dh z!dlS$ub%(^J%2wo9A`W|2pa867zlZXIGtSlNd|x-0VNm=r(2G)8vP@Iq#>penjzgm zvm~ZZ_>7P;LuK1$edsQ=RyyO(N-_<)d~|w*9q-ZYRqfgCXYXkbD#QRW1Q`50eT?4o zo=HF@dy#wk!*||kd_vyjSDM|&XF0guU$Ke)lh1X8ZFco4PKvjS2e@N1PwN-<^*t}2y&rW@ z=70?shx5M4z*xjvs$wcLDtzUTLO2;I*)i!he~ZUF2Ey>DE4Q`g>kP zUO}Mi>i($&Re=J?@Auk&Dr4TtGHt$=bLv38BfHI#XI`A0?q@cg&M-sE)$lsmV!k)i zuX$B@syWfnY@R(W&(CvdbH55W6M)Vf+(Nn6V0jG`d1E~y@!7%MCciM@VsAC{S-fM-iHnk2ZvqQ7t=r{YfSiI(Z2m~ z{zJtfW9&nnv&D8;HdcRZRL%r!R7$oCRB*Q~R6v8pb9!}UD^|AUFEs2Adx&cbx2Ues zHfL63nbPF!$sh__t7a3G3erp?(#(2~XgNA^c2}MHvf-MQg?5g$gNe$H73{zKV1sTmYi_(=#LU#=rDk7nE9@%U*G{hM{H#^nE%ZHDT{sc`EBNVGgu{*214oTm*3a<2kh(D z2170YJQachGWP7P{<_BZ_#u1UU28kQ7wZ0UmLs>u|LW?$oJ?meb!tS@TPa?EqBRQqz=@R^KQF$x;R97Ccd%SVF)alXL0Cer-9se@>w7ZF5z9s<-x``6Lm9*uH({*Y!4hh1*_p z-d+Fp=jyIQ;gY}2yX6Yt*0FLa+;X=m?qWS#rP=b--sb9jrPWiRqMljp+MZ9}7R05? z+>v$WR5{T{@7+OraVw8mLjTaN0YwUR0k46?C_u<7;v9};uUO@I9?Hc}cB$m6#%+hIHrGsW$Nq~*eT ztx-`RC1EPDAhfx=j39-N!gFzYqZ?|^b8*60IFO6r%X5E6|N9zirtwcw(Y>1T*1zRH!av`fUSD6=%Yde$ z{L~CWk~d62zA{Qk+n+Q&qF?wEf&{ML>z*szq)37mV>Ig-&Y^@&Z5w+d!M1eFE0 zoP|3s<^@5F=uk0RGn~ry`DZ!u$HBEqNir`Q#tMGwC!5&S&k7Je}w<9yUWg z->J(6N1{V>H%a3I;$E_pel8mf3_`qbEVZ3&$C)O3kH2|YpR3Oa@Jkuk>Yb`NRGJt` ziLJ8IW@|Z<;FK^+7?eS8RKr@Ce!cr5+4wi6@PcQv7s-73V(s{liXqndTzU_=Q6+;x zZ*(QTuo(=7B=3n|)+@q)gJl1}f2_^Cg?UolMfZj!VgC}j5aKLDF@|QTsISJ}g<5o2 zvrfAO98H~_8XO{>)aGiH3CN) z<_9wwJEb;&W;vto=UB5(n~v9ZB$NLCNc_`ZKLe>9!zcB>2)duHyn)?Mx@7#eI4k$o zBu8vh-aU3?2xRY%9|_|&OD$UTK6DKkrflw|gS+DI#W1kZtNC(~R6$bA`f9a@p>*z@ zA!5m=i#zC#BvByc)6>mTlmWO6NsqCStz(sNNA6}LQm>{A9e4$#lrDH*KHrg7zw@`z zC#Cb^suGmXg;xrJD|bqx@<}j1sI|25i;t8n36Iw3=0Bf&Tcg;Y-qE8iwi}O$&HC7B zUp+%^u4@lv{r1o`J>h^AY)9VR)5jag*RGHT{oUA0V2l4&-xK0ZgY<^!Us1A#@T9{+wpHJEIs%Z_fJie-myQUpBBEL9@lHx*7~=m zt7WgoE2fGOP2)!!@z(m|;BvUe@{oZjj}1W~lNv-CN8f*m|N7IGTJTfU>Mzg|>ElcZrH?tUs zWq-JH=9xLFW3DJKGd9?p1aPgUM|Wvsi{|!NMcc*Kd0|7>J6%y!Q#IE_)OZWBJ6Kb+ zv<4dud?y1s%h>N!GW+0%TzY#DzmWTRT28o~W2!wn4wisHMZpM=Msb52-}ndO#t3#- zUq`h;B{^A8scEKl`q$end&37r3Ql=KiacL>9#qp>3n$XX>>cOz^1i}6lSq(4o3*p) z`vusLX;qxiABCLL3rO1G}~6|~IVvc2G?gX(Xa zx=SG)m3eghu7LJ^u!G{@++gKrm>Hz;AfwKR4V8Y`J*QT|VhrRRqf@KXy?vuo)6@RL zof}nN=%vDbE6S1G+E>QPzC=+vIR|VTqsBrpZ1<%_9Cq7oULhKTpiy`0T@>!%=7{a@ z@j>#?T%`~?+%#Jp_3s3vfq|uC>i>k4`lH2T&gM)9!>)8gX3p_Y=C}+5DchRgmD{j8 z!WTr7z^Z%gP_!&(HHSd;f~L#^gvIJFy7Wo+rtrfF3yXgo9h=dI~r|QyCx&Lgsi7CeGg+`Fx=I+1j=_@X~k(=%Nu~1s2gsL4}MOwcc z#~og;D;XV@ja)`w&967jzXiF90i(^6AK+|zQfKxt=BW+3VlWe|C^=8HALR}OCCim@ zsyn-KvBJitc^s!sm`m`&a1(?^dO2d8$~XbRBQB4`#sxAOE7Di&3{FjDxU*b5X@*l> zx>4$&P*9mL@|lTT7;}D{uWc&g9lx3F4*VtiqVJqMc*|!;1u@l;brToaN|G*;be$TP z(A2EJEEQ^MC~HkGg*aM1|2hWzQNMUM+`eb&0q&V7+hIMVs5!jIW78WF5Ox!R$9a0N) zC3&H5&oGb_W5Y}tL&2V#$6!CaEXF&x-Xl89MgxMJgP+M|fB1*}mXY(g9(|rdHfWDp zFvhZszbYhP3uh0~z@(wEHfq&*q7Yyk;>g(KVtB4YM2o%&6=uhFCpaPb*`-j)9F@eJ zmQ+sy3_Nf~=^Tv_%m2Q3n0GA9fsvD9Q&A z^R}W(atEoNmESA(S%wiaxxDX7B}-SqWUumUb0vEq9zc*j;1YY^^dE00e?^AQNUw>r zO7Ew8r$(83mqEahtj&42#3CR`xSJ19nlHN$AkCFc{0O%0Nw0-zcuF=668OOt4=?Q`d_jCZ@oYL=N-4j}XDlY{xVJQnXQUYv`gF~2C+^TP zd)Ml=V!OieerH zMEc?J=Ko$OW*?w53Mp2S_ajKc`$_^_w=D`PY#xGCwZLZIU!IJM>XDpC_0 z#I~PSr|B7{$5dI{1ju*@^uhD5t=+eVr|4x^CK~}ZN7zJP>>KVV8HKrSJ3C9 z5C7c-tBjATWly{85kt9*ER-Q70R(b$-h_S9bbcrLXKV5Towkb^i#8IR!if;-NWJyF z6390fR)lgsje^}Y_}D-iBkk@$3z+eOM$E&~Ce$ASQFx3Q^J=^EdCTMkA6nDtu4wmz zdHksY(Jf`F?1B)3J)FX;rz7kNrx6wx(>0Q6EaoHZAUHG8YwvIqMp3yvqzaBG) zfW*+S*fG@ve>xgKf!!1^wcbcUe=ef7Z!9N^B5Nu+@u9~*r`%wZlb}_Rj({8DmpyTu z7&}S)pqk_Igd|HvStn(ibs+?Po6mixZzr7k0Tvu#9#M*z;Su*E_$;;NBtVLew%6?E11m?JHC&(}SqJsS`-a*Yiu|m{2gitqBV5!M zt&0J8huXYQwG_&%VO5P(@QpZs;jhFSQ=)uFzG*bf0VoSCXP#-avs(AH7njZ98p3)X zFV@HB7c`nxnpiX=@aPzLl~dk;eK=&ECM;kChD3XT1;;!h>90%tHX_Tf4m5_ zBtns$%8vLm&0~AB6>t&sox=fHgWm_i?KxxmtgR~nLhIx;ex(b3U+5xd5Q08VEaQ#7 zJQOi?xEc~sC(H__C}Mx50tu5Q9wsxnCrLt{fp|bVt0H{cVqtXgR|+YDw|87{i9K{B~>1@U+jJp$=)IA2xD+D%FPE>uBy;tY`TvnZ~1>Pi1Hg zthY!M!W<0Qhmk}LT?d+@_bh(bQ$w&6$AEW?PBxBwW4{O;9`4IyjjUMxB-`Lvb^p1F zy91)WRhb#Irr{ZJxU+dbo1Q$b3VNMWiTgONXl>$XKTA^Sh5;1bwAMYM4J`R0+wroT zu)|gPSCPiqwS3b=oCFzH3NdZN760P*L)@9@*U7x~lmlALYT2~b zs8J=mE|pDc4ozn}sn3KtaDEDBNY}cESv!1+8H>u4l~<9ss}leOoc$3FBCl~C-?;}= zwZG+=v6t(SBKiEv;zJ#xz^W}jGFS0ewCg>-=YTc{bHm{49nRJW@4g)hgk~Ll^=}rj zJb&8tBp4KQU^fLh9PTi5w=Q9igT(V~$ShBPv_X)K@4f9naf-+juZNu$i>WYMlg z3KZGJE+4wWRGtj={|onkL>5QBn_yZ4Z4^g4eXPT*KTLe-vLt*%8n5DPySD$Z0i_IN z;ta*t-Vy<_)Ed7;Uua~p$Yk|_jbGObIByjen^f16?6CJMGK{-G~I$; zb_X&82@z7MhY#&!|IF_Ao!zwjW#B6GUctPWk?VAbWAvW*1gHtkA4Ri}+~~d)0q*sf z_`@uaNv=tY*szU_bnGjftxcZSq^INABEH+~dcS|WSWV}c*%}n~8^%fuE-<6d=3(PC z`}8SlV%)>({=!S$Mb#gGLS4p>j$Lh|k>+5j#Vyi`OAnBR;-8x|pov9f{&!N6WV-K@A z9FQAEL{EfO5D3ojKvTu{Wx1x)^X!E>4v}?UBmyhTK(EMSo4W8lB|5-r)MacIESlCm<``?stPK z5DXm1YfF6=xd=^FyXdf>ONP~URFP~u3tu{wSp3BUpG+eCoeCViAS9%oV}b>|%y5hwWuQ?0)HN z2s6=;Eb)dlwidDKhf{@C@m?cqA3*MgN=Qs5$Qr$h{S*NU(B=_)&PnS(8Q|2QeQ{Af zHXhe9nOrKvBhyADnul^f=f2-Z6u>ira*GH*x0RJ&%@~?fZZZ7Q*cpEa@V?KQClt@r=pbhJA@U37_4@HiNs5vz zTAM=)IK_0te%zxXBu~7Bo5Ut-ZL2N60(U|w4wryx;37&4jF{R$=l4(WD-!KHdM9_f z+1tU@HZo(HHH11sCO;R;Qn-+#Udul83J0!O{}3+&cs7Sq%!)W@!ka;LX{n?Pxni|W zz^hOTV2D6BFuRqM_+JS^+-y;5WNL3{ctLBI)fU+FLNT1-!)10fcMPNhGT105UGs+k zJ|oyTKdet)SJfr<^Tc!-O-|)EOr*SgHzOu-FSPM?6JZoE11mGGfW(o3?4)u&KeFOQkY0%r3HG5ud_)-CC zh9S=Hr{yMKyMwV}arJC}rylvY=5!dgDBqah+~Z7g33jzPa@L%ji(e1+h8mXcak%|I zZnqBG!eRS-L_HXQy-g|{SD?y|Tq_uiPnrTlyMiR(P#Awec;61@aYuUUas z8EZ6B$2&1GfcP#6G_=9fd5z4w)`C+YlIq=DLHF|l*+u*gZPj$#=hYO~S*6%Loboq9 zQ(w~YYdJ0JHBUUS$CA()@Q!TDxDd0S9pkBDxqW+^q|H0+@f{DyzwOwYj@oy}p7B0W zwkFE#^BG;oQ6(sjDTCmy7o92BXtpfH#0A}*O6)#US?Xb+UmNOq_+x9X%BHB2kIJ4Q zr-R+QtEQ+|xy}1rfZ=7(*4?^E(Q0hht9QPJD6_kZ!|N4%<7iq-h>viB$6~rBdPm$S zx+e5x0n{$P&{{}7At853w!2h9SzJH9140=)F@JR18Vs+lji9FUO6hOiS(vi>-!vtmcR|pOkzb<6l~C5q4A9Fr}l1JD+Pl&r-v`A-ri*n@?bA>X3`z z!EjA9sJkr1iPOuvh~A9 z{BCyl*RF#@Ppo$$GFqRVIMr(!-aUQdN?DU3MoV2_4e_4;Gw*HawRt1-)%i65ukx9T zotgNm<8;1f;N$JB>2!OhM^3=8k%3mAUz~j6SWYQiL})^C{yVIzQeAN6Y76NcHYC z7jn^;n^PmAaVMcnVd$)~Y{vdr*7v@m;WtsbEqx5X#WVjc=<}Or6Iy5k*B=~u+iiCj ztH*#ynT{uX{7m0(E8TVaBFjqzZf*osx|{Df>k-_h(2~&6;qnms`hE(hWuvLDegEth zdo1e6zY%=v{svPJ^Zf&sxsNJJ57#|sP9-h6JBxf0<481@A$S$pqT9m?6=$y5R+5sN zCTB3PhUxFNfD54~1n`U>S}AH_n~md#Siy@*aNEg*!q9~9#|IRQ%HYJ{azbSQU^n)m ze(*Eae9=aPDi*^TaVgoA2+GF)c!%f2`S<{ml;$>&rIjs<%r>nLE&aJc!8K=bV0n6a zA|*(Mpvp(r2dz%zhuTZZWEN<{KKTo)ibNW5W6o7-ygpE-!;kA$qQF}WU}SDDoWKe& z0zFEjrb;I$j$(cN@|!WAqy_1sGX}vkr$kIj_^rC02AERGbl5?Y?xhyf@(@N)G^ME! zSR_zld%%bnJ0~?(rgYygV152=d=&c>X~mSBY6eq(k?H9vM<*1 zgn!InG+_60M1=QCq36UQe?w0F^@qhi_GkwQ-7$4C|MsV5GZ`oo;nxH`HBtdn%4DDu z$iCG;pp)>sRsR}d5PYzXR{BmPb9BW`po8^sI1-kZe34(kG&FziFhqVe#NnoF%4j!?8(i<>Pv2@(y&0p8PhzWBmEgHFXW!h9SZY_QNM`0BbX>!gN=@izzvWiH zRKF`W5~L$MWEqTKTibv8<)bnk$9fBrH5eV-NXj}QStM><9h#dQ9Unm{xCd91dfBQ7 z)_;PcD;cJT!~X&@;&;jbTQcNL7GZxZ=D2m+W{u9le}aLg69N4aZPX8Zzxs1vxuCrn z@{2*?;h;Gf|J-m0DhWDA1eG}Eup%U?CPkWA^5jSD#j`5YcV8J^6oE0nZ^ql0(g~|7 zK0#8beNf4zB*{;b+{>VgX)F_n>b62RqJMl28`3iNsD|!M8d7z25fQ?F<}j8IzbsGH ztv>6jc070Jqijgf8c5va`sxhqKhjbF(x`9rNGtZGK%BuTNRr|d>s zO3^->ib_d)f|B&;bHdMi06_nfZ}6gGV#lX2qX@%aD~${&_eM(_=(^yc5wE^5Tx!>F zRz+4$VB^?7&Zb5k0*q95cxlK1sr0a@J*25mvkHZv6t0Y1D8Ert1zOP$ljPT4_+(+V|4wEWF*87p_-! zns_#D>{GOged7&=W+Ntz{G@a6G7o6cl$5=docBP0=L!bppy0f6($ik>PWbIgUHaJS zPPuPgwk;vt5KmYDM8fR7D&8({2=uyGSH)OZbE(PQulx3{v7;t$*XfWs5$mD4uHO24$9ej5^fM_I<2NbmoR82!HgP{4s7RPcU%P zo*douF@PK_yHnDk_t5Q=XK9HRa{Ei8zJNS+&k?TNe(n~~_vfJ&MLkqUuqD{GB5PZ9 zQ`B2-HW#jdi?`!P)++x-R>umC|yN6``%dYw^D*H7NrEzH!+sbD|qEhUKg z8z=Oh&;A@=&Ga#;<1i=~7#Q5u76y!MGl#HL^*5^@bS zg54C+pz6pBd#!h>Z{`M4yA%w7eq@nxkdNN43W|HIb4fv_6PHWaZbmuO3?o@d$fizn zZo%`Ux$352-Hl9K{q$sHk_m3vjpOC--n-)5_yK zmTm#e&loGUBUq^*FfDiB$|>td1h=k3EEwlVV^p5t4#VagCBYx0N^*X1a7z`Nm@20n z>@Y-%HFo@sqwuwaPQ05Hj5KWLFuLQ*xHp`Yp@?pJ#2 zkMoEo1FN}OAwCI0W&A|1O0?$YNyU8&abIT9H%V^RC?dEkL(321VYY|%RXcf3Y5TM& zOmXAuCkcnjg(EatEUHIST6V9NL&-BTc+F!ZlQ*QDLGTv^d(KL52#TkLvZTrgqdd~$ zmnOWXV$LmG4&J8_Qf9R}0u;I* z_g$mxPIrn<c$RbMO$Wyv?iw|b={fQn+xe5**?`|k3Ll!&UrO_V0h**{4Ux*}! zLOcDfy%q`}k8uLtLL7~aZ@9GcEwRI0oJec*wFN593-H8*${Z%Z-}Yxu|6<8|K<9CP z>4iza=tdyO3bFM`L4c*c5r3fPI2n4hviKZW{t_9(9PWdlyadca#ta`3Ro1tz*&>%gOc$#;*iMRJSK%%Vfeo# zs5Kth)cAVYZo?+AmiZFERMr&}rn&q6Ax2+z)V?CS(9a;;9nv)1nwBgVgOUS7eJES% zwXhE|Z*KF^>&x;e1I>1I8U3PiN zjct+u&BI=pi~%@-W6{bL?baV`KPE&F+DM?? zJuU%V!mz*Z720-27l{GJ!%Q%xn-aa-Mn->#`;;f$$BJ92cz-;j1p2raq!5xsk32t< zcf0@?-XI>WlCR^pe^AprbI85lpTSId%W!!Gms{}nrU(^>IL5awxwp4ox|D$Xzo zLRi1#fKrqMUm^1#BtK&?i#xb$ll>q)N-+CibmN!U2~LTVJdNV;Z<((c)brLB(Pwqk zJ@Rth95{bSiZ1dxIV`)dK5)va4ziNEg{q9b4kD^-q<#(E{Z zSz~W9csgf)w}?Y2Ky$OAw@aZRJvx%+YZlG`lXX!tF6wa6V-jEHf=nGRE`TNd(Gg~T zdbm7YX>&h8;_O_`(e|j&QCC*)hZityBz9p4CO*M;>5*) zRR)pC=QEg$zsc-pussVq+Oa?9F2DBYE9(GeP}CZzgrPisMo2v~Z^8Ec1>XwxGtp|z z%5l42nZ4g{7j%d&SU6~2n_c#oZC7ek&?!q}B{b;|LUJtnV=AA}%tP@eEKzVG`v^(r ziyopru9{G;1yjb<3UNA-(3KgSJOY9-i2&M%F$bAFi*Q_N(gP2?%Bh}6Qx!1OTTr8x zcyU#utwm3(A^G-bOh3fP$RhpF`8UH=3?=sNW9#0&zkgG+zhYWpqdN^6H`Ou&njWIB3 zFyNRr{>E-9QSM-#YJ)h4K*5k(Ey>8Cx;%PHRw4;bkIHWL z?~9YDgILKYI%a#yUGk-c4Ml!^g<;ciB+5=f&kgdO z6k0JIk?|6Kp8kmi_2P(@`txebJPGKMbec*E&FymE9Y;pt^@E4|E0uM?7(_aPh%VR- zC5e0zf9ZTYep|mJ!VJGQ)t$mQ3>P;3)gNz6u%=scfuKX8Csx&x5z*G2@65%#zHjnV z)%cL*lUIvKw0^c<^}t6Uykn2L@z7ZBF8IE@*G^9gdJs zaZwUX!VOjU83lj9>8NdghnSeaLUa8W09rt$zxDZgQuZL%$U*k{_4V)OE8 zNV>Q<;Z9yg!@Vb*sg_qTS~b^911jwu2*k;~bD#wZNzD zS9C~QDX_Jo>&i-q{F+>`M&J<7Zibg5v#@=57Ea5}6&%rL!b^^`5;BUErXp^tRq1IB!xIW(%v9#g?pBNYuNARUZ|=0V z4f~S6Ce85r{JxgFr=`#EWhs>&r)a<~QseQk9!D6b_66(&ejVC^eY7gH26vQJo+@(t zI=WokC|1#zPq%kpc9=CU#W%pYa===T%hdW32XIAvLMXF5LXNgXX^uMUHj21piL6n_ z%dt<{G1jzH{H@6jH7ZoA1s7C^QsHC#4f{fhwC;qn!F&Rlv-)dipkI$36>7uSEpqwo z<=M+x9{ZNM-62u0RQ~#!vSil$j9Pt0SAHpPA?3zqh)h*%5lLyJ@^ehFyc4f0zc|O} z(jU>A+KRHoy=GlY7qeR+T&>2dU*bNn?)MMtpRs*P0JJdHH?RA{h#jfsjnr)V@dM%Fn6$5fvv9YYV;sO?~;-HlhFJ-E^0 zG)?cyxsrZYSHHtn%V@1b%JRj9a&aED=gwn$GB-aGS1UuL)ee3Jj?Pt>Vtd01MXcT` zDA5jkqX3O!XyOCHgxI20#@#HrX2tk%qdD=HzlV$~x?dO}N= zxyx2#$lNcK32x|Wv2H$Fi+D0SG{LdL9AP|KFV1J;Y!gFdERto|RwG59jo;LWCAzES zAL$eFe3U2zK_M;?lJFv!p;8f355O_UnyTG))|v|#lmo>y*_%>`v;>LY0lT#n2SFkpVl<7Iv6;MYZ}tQJ1R9UW zYCovR{uF==xxLYvM1_3oYO)H&y;6<4tmjNlxy&#b@cOlRe83ReW`;6 z;)v7`h{K~RJ(%_z`y;*sj(ji3c)=rijEkN+(nv#tN*WAa@72wfvIZL1Mf41TT{x_o z%@thAHY+UTb74-rwnfGMih{{AJO!rH=TUu~tY7;mSXq4y>?WEA!S2-Gc@PZevWu`9 zO!*lQe?`0GmA$j&xdkNS8V-T5QvEhAdu` zTfge8V%&2^Ieb~V8tjPthhWG0zM8u)VWY7IlzbG#6=);m>>V)$^4hdyZPv1Ec@B_D zUr-9U8W>FkZ`R;ukP9K)3vuEW?x5Z3NX|mgpow@iX^lJbY+{&8s>;$^+MQBJQo{@) zkxbjYNmjK~oJca+SX=}R@OsG6Y$e&_MmTUs`_eFT-;-B`WGl0lt*f&13M;$!6iz;q zo=;lxOnf>G&o){C3S@iSy^D`S6z>ZW_#CW0uI-J?XXFTF(dWhXzau{nWti1pQx}7M z>uMxv)75CzOZ1S$adzk-+m!tj3&7%dSgS3DSfX~_No^#;>FENJpMyGQ$b$|2U132L zbGxk=N$7H5U0cZ+M$RUor}#T zt8^_|VH;8nMR6iRI-vpHeIxmM6l6pg$HpkrC=83&CycaUD9K0B~txl zJF(S0KWD7R$W|(Pj}%_+!39S4Lb~wj(|yIf@9qWADYbvTb|R`7nI-UZvMOSV0Nn^O z?4C-87!}w`afLaL9%`^Ti3mzXt00lZQ1%$U2CjTZyfc?ot1BXb1BMZ#dqPHM=e9ao zMiGVvJ=Iji7#zvNOi_etJt{~@5Ioka(CHPLZJ^2%LM6%3^3KX@C0DE^RFTYE@thg* zP#r~2Q&I%0qbj7?s8Q>7L-l3eD$SC*y8Bjxk8o=(dQ(^$s>gS3>x7qi{@o8jplBey9Upt1IVdB0W`- z-k`GVYNMIe8uXZGutAUcdhB(+Ar#B;%oE;~tIc_#MKLkL3}`z`^BLS6cXG+elS{B` z_(@R{GhVU>L;hprucg)yO+h1Cf!0ik(E`04K&D!-y>Nj@5O*eW<#jRr-h|P7qPSA3 zss9p=kkGg@9MMHaHFZu|YbH5kdBiaW8Mc*uN`)_0+$q)66zyMU^;39G2~!kdWN?M* z{4pPD?1yzD%*-Iyv9brL>V-w=W);#W8pU2V;-ueNbj17t*#XPJmx7eNzqH<|*WqE% zQvLjHoYvkl)_AjOzHsyy$r#d)qiC+bHhw<42LB4R`ALveq>ZXb7$yu19VczBD40dW zy7LUthGRv1<0V<5NS3@+7`;|L#rU4KfBv#7SC%c)@>iA?6f9j@5ute~%y3<}uX%w& z@bvS>Z$w|x!w~%$|AxSgMdFjcKIEMZW3LPU_!BsL(H4CD#C34ffOB?*26Gjd;tN@^ zpTX&8FbgkxgaZeP-es5}7PPPQ6px#S?qOCU-j;|rjt2P*{NP`{`0>btSzxCHhco<9 zF(ynC;z>-Hs=}EI@FaZKJ2@0490rb0#VNjgN8QgBIHIq=L7%Ib`@O1BdT7g@ef?wm z8Tit2Z~S!Z!OJAO1MBRkzZbD>jwu|A)vc3t{-hl5Um78n<64~6TRr9<7M(ZbslQaR!oozcWChhf>)*q$e_>~| ze^J>26&MeNof=^o&R@M9-_)7lJ2}bRBo*qWj)cPVV2e-x6fap|B#wEAdxh#P(81UO z4blx3XlbxOqGTLwC>56Ar@Q^|lMx41P^S!>PV*YwuK*t8!EWNFnE7Bj1>f*a7z*Ks zpe`;>!^y4rA%?gy%&?m%$2;=QiJNhYGxkt&*qiZq1Z*oM4Si*8 zEBtKC!AoFX26ca0`kF`F(E{X=2mQ5)A1uE=9{<{kG|J$V;`@j2>DJnu>Yj6FnP9_n z?nXO2a=#F(L?#}LC3$V3P}-M~79qi(8r!SJG`;+BGHiVYQ*|3WTmu#IRp-OHy_d&h zb-+$Q$P|uYfBgOn$@cY8XDy~4(?~xj%KJRhuZJoRxQf72SjY2f-2VLHsn7Qx{iQfR zPB>dN_Jp)^(1Z^{6E5d(P8<|#)n$-A-ch#k6c3;3+Z<jU@o*3 zy6S?{oy~sveD+n3qm7_pd}wF_zUn3%7Q9fl{FIELM%C5F(m0N+u0FV{wgg$pp(I~T zg)~v5D#ul-+Pw3I`pA=Z`YAj;VljJPFI5m7P5cTaU_;Y8oN8t1ikS zHAM!+hL)9-O6%z97xCPQPq6O-2!NStSPveWU{LEdY6KF&S`YH+jw~d*i=6PUSs{@gHY9(e#!ashE>x{!-`m?x^RlBJB+%z zJimhA4l2+u3rPlMUpBjmAkHhw*r2S^Rbihv8+59K)>zFKDdrTIo$p}Nv zpx+a$GH2&&gmd`ew_oF;lSSadgM&)9BUpG0-oaTvB~F8rkHyLQE)E(@q%0xw@~Z3$ zR*4kleZj&~As1g>*%TLlc0dK@JlLLFFKr4&aKp4a{8koyf`4tBJQzX_fr3xL=UQv? z^m~I$RC$c_w6u|y>V2O0w0s7B_)RnXXq--q{aOB4MYh6xSw0FE`RyM9pGuA{6(91` zJ38qpyjS-i-xL85ZAtjz7k)T8_O|u~)H+*f`%zjqw%vJr;-hv+Bf$UAfdhN+&uz5} zE7p%WCc>}>DXv5~R&7CpPQz~jQc0-W6hE1CARlV-V0oU;thE2wGN$koT;iWR3%Rtx)9(n+EFv2H7!$0ga4He<89|FP6n-`>re zbRP!o$Pey&KD7h;c|{Luz8DyLc=@N$J>|$Te0N(`@!>-_AnwT5`_Ej8Y01|$^ktGE zcd5J%EdAF@-T!c8jbm9UjnVx20N;bU5ATysyMKS``md&hZ(3K8%WyCje83-u<1ILo za5ES7>*bs>ZT6*?M9&T~tEU(=dPu*~?_5c<{#oJwdP?FE`$cR#C6HS!kNpL&6>4rxeH8DLnQwpF2Ttqt25xr>Fb3VCkqQR z5)u-)VfZ$?8V^a&S;QVDC_Qdy;6KqfliUoYGxL7DBnT&P_ag4Ak0 zb?Vwp6T>&IVMFnA{4wDjellF}?erV?7ie_j7}$A$Khd5!ebc7t zGdFC+Ka=o>^GLe^4GKx0<)uJ@W+zI7nB9vOe*5jhFTXD<&}XM<@*1zGyAkv5+i0)& z^=oV+{OoW6F%9QmQpFYsCS^$deC3MB=__OKy;4;2;)P(2O1O#Nod@d+=b>(H0<0x| zWEafeJZsjLtutqCDO*tSQ&_jTEvBJjL<6X$^^H}!`LV3R|Nnw^4J!CIpoST5qri(r`g$Fw;W!nU&zoF|M!b-h!6b=lW`ySQO*7Ln^NVI?xFh0*xFVt=nj#wFuDK$aKQkgj#SIY` zL zfGAD-XJ{GomiypM_~8|>fA1?u`5D>?_LUX2GBJAA+O_Bt)E6IAbHO&N1qV-oS!qzC zECcI*km|B+&fIO==FZuM<8xu8F`l8Aa6Ohy<5xa2r4<9SWPDj8v%Wk=w%XPYnC_cz zUV%Zt?<%4^4}7Lh$J+MFkJyAIZcve17sYN*TCjKv z9*hIu=x4lRf6u%)pByNTUHqMpRx+_%G1$3M7?7=^ErfX;*iSUfotvXMxX8$F-Z*yU z%CXF!E)UkCLEZ?R{TZoo`OoJt*Bqy6~_tCKTd z)kqxm6-wupo!pr!QAawkKASJ>{(m3%$mKG*QOzl<|?xVS(<{azg#6( zoZsgm)e{w~6#Ld3(S-fuO}KO}h)ZpP{D0kh0X6-(1U0#E7_r=^^AnaWh+UR|-^ENF zdM9^UKAGETz;V7hMUv+a8`5u zAwR6zUM9a+#kS&opU_rSWK%G)<>j$PZUk<@-E4a7+Kzn>FJ63@wh={RRY>H_F(HxB z>GUQxS=okjqMN`xBFtOIpq}2wG(BRl>C(65EpfIhU9omX*yv|Z=NG{ z7j4l|(ZWP6H7jX#9p!7v`?T;|h)(`wR@5KSNsF4|d1%F$(9kiUu9?d!L}e{Y#Jy}( z)wBbRaN#FpmV0~kVJ^@Dw1h7i*UD_PN`77&=q>T~5)GSY-N5dKLaL_*4Md&cXP_2k zS5oLC$~u*M;TTFdi=)ue;0cq0b86XX=ofPQo0BKMIW@9Kuc!75A24v(o&m$Tft%D$ zrU^B69$N26nloT(@iXSTbBqk)%w6bX)C#plAK_H2J9a8NGb1ObvzWWoyDabY-8*M; zA5gnvCWpt&n>;zj^c!EUz?{XDBxP5*OS70K@RLRe8NPtOM5BbH#Zh=C)}72dfBg9Q zyk0ijM-#tK-+%S${{011|L`G0#*Q5_B%B?Bv(dlwdTtWc6d%%y5=2VZ%a7cx>`LHe zj)X1C5zgKq!Z~PfsZ*f6&q@8k=2TmhpbK(R`5s8kp**GLuZ*R3#_RR*+%TkjBoppB zEmJab`q76vMR~H_j7%76l5mQdZ0t?xPbDXxIQGToCn?#mp1p>R=-KBZ_CY-d z9D$bBDfBfJP=j!+kcSifC`_19^D;IE9~}6283hsLRuOIH+C}`1UL)&Wg#Sc!ii?mF z{_Xn(^B2ajb13C(Jd4E(#`>Y1bTB)NQJc!;3R5}LnHt|SgR68}663;l*AE;hDLJtBXKLcKkfD(? zhmD+BnMAEt6?ht4xbcED^%w3zJ4*K=ykutGlGT`INfjzUv3|mzqv5Cp*wk>m4PQH$ zdw&1^^BQg=?yA7JYbBmf_e8Q&C(0k3JcdnF>f*4mix-a_|4HR2Di(BmAr;Fl6#QPO z;P)J*)d(-^&|HPLXvM`i&N4K|<6_C-nNBM(H4Uk6iN5X@E~j10p3YdT)mm+Mr^Ls* zb{j^u+3)}0izo6g96NR)-*nP!y~_&rrC+_8z7MQ-Qt-!P#(W$+iR}+p7j0v?{7Rl) z;_LS$zBaL(3~TUi6pT8+OnC@S!TD!$&Shtx%P}3Twg%^aWXrZg2exlLjBbC}U>@P5fn}UiXPh*cMr)~W_??L!c?v>hoNUa;jYxO3kW3_K0x8qpY9TzXM={4Ls zw3c-wt|#`AXbbk&OAv1e3=G4jKW|`-I0oaq>wL5nvt=9ms4=|`ovX{miJM~jpgINO z2>d-Rr7HMHY1_=m?IxMNnO4{@mSb62tY_JOQp*~V{7QxCj<~+@GireE-kPy)3R;&75sD7 zqyCCQh2C=Y?agx3cKy5*{5F2GJS{uPiz`e;SNJUTD_4;hzIJu?@`UI)%M+>Ncs4bS zTlO{gEy~56r~|kYn*Rw5ZH26#6LQkzg9|Us&4=DES{XfO)#~Wkt12f@2|9LXCl~Z* zv<9meDfqM&J{wJ1BNbQueV!>fD*?9nbLxg^TqFZ2@P6&m#WwpJ3DFP{Fu#^@pf`igcW{ieYHu| zTnb8DQ5NG7Yl287)-8J9H2z2?dKr&k0`WN1;;40H)v2&-1-|lT@=$c}2fbdeIgi5! ziudUIDKjK4-c|W9)%b|(|KeUs_9!k{8;Hwi?aKqzozB#O;8=cXDXU^i8FxIG8IN5r z;nj)y2vg_1{m{Sn_0{R#{2JeW+w?PH^_;NF(V8z}-qraS-jB%{Ifk#V5+un*9+D)= zizQD=9GSb~63CVYvhDA$)AdfpzxFbf*4W)S(30`%Rc$`iO1xRlFUUzky0|OccT`mD zsAM+|$ZhqKVdUYQ0=$Qwj`caI>GT#h$uf1i-L3qC3Ha{2+ZLe}x1lp`=iwbQfm^Vp zZbaj%xfsS2Z#l**(X?_d5js2obMv9Y-SG^@3CBQ(7mBwHP(eP*G9@=p{B8pKueA43 z(B2T8F8FKw%UdR+W%YORqeLl!tif4Ujk6^qgfBS=obM$C;PNON&T0)?BHRq3_MAg^ zja&bWzZs5=U4V7)-YFL^j@yHa-(th5INYE2h*K;%Ax*{AIf*mhhfEFDRT4v1qbVT3 zR3$%T#W{YDx%W#)2Zh}1ysH;#`*nMr&Tso8*{e1wBB1rt zEPHz`92p!KDb3TKE2uqt z#zo32@v5FZW6vXaRDzY3BXtG#v^s@!It&+rQxH>W)cMq)iUO2o>l6$+-EV?Zu&lEA zi_gOm?pX`V>74{7ZZ>%vDcLye-~YzTZ|s0Lev1otQa*>vu7eh%S1Y@V=^KkWBA2YUJ#~85F0LEuA0xBWBN3$#E2q~kh20ST3G&n$#8(&Yjv!oG%^04R1zKuiHn!ab zBNvhlsyc23OlAY5qLqrnFkXAJcW|-c0=~AL0~a=m>4rm)V`ZCMcABo)6y%o7qVapF za(1wC8C#|g7|7wuktaJL-)JX;!FeLS*@g41)5yS}IJ{6@&zcpJD72T1&*!VsR@Ste z4BYwQAUrW|;{&v!8&@Eo#pwdk0z`4r0#W?O$Yt7PpOempReF7Vesk_O&Yik#t{V0v zZLFFMQ;RloeZ^HFyV`h*AKMHDu7jkHlw}8;b1k}PFCiuaH!<|bGrvzPM$vC^cY*W0 zK4?aPagd#~nD#-1L%;DnT4!zO`SYQlIT#o6`{NdBGz+ownDaI}4xNSPzSg3L!aSB& zKyN`A}1K{2&xVQivapXhpW)4uHNecQ4Qn|07yGss2ASGMbm zzz?0oV}m}x@jvcB-e_}u(GNMIPf*9o?g~srcSMb1mf>ERFQWmwwc0On&f8poRIAN= z?G1&P*WR_8bH{46Hw6}x$7)=ZQLkrrpx+E!kE#~m5d4mRt=yi`+8;AtuPzS;d6H^Y ze-k1!O_fU1H6uWI0iQr}d&`+$4Vlm6UU7`0DcZ_~E=N5#K>;qDb?NxnahhcEdfPQk0a_UxiyOkzR2* zcO3r#QC?YO)hI8W<&%H;l%GGBluX8zuiFjrq`0a;;%A*%J}I67zIz~+5n`%r)Lh~V zv5djGF+Z1aGL<LH}s`wj{^U5D$fNZceKc(My>XuaA9f}Rm-m0J)(r~qo#s{+wlFCS<=ML$P2YoZbGK*_b zeHQ0ujo8fH!_VVZRaC61WOrN-pMUmbS|L7)-YQ4Eai(@(I-SNYgNd9stpH2Y@6%w@ zSD(#7pB2U$9y~BC$-^6jtok@hR=ua~OwPVGh%+oBcN!my?}STdrvCs@bsu!dtPbgn z+Q8@XApO+`(KzRwUGvcYoUW=38-$#UQduL2N;_EdMyp+!*|_O& zOw9nj+3;u}^u;~FO8xognxk4obvL(KZTGrQVq>--a1pi&T<20Me|w>v`y5}w>8L+y zi`t1scC3Y7_huqo<>3B6{Vo7NT8 zS*a~3&?c07a<7Yp6mbkw&r;X$9*lmr8PF}ONMH#t4S6^sYCN8(`wYp@)>k;4c-^{J z(biJVM@;bU*X%^pj1#^mT(vQ}Lx*(H=RLSmZXmVCV6a58cYs?t_)+ybhldFP(P zBZd#d!Ma5yXvurrP3m7P)s?&155*a9XWMjdI?H*-sL?xJ&Z78mO=_xU;u2#nI;BrF z7*e@OmRPeCc)#>y-nIc}?kPNK>@Ym$$3<7s>>#d8l%RYNnxZqlWhdxV)mt+(c%92} zGHJV7=VB9%t=KdYwE{0d_(pUqq+)O-Z(Bin z%J6FtT5yrimzILCvvocX3HhSl8T82nz6HiDNR)fyyxp7kc?Sd+mscQ3)SB%^p z7%scxvWWC*!=+?R#ieoh!rRq{O5Wo{D)gXFVlB0nkMWM~HpM7vafrSdiRC6&=1v)l z!Juqipy*7J@$J^ReCZsvf;!0cMJ4zSKORKett}`%iAe6}%Rg~Re5w(9%M*9QQ!~-@ zU$M&&>S)J0+(^sgjc8x|5%t8A27d_NemMmM+`?!`U~H`{s^6wHvuyn1dx)R?ZsQuGt~}yz)SA;R1ZC)nx32 zGRrIP52JFMI&%qh@~gNp-o691LiDaG3uc3KXeZkV%{bdWpgom~ld%rfhlF4u3RO81GTLSb7-Ux1ckUY4Jkb0~a>B zcY@8=p)9_HwPkDAWPF|JO|f;E zJD$k5N@09oefz`!t#9;29E5+>vu{%JsX9zW2~QER)~t86saX%b-O=3NeChfUH|&Ae z9zTh?zOQ!V%7Z9fM_1@=sf7JtuEUA6zQ}D_y?Pz=?dQ(rS4lTZd7U)OiCRc^&&2bI z{oJWWDkp@`#)jZXTljLH(%ZUWM_iDB8ncXh&1eDXWtS{J#uf8`v z-AwDr==8WSPpn&Fex?@xL@k^Bu)YoXyPf75d#K8DwTvF995r^vpL4zFY_1a($*s0N zT5AT=8RE@08$9$L^k&2Ae3K7l;QT2HYpYh3_OM@7TBsRYv#>NehhIk#$EsEg@%3hk z;X-uySYuv`&sz4gdQ`7Lg&GRYi3>yrMDR-?>s7 z(biV7?mu6dFtYE;bbJ)lo(++GEn9jO}jP#&o)V z_gPazv7M~d!9@!*PRs=p3(rFGgp+Jjo0 zBYDPwLNx|H1F22TTHH12ar2QlKZ$E)t;h{$w^ZdmAuPFfho2z47oW(JrR#wlRH(eV3s) zsYX^HJw`t_KU#-3aJ4K|bnv7mp&ZD!oS)9cy6 zsG&!>6Qb&rH#;fXf^<4lX%juAO8z*$OIk%0uS3l z0u@H4M`$cIvT1kRE1b%l${MbSVOO(sxuw1oI;YWaO+2;8fc@cU=fO6W^0me1kC?0W zHLvD9&DUn-t8krFRyK>R+(_?gx8yMQy3V7t?$)~Znzde&PVZ({gA?_j>%yqlC_kUA z*Gx+lbDsRsi1!l1EVYd5NqbTr&&yntcF}_eiz+H(>79Xdj&U9G>z1hZIF{5M9LyEg zsA$a8)#*4-Pn~q3{%)*~n6l6BhBJFs$0~PxZTnBUMj6(lcIB0NW<=2Gb?hkQ+Nj*A z!in#{beiQ|V3fkGdiQ}(;@1V5S+4IT*{fBax#n!A7wz3>XG%~*{2OXmQDM|D{-M#h zjLrGYy`oML^y|hMM{ou{ga)I|=nXUor{L3g3R;cU(FYHN97F?6Yf*nn27Rq)u0^-8 zI~*QvrkwoDbiF3y$OY=n41i!;1BFU`LQQ_b9>nUmk_t<8<^$xLE7NHiw-dRz8SA6n zF2<&)5x0Y;r3fYQb*n5(r0 z$wyV{fXhLoa;d0`HrB6jK?<0dQ?}`0^3wC?*&d z_Uqv*;)>UVvT?EPHNm@VQx;l+=h(k8crUBMN30$v^0Dv`-oyEgLu}7oxl4ihvvQ0U z72r1fq2=!Bbb`H`Tg$wcRXn4mt!h-ug|;%r+qP3>M%6SrzMXJmv6Zk+qhaIF-4PmG zf0zdNrC4?`Ds7&ZP*W=#n71z)shW;ux^+pKeiJm!2fT_#cxvp)bmCZM_GxMw6KdK@zYRk9H@|># zAe79(|MD=U>eJG+7r*`XJ1QV;(5R6gelVPGnawu!4x3b|=4YPL)smqrMC3156ru|l zX}LXD@lDj|=5^E*-@HC+`KPm^5)-LRc9n7&o1v$=>G_0JJ?r}DbRLKEb6Dz7+?0t+ zmX4ns&$rOZN6TPTyJwN1ofz)~NKwY)q!5*3CDO4-heG+G&eY}_{sYy!c@x#gf83b0 zaDMdM*iZ0CmT{K%&n+y>#hp2WJerHs<8>F3le8DMZ`V>zAHLgn$Vcz>9b^ijy5LQE zL8d{SdT!+GOBZr{d# zq6W8aqk5bZ3vi`1Yyfkb?MNFH#$%^WaYylPCXg%FQycf~|7>&G-c50FamGSaI4TFX z3(JuyK(0`mwrVLpBlwA}#S3RH9x63f7(byhwYh~$QN3S4ApCntYz&LJT;_Hqr>4r~ zA-Op@x%g8&o>|Dt3s;XTP^MqrLVGg+QvFPR{Z|^iU*aG&_p8|g?&$z{*iO)?(f+{$Bz4W z$hfiTbOM{EY{MF9|B5L2Hr?I3I<|>Xqj^|yUSp*8#fFD3UK~DY0oXyLFmEf8_Comc zcQb5T&Qd|C7CFQyIub=yqG>3T4-6|zqw>nj^YR{+X@*amJbJ{msdOLH&vI9J-@N?% zJhmKd!DE>SETjBPlNue}zWr!sQgR0RbwKww`+v}_`v8+#d|Z(NB;zYOStaYl?>&De z*=o6y@fc}`&YX&}a31~=je!1r1r5i$asJtJ=d(}cpOg2`KXWFZEk*0`9>$+DQteEW zn%cPl)%mYeE?nIG)dk9XM8AIz4GwyLxTK@0f{yZ6(^~eLteef%*gjw5FX*WU|6mqV z;4|qems-N4;;g)T`6*A<)oGS2MrjnWiJh(1C9CPb?;+>!e+1wC z=jF*umrR)!pO8)`8fZUI(^Tsyat0ypJU@&q4IjUB*|_kflD&wzVEo!>8*^oTg^$?% zUhd)sVBe~H5rp>T)52u(dq;UILB8Y8l$CLNBun9r2^J(SZbWMD@{$zK6X%=z(Csm!Suf;(A0h{%k^SS!BN>tv6+goG>h!sDr$^46 zkJnW&J;d>h>y)3FkFYTj`Ap3JVn_b@&v)igPQkt2{W$oYJ|CHasm^$#PV|EPl?sb& zd~RQNyV3>9t@gs0>oL?;5lxgeNSu#=td~nzTvCEDE-o!CI|!Dt3oPYTi>2gh1WOSW zEHZy>J~Ehw?V6FbecSQl+mer=duw_9GTN~aKDO>E>@eZ;ty+rnrT}rS9@xq%oKjdA z0Sgct)Rmf2K~1Y%Pg9lnKKc;73_l;>(h8bk7vdPS=*01Cxo`fB8~G+Tt z$QgP5n)J`F-Q2RDJ!*ZRg?wN7s#WQ%{%nx*wes@IZb4_M!7bXh@oMhhwuNya(hNC? z1BM8@sl@EF8-m5Pv^&}|-hRGSH@~**WGmq!RLG(Nw2V(j<92X&*jpZy-`+FGx#Sn5 z&fbSM&*r90TNW2NbMYd1JN=CtdM=4w2=QF5o;&ENW3ro{UGb$M|EuK}Jddj#sob!> z?}d!+*FQwqo7PX6r6`qTYsY7i+LP{8Tk+()bmo2&M~%b0Zsnq25YUa=%Pr=T8vTe~ zI{*^iE@}izaBWy(d3B3ZM zy8Ln-xn&<_Z*V(#Nu%SqRF?6ON1jMaJHeH6dGcgl&@d1*)^+oYgyjEO68V{X6jK*f zOr7>dz*OTEe*Ibpk`8+`7~=E_K~iaFdM%O)uEonvhuf-T3d%I1lPFHu`n-jU=hij( z6*Y%8ZCHYgXAdH$YusmB#zm}4oG}xJ&{tR=#=uv6lkeHLapOMR6|Z4}xm$XC#P@`9 zpwGqVMeLXu=l&q}+*U97`S|#^@iE4t+<{rR*T^hhTYG~>P=dBfd!fy;H<0#1KUPyy z(Pb80-p%5Z+$}b}5%nf$X@eWLkppPyfjQiuY0H+(9ZEjR zt)!s;4@)iuFF|t#hfVlUbS<1J{21?7j)h}`R!gfz$HJBuIXPJ~-)>p_vdnH~tG(T~ zo>T}+g>s(wlJLw$fAkW17azfwGL9U|JbL)3nA0gArpe0EaNg-#lhV_Zw(Un(26pW{ zFsN&{0c<({)Jw7Uuf?}EkXFiT$*%Rb?%@+#fpf)tu^qE#O@()28<=u{w{ZPzT0shb z{V4@hC+XVTYqc;UiFfD91fAwr=zXj7gO_uCZ#Zn8;~H44s=eq|Id3jTaH_F5=AcHC zZsM~`VP@_G!%-RSBu_rEZrurP2XaBK_zu2{W`(}?^2@J2_T2BOor8{UJz#a+>9`A^&fJOfYT9#U$w0wq$8 zl><;V8YFzHJN_EF^oB)UhK!HC6^nF}P9*bK@zf<@i2prtVm|d;7O*zIz#cI$;lT3!U6G zcJAeo$#IhnNBpzlJfX4=nF`?O#h*vq#_Z!0C{DG_=To}zC-rGTh0a=XD0;%guQwB~CW=v>^RxysFoG$A|D4`Q|U zFoPHf!Yf<(G2Siw{D7UW`H5drPJ*|yj~lOry!9!i-*VyUXs!H^cSBbnZ}_WDuLcHs zySc+@%&8j6&AZ(`-~zDom+yDloe4gcTT4x5OXwy#x;}f43LmnLjmM#k3Xj!^zfcHH zs=MN_pEuPE&-@(U?>w~6yZ+pY9rF@*{XPV~a#)9!Qywu*`}$JJD%C`4T=$fysFd#9 zrjOC3zJNlWB!|{@uZ#UoC!bPNzHx87yCbI8_>|tg!R}7Nl?N%JWllofy~eWU{EC>b ztB((V&4K*=Dq37-gp1>SjrpB2sNk2k!awcK=_#XzzrTo+@n@X=JXLufZ%3o?_F=`B z>;H=?qTR73A_5mrnUceLD6sjBjBoW|ur9=a2FXHYGU+t}8A7GkluRX?FTJKAdRQX8 zW^7)|!DpvPuN9yhr(|CNeHEgh4lsgy$=;Vukd2W|mraq)fFn%C>(5*8YPxKyKu5#> zOn_GrFyed3-js#Ij~Dd)1lc&*c=&9F@NO*p9t+=|1vC`C`9AzzID8`#{?1F*2mXF0 ze4FRg6JCeIcWVEJm*qQyg>%l3_=XDwvN-rbvK_LW;HsLSCP0UxpF| zj33apOk1E`nXW+JX5I$+0rLUSF-$1X$;@P+k<2Wh^O$)+*D}x!%z9=$(5=i?ph-*; z&@Y)Ufqun&1#~~NALw!BIM7E7M3Ztyc^#mQ-K z&@S>WKzqr10ew&Y9?${u0YD?^NRC8`Q7hG@q^+Aphb!z zpua1A2l}VtPoVb|_kkJ}Mxd+$SamsN(k19cjm8X=afSyr;_9}l>-T``F`2eU%33{dEl%TOHM#TVC zsDO*Ao(kGW)mYUSsGkb-O4UXMZK8Tb^$O6As*XV4P`v^4ZB=ie@2K7Z+E3LF=s*?d zn`)$LB+zjxkU=$7H5KS|)eNAsRnR`FSXC_0#j3?Xm#9GhRS7DvO4Ul$N}y|0pp&X? zDzHt}b`{vBYM%=FU3Exx7-)tn18Aly6XH^S9DzHb@cPh{a)ekCYVbvWK zT&e1=3anD~Kn2(9z&L;oI5;}gol|foQMdSkx*!8a&`;n4JMLpBA<9&c%Q=;jAJk-y+@$Ubu(yx8E~3XE^-6 zDnR%LB$kmQ7!ZwNiASnS3v&-YZ@j~Dj5>1PzUTZG{!w$$Hi_QgrXaw5gMaR1Hl08~u%Bc|qUL=1r*8LDx38(wKx?xO zKSkrChwa!yUGw#J8ojrZW=)r(m{^Z#0f}hDw%Pfj?ZX7j*9H8?h*3GD-O^I-xJoqG zTU9cU{023oHi9LDTLkIW@eoGc@@q$ziEdm&5!|>+frOpcA82Y+h0e3<1{=LJcm6i# zd4Hb02gIVR#NuATpHVz$rJ1lSw8WCoZQV2gSMeO@C7AXV>sgy~m?+!xoyew!8$m~Tn+(hcqif3q=+i{s0WgsGe1i^@G+ zr`$Z_wemjka`RV< z?^SQ-$9Zhi>&e;5!xo;?&)EOA?*rcVDkm+g9z5^)%*W-JikF_)Vp{yuIUih)45k^c zz0zNg6S@PnChn6;uq|_T+ZoHFY&CW5;7{*V-%rffGIwfPmgjwd?t^L57A%YHZh_ezT=cp2A00u6+El0uKPA5siE|aF?X`DL}ra ztf*-eHts6lcdGrZ`Do*56Y#xOrSaFf?0DNHM~m}HjiIDQ%$XWrbDOVx2ZXO^-3D!4 zpN+NZ&TF8l)kuAE*RA<6#i9ZImK;)pJDv$wb`SDn>QlH!a@{eu70rb?uv>$g`;6f@ zvo}V_PCC$V0QF_sZNl_v@0Ah=+2u5g%8TTl{T1uy=eOg_mAFOvob+A(4;ni)0&Vh3 zZ#f6kk?~C?qCE_UIZ1*4ujktV(kj6jb%;~_{11SSAwC;W_8V1=gxf6v}0? zYbYA&Vx@>4nzKB(yxDgP+o^Kp@Ue~rY=+HYHhcm(55<3Z1`im?VX_}9Mb3chvKIc? z4L}|k`6oeQBIleYJ@ewh-nH#8mx@iIgY-Ux)e`fL$Lc;%$Y7GdIYp!wA?X$Kyg)f` zntLitY9j}I^`)+seN}G*`tl_xyG2cH`{u|K;#f7Un4|-}jURZN8Eeb)J7?d;58~SkDU7wD~!q#ZaO@ZKmJ(gt;O9BQ% zlMlD#e0d%_&*1*El8`j0*(hrOy}_zzqo3M;Ht2T0tV%$QtH|67B~$J ztZiStSD6zvV7v)}TeTS$$pUUvqWTzu@>J$RIie$XBLPvk-h`=8D%8v_N<3IG?9h$r za7tncb($aXo3%)Ya{I+KS9e5!7sakjDIvyUOyugPa&6D$P6#&eUaEVyPf9usft9+zMfd@Y}Lx6z?k z83h%d|Xk!`_->_9o5%pCf0bx60iLnUWy_qXN?ybiwG`JZF0l#C_g*Q@?yLEJt#rUsiO~@E!d+^peJZ{%29)`DJ*Y>Z^+P za6JW?bK`9-leJJ%!TIk8^D{wQ+AZ+(O#*G}ps(&<_W50~&;I@)!7kVHbu z^O9mbA4~WA$*|;Pv-y(y3%h8-^>&#y5N?RNIdA17C;i-;lwts8zfE?9qGCweaD&xs z>4o@ne_u(30t17^PFKO(9@VxeXU7-b_LvXDkk9^-sKq5eecPh?8y^v|;HncS7FIiL zvBUX?XL__Zl}5b z=y)D;dSQ5b>FBJz{qCZiMK?iUv3MHb%~I1rPQu+%Ny|6+c)-b_kQBJ|5%|?1LlSKB zD*ln^8!%np-LUl-cfSj){`Yj9YUmYC$7Z61+q+J=t7X3VGRObPe9v?!huNSI*sdPb zLgqZ>eWSTz@CC^+3K%{=Di+}jn>v% z$i>}TK!_;M{qwf2Q`;x= z`QvOLob#iZzAQnFg{9Ez9jYa39WXrRi(&~MNSxE@Y$+Zmj3Pn$zjETuX@m{2~rR`^x-We%{^y+VCZa7Mho^$ezb8QBHDI znQF+d#Wq&va2A~xLl-_v6Qp;Mdkei?Jv2T{w_6CDjp=Fc^x7>JP0(hDUN? zBQa(h{p01wh2j0_p||G03Vnx^$z%5jl3<>^%Gh9S(mgRVkvI`Pk&))gsEfShG$ zeZ_t3znb3B9RSmT%-$nPi0m@ub4 zH2BfgFdAuM(cLiIFkjFi&>!evXi^pEI|$qTXw}qe>-A)Mmjdu0%l_uZ{$8M6pkt=B zQeSDav)GO7TM=xxGbd6MMpF=Z6Nw0C#W=*sqWw+XuBN1*4^zZM+(TY%|6i>A|Rm-iqzIEaYZN%z#n{;-!E0ZrG`@$gxMp2Did=|Oq?$GEIe zsYR)#QQ3Tb&b>%XGD0#Zx$Dh&q^805vn%rVJk7brk@k`5Qa5#@mAJav>Px+ZG1E9k z(&aBI>JEprb}O&8d*kczK-v%;df`$&TCtk@Acbq{a7;3WyfHHinp+lb%=2}eMT)-dlJ#=H2k zhZkvGa!imuj9WnvzFfZdiI00<+w3vm>av;u%V{`qnjRM>7 zhL;!9u1VkZRJUvU7LSq6NM`IQcUM0a4kLM?kof!0j-ThTCPeK-@jor(Cv%|#eF&VK zeSQ|CCdEy_7e zo%N*VvqPU9YWVnoz@;LSDW=MvbL97@wqqx=mJFod=MfKe<4}U9^022 zbjUg#JhLvG8<%%=_#P!Mn~IiCbiUX3>~MX&BgK-gUuTV{MbGN0tm-t3| zXS~OhI~!sP?jL$9ubuVISx=IG1UM$t5abkMCOSJ0J#nq7-n3u6-<)3rs3909q!YXy zVl7fFq8Xmvj_s-KuI-WSne7hU=`Gbz5x%J>+QZ0o-15F!%mh9*55{Bdnf`2nG7@Tc zzYn$#+~?j0#>?b$#xdq8&N)sX!!X;JYx1@9w%vAISD+V<*QWR8w^C>Qz4_jHcYR}d zKy|x?5^DE;>NshdBuku=Yp-dloLmk*Z#M50FJn$L@1<8) z5B%cAOfAgKs9@kG>SDhQ4tdR6GQpTSKKcT9??DtIP9q4NWC&CH^Hg zDOB2jIwsEVv{TBoiQ6Bp=NcoXQpCC^9$Js>r}itw;>5CtMPm-}Be*f#CZ0o|LLbbo z^)F}VglZhsj{~Lxxz1gaTbZt0H*MPpT(_^g#=T44MV}Y1yf>^jCO67Abvnp823sec z=j{uORE<)Mi|TcaN{rp>6Y2}<`|ERzDeDL8?N;T}QYsTBgp274jA|8SN>sDPxDK7y zmPb1jBVr9v*sN-CY{?_vx_DFFGyTcoQt6WMQqsCO&aj|IlnG#M z>r^)DhzEzKn?;{xmgSUH5fwI64A%7lO&>6vJ;dQ`*Ujow)Xw_Yr~WLpiQoSDl_Q)9 zh2q@868aBF^>Uaaj9M1z3(txL4m73j7_BBh{LO3~$&fj$gWtl=Y`HNJF>D3;_i@L( zp~RyK62jN-ny-fl)c368WO^qq=2is~+z}k;Q46t83dML)J@gi{*9d5da&mK9h)@Tc zqpw~RjH*Kw&Ak=gKg1fQ|DyluA4m*=9x?+Bv&dviGTW(`?)lgKx6&7GWsTmERZ|Jt zonVJIQEhyP}Ev$991;34)uxv_&W0ey|?^5#y4tXU2nJ-Jn2( zc1RJOdR-sF+YcS9FUmUr(Ry=D zc5uYvy+g)c86XTDNgv6AMRq;wf3f3_tPc0i z^|5O}q*w1JoT3$JniCj3gvCRD^dVwZ`qwH_th#Cui$)J~C}Qno;fpVHH*3itv7C_U zCw=nplZM+e{YIA^&Sf*)e)4)1=dKa?kWOSs*7|+up*_UDjQ36LGuCa49{P}aOoQ7R z`b`T%pjbOpwd?HlT*3H1+7SBRJnM!N{p_E7L4fJ>>MbmDKWC>*AF}v`Z`1x%RQ=5)oiSL7CVJ7kaItGgdHP1dR#E&M!0>Aou=$!p6%KgAF(D8+jYt{$=8izzYk{v1CELgf1|6(nvTes6N`XJ87%G4wG~{2j~!|6Xnf z0r9|N_m2c2ACw!Fu$sW`E#TvR4ADaNFdF`A-s`rSH`5-8nl93o*S$&YF?1>eyW!2a zEf?I&?K+hty!j%bpylo`QFkf5Qpj*<9^X;~9D$#*LF%z?pBjF~mjD`^8{@^O-Tbb9 ze}S7-*H+rYZ)QUUG|8e`jp(g7Yz#gL=2CB$hT9#H=;?#FKO zF41ln*j04Pa4IXWtCx*%L#P5)8!9O()KcFrYzRejD!Xu0CxO7Cr!=5=$OPY&nL(Y^ z{q+D^@q>YJ%kZ}XoNig&iSAIEoFQNU<|mP05DYT0aOi)o*z3k(01~5=;AKDUaA|)5 zTT`UFf??xv14tWTz+#sQ(YO3^29@A?nzIckM+F$=C#+whPtu|1=tt4}qEGmph+s6! zlyPvw>K59g@M`0n>lm!FB+At56TvO#RD)B9XxfP&N`D2a^svZlWND6RkOl+AF6c~L>Fd2X* z$vzSS453usmflntsk~Lasea@A9i4@#RYz>@VKpON5e}3wBc&*!DTl}&0Ixo5E^H#Y z*z? zNRa#Xk;gc8T=BkarFXEj8(|7?^#EeES8T#=3Q3Q(W0?~gVr?z16yMwyoMFH>RC&Nv zrM!8}HlYA#>boRV=%~M+w%sa5wzNv#_bqdp$pzDK)}P0z7g_n~ZF1uBvZRCNJrNq)os&%UUieu&$fb&c+}82}e5AY^Xj6ZTVkJ zjO7~B1r4bTsjOUVu54@yH-lG*#5`!bz4v?!hHTp4zxt@pyX=9*B8l$-2fN^kDW5J* z_xGvlj^vLT$UmEG-!CLywWLnc-;F4bZ2qzY$t=ngn;O&fYo8k}ipCaDFtGN{{W@oT zdPuKyrkDxkVCrFZ^6k$rYCCP2U7VzlADncYeKxVp_4nqcFHfuGV7CQtM;2tA)KpMf zO_|Ta7m#04YiSSS#VEhFu$n~bz}byP0Es^c{r4}*YDSq}A8ig=dP+nBao!wq4McRf zJ77&UP4vyplFoT|Q(kyExigtm8nUFg(RF>Z0x+OS{b^0F%Vndaog_I# zoZE%3zc`DARYoQ;YRHs7%?lD=7bCYRz|y_(h3W-6v5aWa?>aPSqut`>Q$BDY)rg@@ zWgJeI53R@C9E!d3OqVKqUUNjd?yy5qO(9e#`)dPr3XV`fqXQcY%iz7Jboiv|(5C^? zl&vB7fOgc5IUIrC_9-B02Xn@Q?AaFz&tKEhPt#I#Wd}wW%f-R5Wj`Qhj5=ty-hSDv zE^p{IbWx$-RM<#v+Ku^qeRZ_-Y|s6{MRP6xFCXxqeD*4h{X}XZ0fi_v^!bstqk{w< zlMisD{?hQdtr2LwRW`;qU9*zXMf%j^W5%OD7!I3|Lf5~B6Sgk2^OTCRBuT|Mld@*W z7g#R~@eE#36V+(~&Ui#i{RZAwJG@E<;(q!a=ET~Rg(s*re*KrVjyY^I{Q#{CY6|1% zM+#Kc3-~hw%MAPKM4D60(@f&gf#2atbn_VM~DF#)t9Z{ zAwZetm;0R+iEhA$8P#epZrUE_z5!+Vor*hP|CNetuY`*}wGC;+UCM5PF0Drqmykzy zrwRGd*wgr(V%$$x-!D2Od`+?=;%-A`W!>xienCjxJo_XB*av`sU48@GNfz~`Q4qH6 zg~cp3HgI){N?Hkml-GIG8IQy$+T1cd-Wz=;rdcjr749Acdi0hr!wYo3e{TEeSN_-n;<$BxEs!T$*$0b+cPzx$c9SjQP=>)ZS41SzW@rve zhGhT}05*4+<~;osN7G*SrA?1UBTTzyj~|+CY)7;3F$s_YJM=Y;3Pi;_YCYJ0(aUNg zc}jZT`ddy`C9qEp)q@a8ZG>8;d36)Q<4HNhmM1v$y$|L-{<^neZ#`#aaW)`bhH{7pwDZ5u5^BRcc}hSgZtoz z8M#V-Vp$8Sea{mjX+s{cPJvf>Bxu=)Hw@Pts~e!vmbD<^YrK4u(L?%Jjf%%@CRvLR zmBK~e7f_w~;c~e4*FE;HBwb6Ni!rs85mAAk>k8^jqBo3~ltX;14e`5Rt8Yr|-=ya~ zeu=9-*YrB1<~}YIv1ZP&tnlldck0lL z3X4SWHL$%M?nKC|s5B&%KwiC{>>JNF%vyZhLay>ps(NDiGvBPpyvtVfRx&l%x9WIN zO>C2MmH4&r{qIc0r8@gN*dDWwQ6!`9UWayXYL(`z3Se;~B--nO0wjh0HMFYc`Ul=P z((~sW&p|Ki3Kqjj@u~_}Rwv0id&7gM)d_@YMlkWOkAS_WK*aEm8QjiN z4}Y5;M?KP&DA7C;GbR88YhcZ+Wh2f52#r5F3_n+*i>!STlI|+V)OUxU{eHC~6D4x% zM=}oxY5R@X*K8ogEbGa{9-8;HAcsd00{TU8B1>*nPf6spqZF^s%BuRkyNW?iO zQ<Nb>lanUwmx?j%hSzXMEUZ0co>DApq;wSeU_C93|*?icI@ZM!~ zIz9XP+?-2MQsU=lU60Fy-i_iaOP7@6}UWyrD3?s=I2c35sB19r(GjXa|Bl zP0eq!gVlz{7g8lC2-8NOnLV~g3gP)_`qV2>J1Q@hzlnP>^b{UgVvy-MZNpQ#n2A_x z(zKV94423{zFxg%^0!lk8{mVIgr$&}O!S*K`ppQ0jwDQJ4h*6iQagjSc;qOTWH{^Y zqg<{wg&0Xn9b=|K>KBW)fa}K^Pf$2op@O+udfiyKyFOs9w2{fom5D@h8O{1P#B{6} z=!sc{Sd%$oJH8n0O#eh%5s4QC7q|Q<}c&R#Lp6wA0GSgvXgP?VG}JR9R;~H zjVxvJ0Qlio(#TF&&4l%p-FAWGtX-9A?N-Tx{J+0oTTxQS*OZtWqAyC=WfAr9|B%Pi zEY_f0k}09M0@b-&ZvVs-qto(f=V59<6>ql%GB&19c|GH}rod0DxRhPRx1DK@SN708 z>}PT9a3wNmTd*Z$bo8kG##<+JjbTAj{mp}XnVg@APPmCW)Mut5>ofVeEi~)S_^-Ic zLPs!|$#TTb$%i&YJy>5#HeU;tgq8x(zUzSlr+DPcqp8WGH4b(Nf!q;6?W;|c`cIwn z^sYTnkN)IMQJ!q7G9v3&#V5TS zk0@ul9Gu~s>V9tqR3%CWQcFq>PC_Po^%>n%ezU6$Eq{_HW29N2o)Hg4tFec%KOMQtJU<6$ z^pCp;?5{WN`=7ra0 z8Hitv55pwjs~0-!qW+gm(*YPU{HT@T!p!iN`J@H7mS8{2R!p7hk{@3`Du-{;?|vss zo!XQ~%MP;sbcTxz=;z=5$8zDr{gn6j7gkF$9M5h%0JkQ6vAP+7A#gD}T2{M*lfxt3 z>U-C8zFe9bZIF7@7}K&(@ngZe36gy+RL`PdvGg~ZcEOEs$xg$Rg>phVyTx*}Se6+r z16j=FQW=%i!zdDh`7{f9&l?{m&12mWR4C9K$?nqzX-kO(Efrk*BQ%8g;m;0D`n2!s zv0EZu9q+if-7R79a#a?ek=S4Wr$Z&_d~R7foSFlj?!Xy8wZVP(9(K;wnI0cnBuz`u zvFUIGena!|bh5TLcwxhi;Fc|I;;MMmAv=ZZiEu76!(r>)DsN&Q&X*4TA2BXp#n`7-*Ku({&{3leyxHRuzFE+hn-eC_nuo>u6?j@l z^RWP!xIb#UzdTDj%$f>bhk{S>&Kf~xO;LXG0>p$eHtIsutGM(pFF!-UQ8Xg*O75=c zPk}#<7bxvOX>8Po19tKcrThohiA~YLES!uO^?Ol^`urQpSQRb2u$Dp!L;taR89bTdvu#xnZA~;=7S5? zl(`kp#8+Fz{ER&c5<4`ruyy0I%$z;MJLf>n&2~_>WnP2Z=nN9oi_BL&)s2C)QIZlw zt@y{y&xuPW+8FIirjbckeYXvpV>|)f_m^iUBrQ&ZQy1HbZtY2-R<*g92&j#Vz=)&j zw=1vLsSv@|6~|v5_9rvsxq({`xF?G`!#`SkRwF>1LP@Dv5B+8#MJ#+F;J=2b-m z@^X9D`t)xeO@4oHYP2HvysoodbHh69z`Iba`EtV|4o(|%lh*8-o-3gpusmD>m@g@2 zDwCRphanpGHsTpgQ+H^++vBuQNHlgd3bFXgDu1=EX2oIf&F*txl|RKBBzEt&(~6{V zuLn7}epL&ZOQmZoqD*`>M}GI4!hv3-HLPj~f0kCI+>dK^tQaRu@!OWb(h;_LzU>{7wZ5r|4t*E6U z%;6LQ<9(CjDA-NTsdN`Qh+NO&EXex!Y@96cQc=v&-eKY>^Kh`_=m`{i}{?HWYR~K6nD!({oa*w&Whlsv95lvjFZH9!8yZ z`>?fc{mfi<)x8=&=?BJm@nia0=W)=^t(ur-kFZ;+R*z<(=r;dZBEorz>}IXMR;~Fr z(IA6DW=r_SvJWCjHfv98KD_ZrrSD}j=>X%gvec>BZFg$Wz-p4FFpCnB&b%lFr#-LwE){yJvRifn9C#m@z98LF6_EyJ# z#dBOPJ9bG33O6T5XV)vT6kB>PJKi}s+}&FopDt&oM#slTSC^5tuLhN?YZ;8cd_=Ud z8H3QBy<{|>zS5g7tQo#&Dx3R@bCJA;qVn`Jp5&LxaGWkz00118zicw=%}dL+n@dV7 z{l!pKc+xU>8aQ^IsL*iW8;-l~zaX1GxYF|2!M6q!Iym(E)f>y5NUHLZYMVoOHfNF? zeT-EY3Z1F~+tb-j4LYlt5Gj*5T>YO2?iv_J*|=u&1Y%HpWk0^Yv{pW5O)NxEXON8p zo7s?Wbh~k}cCZ?PIhr4l=)s>2$#NSTM~SywzCSX{iWz8;FE=-;jN)-3K0;i5*tzZ$ zj(2q`%I9~RcP+Cpjo@WVvQp$y%WkPLWn`qAZqU?%_x;P1#>+7%?2M9@Yp3K)M0l^b zCv0JjD@M^dWpvJPpXYaS%#RmIm{;!6y6Ct3O z2Fay|oOa-CnvEwVuno(bLffSJRizo~Bg2F7_#vXi=s+Dci0v55#^GGiZtzJEhHNfRM(RyTNQ7wvL9Ug8o@8R-0gGqUmbrWmuZ%P3pM%6>2 zP{k;RoN2t6?GUd;W*|QR(82!Bh*qHotEH&PF0cAREm`5yn`)P>@DH!IV%*OcLX#r1 zMvxALd1#&bITvBkAaLUc%1PS3?=BD5c%sjsm%hcPpQVM*;(u^9!7!xJQ$N6LtNS=t%Z7TAJEN-QKoD~W;t+_OXKH?-K0sXh!y(o} z&lG*1=D*tVRFr8eAhZB^x1w|sL|t{AF9`M*g8!_({hP%_o4MU~4;HD$Zoii=f=WSq zU`;Jq)v5AmJ{4OL3sH)G7!n)_?Jbaw!1f0R&34pN8m^oNJ-Rp+h9xHO=mhG!r&S57 zcdguhhTzpiDC845)KynWF75#*WjSGN9M(n8(BpPZAdmzLndAX}fo0~==VnVkuRUT? zj*}%WynZGz_rK^}Bz`6dY|;Sj*~E-kL?Xq>qX0BRbZ$k!IHW+Y!w|d8zM%h z>xv0JQ#*+K;8$VH*!RPosh?#(CvY_tsz~@{OAj(MPhZceksq~Gr`jwk0}3sG`lii> z6@1ZVmKZKZryHv`QNvVfWTw&XZ~{T8J_6;<2KwUv!D=PW(nJ}jE-c!IR+~BWgHsNp zz~JejJ#D||*3|wQeg~NqL=NrE9%EGf)tWj&X|=7Q>k@kHG$9C(2SHB>im=4&TqU9$ zr<3S{D_ve5YctR696wI&MZoU4p;f%*eA5Cv%%^tV~TQSS;UG0R1iFs$#93XO!%KIw6p+%W*26=P>KJyeB z>xZ|lp-Mxlo2}rggDusXMc-x;w-K8}a9WpLsHl_fP)A0Djp1w##{98WxC(@#1xn(+ z_I`1!ILlr=R#SQX{pnXpoB9ws&#y75X0A@t4Xi~ECb;#CdNRVPqh4dfUkPx`x^ZLA zlR6^hVH{~i)JIT4s z&T7ceDh+ZoHoX(iqW+2HKdS2^$^JAN6k@nF4|~Vx+2&41%z%$c1k^1!7c~)w{H|yY zfs{C4HJgEO^C$12#D~m3w^HNI9Hk9A{>Ol!s=zRJ_<WL&R5hn+HUAQyzQ-DF)6jmTL#Y^pYxMU(<-A<$% zYh<`_XvI`9TSB75AYzafRbJ9;-g4l$=HeEd92n^hdZ<-0n~s%!-}^TFE&Zi|ov1V* zPACN>oiq_=t-HFdg%HVq=XCohWZqw12}%x7LL;6vG}#4^Cmt2eTb$Csba2ioZz{q1 z5+pCksM$M zz#0J~5%W;AmLWk^Gjjvw$vJ^1&WoR(*E)(noeSbu-~mH~-yO1dKrAy89#sSwbNH-9 zxm#SfwdL2T^}1YtuKWJ5*mMLIU(9m>L~l&w`7OyIJA#B+SuWYo;?GPu5TY=8@Us(e zxEXNf7%}}5+Vg)uX-!M)*9EzK|9B`-vpw%M@Gcuu@zA23B~VKLEvzWp<9044v(t|@ zj-S1i>y-eU7F@WA%9iHH9%&R*Vp}-Y&ZQjuo)T1?7#7mCj~HVHWjw;NN`mbU2j6{g zp{GSO&b0zm>1(^&FdsEXjfbEt)-LZ3r-80WsZ-kcopEYeqqCL9w&hmxBR-r==6Z6g zJX-^liI?xatka^~P(Re}F>0pv{#3ghR&J_LN{q`G-{NoN4+4}}alAA9_7HLQ!|uqn zlZjPNr_ZL`$P~xR{l%z_m960pqtfe@f3j*XQA4}Uo9!7&mWPIPm2yo=WRog2X(7zE zyCCRSP^KLmRkha32v*IY&@;tJloHK7@S|WYMQS5pCoo?wPyZFSBq}ke_?;;(SN87| z5c^FjIw_Z}#2-A+qY4|o8*_6f|81WB$4bUNrATKK-d{!A3hGGE)UcU_V;+M4GUQh@ z=8i1ld~rr5D216(#F(>T2CG5yPr#DFOJ0*50d3@iNtN7` z6|)gDEB2LW%VroarNxX~Pjreym>yHB0O8`AR;eN!P}e6J;`wuQOqFtSmG&Th0B^eLy0 z_PZX1y+!R(jE69~b1}Vt021;DlZAb5!UI(6g7-c}UYL1#c)4^6A^mC+%jCFpJUd#K zlBZPJij?7!<0<-G`J+)fE1hNC+#p1IIb$pss3L-@%Ji1b)(H&u}bVs#8`fpb=5=B9>pMUAas5gB=?@oHfc zYBvjG+q3ncM8BH#u;?KyM@VBqCM2xeA*>2kk6X-5!pHB6;40Pz*Fb?vY%OR`Cveo1 zn3_(UwzD#c7@u7Z@#z$GNR6>{Ki(1Lq-nVtO#VSijnAUFEO>L%)PZl`>Zti2lhr`r z2Y;x>H66i?aY!(F5_tQYNP}tjz?!;z)A7ogV3G$^hQ*y`Z7;eOO3inEjnhO1kwmyv zyR?!krQxc2^ZsU|N7`k776RSxZP9DNJF2WUro_}5O3wT7^b8)S*i-HKz-1lv`w6hb zKy+hUow>NYR}&fcVJgc{Q$qIx7fCeWlJ&w5WGA$olx7y^B1Weay%k}eQge`{9%lN? z7ifOus<({L{$rLK0 zw{x_Ynp{LHHJ(POXHks=Sd=`bJ!>>EXF^J26X zaUo;d0ELXe__i4y=u_RkZNZOA_g8|Yq?cGeaV)V#nV zNO(3;TDwNrlfJ()o-d?6#Gx@^IGEw0*5Z(+d$HgD%%_d|$f!|ctbaF~{Lr1n(Z_BM zg4@l*U3@+bmxA9oN2Fd$qM|I!oT|ehwO;U5CL~+8+m0RMjJxHf7bL@9kBVShCbym8 z!hX<~R3V%WT3sIUOY|0txmx#rSgEd1l`5h=A!_$RlTI#RIDwQ2#K;ZPn*HPRQj5!C z2^Fy_Br|rUy03}ijIehI(aTa+qhcqzXTR_$i=vI-YL5fC8(mrTJ4Xs3;UMR zihMm&1Wu?GU7Y`2lV;bc@Q~6R4Azmc6Y$TVdAk3;c83D+QWSk>%sAWI?$6h~SK3Hs ziCBrSrFoS%wJ&GQn$=M)GWHM`Yz1R`>d4hg3oNT_q!KNfl+cy5N>3(CikWUl`Ffr( z)1pQb!QCSedhdgi9%M}&aM#xa?6pS%&}yr~{?w!`YdA0MPz`|lC?wIW4Nk~vfN0Y( z!YT=ZCGgAU=f|Q(gGJv?XNp6+ga#VKd$gsI{4+WLNIZv7ArC5(oUuO& zAq9+8`+XhDf3(`pJRf^9>DlA3Y8p(MG(nsL3yX7U*-_L6`RvKHS3#e`#UnIfrLLO7 zED)txY1M8>+O2}bayg!cIzG<)^s`u$x)74D=6rr}V3XDDv(@iP?5Md{9V)&gkvVI` zjdz|wl2{VE%WCOrbm<~DEY2*vn;A^$1Wietj7(2-4LA~}>4%FC@LP~=X-R0`NFqYf zUpaWr1>hMLU_n=-&yTc8t83b`o{jj59hJYeC-47QB2(yJNhmz$3hnTeI}0_v6bBF> zmA|%=VDKeXkLh_$&8hNC{pGU6ehy1z3>=KF4(&@q)(lVMfzp^RJu~Hpi#q&zgU~6k zmplpFFb^^_DZ#DmQ@5m{FXj-p1v6aCEhj2KO#3COnZtz@U$hX$u8onN9jk6DMqaxWZ|_ z8ZgF24_9qfi<9GP3#S?avp``HS&r>1)6h`3EU3{>~ORBE~Mgy@rqUPq1qGZw!P7q%w zcK~L^-Td}cjM)1Dpl93e?JOTrL+G9tk)ts)g>7%;5c%) z)+!Bc=((umul_p@SMDI+0@z{QqP_ASSsAj=& zh<6vIHFnl>L+-b;{n)DQC#}1Xy^ts-ZLTs7js(q&hOhk#iieAd32SYo%|+{wc`lkz zCkgtj=IsC}H5kC>V+`xeeJVa!kanPaQNJi!s1yyuGYDbF5+S-N+2#Q=9e>$_<+J}tP^lz zG4-RV4xM+`uv#K3^<5{Qx$rkp-tV-_82s9nT|8DEaCU+*QUa#W#{U9XK&QX#*`{Q3 zY@fERsB)TdHN_kqFh}*6s^6VxjA<*j5Oz*N^jfr@tt3;J7x3KHOzNuzFdH+nGH66S zjAyJ(vF5{S;-zoud3REGOr6=Q&^{SOzhh0wL>9u?Ma-*YN(O>SN)D>A-A5Ra*)-eJ zkogJ@MvmP42Uu&o$uO?-CPU3%+9!!+&@`|-5#?mFX|Ra>1w34=dXupLuCA&8rDoJ~ znrUu?N6%ejWkdNML-o-I$DMlLYUL}tB&3f*!R`c;adnL`IbP0gG*;||K{V@3@RFjm%mqoxzZ$DY5-#9sStc*AJ2;VmPNEkq!wKB` z?jl>s&48#QitAJ;yhXlZbU{%pkTb5+4an|t_LQC?1r%{hgr{hJ&4Y;K*AtrY>k0ZA ze^g?Oa}~=_TNH072eyKAjif;~IfiC;#vxx?oe|QMYt@FTW~s7~a#ewpu~k&6;t@_i z*Kp?ZQ)E$B;Rd=ICeSF7uBMrEP!-R{^4z$ny_a9Ht)H7}T|nUZfO6(|R@-ae@7{|Sa3}7PoJR3nmk#Boq~zqJq~yY$b0Z?=&5MYbi~Gl|l0D@8vUMH5 zR4LrX-Bh{$Z6yunUx!v}kw$C`T!Rh)=jqzWn0>032XxWF+Ao<);*cZ`h3GVjZ6%ii z?xRK`9m~4NTa%fPiR`i0Ym6-xC8eZYlHWaTIvzM=pkd@8<_6grMSyv`bbewM@*V*~ zUrLV`PrbV1^h@k;fj7yMW*pxG7Y6A3%LD}=G-+_bKF{Lpvr6>p+ zGfAve&vE(nGk$#4GO%ts+NkEW)l0}296|~9K0032>=n6sg?2SM&JQjX12bt98m~A4 zOQmvs-PmSQ)1*^^riui(rzwh;RGVsZDN=?3$`me&N7!jnqylYG!}AA?H0*uk3h8^=B5!!sF5%r1|5n)M`L1`KyCzh z?NdS|*(C6_uEV*Z`ZA5$si>rEs+;pA=0$W&9Yl_yi;tf?ee~$*6UqoKrcrDh+8YC}T68-u2v;JJ6 zjKko)yOou2CKaaGQfx|grP&fg#r9uz?JPdKV`niO6+dA@eB8u|aoi}EV|yvCX9Z53 zsU`1oHjPUne>QIw>JO^XTF@enjZ8($z)Q3h=#M3PG7!($t7+tDL6yoGnR%o=by3c^ zTVN?qu(pu!cG{v&)aCzdrz7?hr5x@^8h#-;`9iYdC0J6OKh_|M*##16h@A0FqcX{A z#s}9%=A6S_8Dk-b|*3fdO32!&FCucnk991?)F1qHM{Z7O>y8WE|ia)JFN< zn1~j9wi$2hLU>W_Nnw^qnZvbgHMC&+utqQvSpbHeoJP8?Azh_bG+QbsW!%5)N*Y9C zU&7V~)hj5*8e=5XC0sX5Ddi%{yAXI1+eR?!3)9XSqnD%=qrb0>$foxyovJGqp5p-%Nft*CHB z&}eUq#?TSy$k|GCP1jga3SDq{h@;snYymHM3o!p+`~_h+Gjhpr$eV^5< z+ib>l5I4mz0M%ZS0LNe@*h{nhkda}uUL&DD!naoHH_a*;wo<8;rWm1448FZ;)$Ig! zdg`C3H}q$Z!E5aC3Sf2=N)?-^w{QrkL#@f&mw5HkXsL-4l zjf-q2JhR49qtt!1P^~F_q_@hP+)#hn3MxB+2`AUU7*NWuW7$JOv3HVYml<8So*e^C z&=i7~AFGyc&40;IOFm_#3hY9^jk!O&3ITxh6r`jB=2U`jO>m z7${<(tC2Qr`G!UmdSs?}1Mw`3H2_nd@VbxG3KLqvwr-@j8)6ECtN=+ znf&OtqYbs?2|9uPZk{us(l5R?7A)=sjz~#2GUuL`34KW{*9#h>t)f8uMAr2I5rUinf7a$<@Q)A+`@0zy^YA zKtH1S>rCs}du;#ngERj|TTw7{Jf6czDC%D{yj3gNauR{A# zBN)fdAV35T92kY808p#DNoT^Fm? zL+kp0`nxk;pkp#t;ulcPMq`0<9)ZE>tUr~K%+Taq7!4|Ea9go^NpoG;3dvc7?prCU z`Hv}dQ6!ccpq6Ta=P+#=kXpy_R6R{V2T(J(jGa!}vQy#urL08)IX5wpms|{v$iaEV zn+V+9Mla;35n`9}X-{Lad8$4#OgqpW`o!Px+K9Wj@tUW>AyN$5W1f3*|eyI_n1?1XMix zwy41q=v8Q5E_)I-VgH0%*jGuwbqz92qNadI$*}5e_Hd&dR9064gI4fMU@K>@q9QY9 z9PEoabD0}kjp{o|bK%=XVNKUsU-Cg5=MJf2FdA(EVads8-8ys?$yC;J9|UB8xr#LE z0h#^7!&u;`SO$jNz+>qKnsV-X)=KdV+S6>XhONGrs^73wzH-ozx(Z!Fo!(3Blgdt7 zO?i{F9O%b_F4Zy6<)xV}901xbU`tVkB@+exkxx7?HA?tSQ(1Wmv!VA=g`&Ln`kRL6 zX^H3o3WN*U*;sTGG)ZBTDLq_PrzoCN{*-m>k(g@65m+C`;)%jozAPr(sDhSdwYII=1G{fhAOs z;s|w9kx50N3MhRCp_Bouz!~%;r@+)QhmPBhexd`Mw9q{CgzT7d5K+oA7;r zdT#h$F@8)oo^6%00ZCLJaylq4T!36J(yS#&q-it}T~{iIQtPL&N<+2z3$uO=4Phx7 z>fu)bG&Dq(gF`8D6p}7S<>)G~m&=)4xcku~tfel8l)afI$P;R)I=zv(3Tp()H#_{i zQbVG`k^2j(tBAl8X`)ed1`EIDvH=Bhlf&Ad6{8? zOn!Y0Xcz$Z4J@hgTNd}T8%#a}TX)8wacL+lhA{#= z@$u+%<@>3zyk984zcH=TGmWpHHG0#v8gj~`(i50jC{NYCN5Lj18tgpi7J|y z;Ecr=LpYoC!I3UskYTD?H?Ex}HP)=~)Ovj~y3cMDSKE&mZ{74Zt4N5_CaaKZ3L z$I-b5FJ9ld_u?t+N70J8o0{uD2W9nJ(b!1wB5(Wl+*4b(o#GZ*C(PwPAjP9gsAsIo zztz;3v+}xD&Q^oTwB&)VlOIOM<}q8TEZ&&+wS502i)^ zIN=bP&&-Gb6+~1-MKUBrR3bv0Lq&pn&RP8T8Sdp?1nT#X&yi>E(_U-uz4qGQwe}ju z69?HM^}{jjBoTFZgb5d7NWHj$&m&VfQ7QmKM>hQ3$SrSLbmXOaF@sD(qC98?+G@o# z9hNbAA%y8Bh703K5PC`)tj_$RC5=0vjd9y}>JL_lEZ2+a#E)d=V92}g;_~AGjj+L8 zJZ9@D<_rM+Lv{vEVuxWj4)Yj4A)TBS5A&r@KG3YW*Keg8xFm60BHnP5^jf|7<*EB) z@BOCr4|t&0KQI^_7&ZYt;c7bbr{HHlSaf2X8^ehgyN?fG2Md8xu3Wv@*6ZFP*A|wR z<%*de&y*}8;}cBpIO#nd(-W9Dy^_XM2NB3IIXE7wg}!ggGbt30^2Zp0jo=_4gx5>* z(~lyfYL%+IKgwIar`7&IKV~s(OiV<&Kbd4AIw!GXNG7ulmyvG9-J}pdW!%I?!elZB zpOPXI!Hw#J8g@h_h+>=++)r}E0)9V~ z9$$;zMVtvEE1r3E{Z$=lLCG#4$>=~3y6W{gC(4wI`2LP^idQq2)}Bz$hV>QqdShe~AX^C8dqHeR`B>zu z#qgtrXlBKYUvcA%xAD`08Q=_?7=sl5O5)buB-_NF$WCD@$w4~SP@E=w1?wmb4aF_yIC@RjH9?rn52Y6v*4#%u}sU$lI3k=@jOq*)G?6>(j>$?Eru#%#}o@A z<}d?ETQJMK4k0qE16JZ$rnTVNKQvKjT zb-fOQB(9J zG$BD;M?zb0R1fx+C=izKMP{QXy~Q7xv6Q0W-P;}!Or~NBu9~Z-x>nY>)IxZT_zOpH z2$^RNd&ZN@Dk(}ySPKWRls`lkYs8-NA5_X8&@*aDwH=xkSgVDah>7e=YWNUETL+mE zsj!fQ-}TvogfQ3ZLXe+wbaei>#gS_uy&NzX8OItN*JgVhjMtA8Hyz^Oelp& zfscUl(lt2rveri4v0y8TL))I4Q-?lW3;JW}RhccjZKc=FEc~UW?&0q>59`=bIG7C+ zZ*uq}vrej3AcVpZEZ`sI@7|f0yK8SAnfU3#MXMqtpRFafbW+~DEwQDAWClnn`4nI3 zp^Dzv?oefoFgvsLvUqkSA=pbnqcy#bctXwuIZMJh22uHs%{AsK@cb;|gPmNQFTTjN`x*49+SMmd!_g>PbCVf>ki zSOJ5OgLJ_a4pQy34Qu#RD9+I0_yXCZ?Wg*dyIWe!ZBSHxKSu2Vn=0F_j02;Uv;r=J za=rk{Q4bkd#5b63U5g!OT6Vl;r@drqo>n_9?{>YuAt9kbk6odjl*v23j1A&bqL`hy z8v=1A7 z?||Wl;Z?8;!=L!8WRy;PO`)aO4F7ey$+JTckZmDDt|AW zrKe$Hfs6wDn*C*MXQDj3{aLoBhufU@YZR|Zk-bvn$(xvs#eY2f^G0o5JvGwy<9G0G zB{kGIn4Aw2fG)y*{!Y=}Jw@kt?>SF`*M=8uLsQ;izkEs#uvZqS1Ib1C47))S3Nf42m8ot6; z%qN{>OD2M=Bp0fx#Nm}3DZXL61vfb{-W*4i($UB*{`6KO^ArM3AYQsga;u?1?A53v z_wMl1r~;YZVr8Vr2+cy1D>tzzMoiBb=*$-|28@#9;u#&|f_K45xD8Q)D2NkCwoa!Y zawRL+zQwz3;c0I1K1;H^-@vR+P^^jsgK*Lgjr|f?egRj__H_7*zAJqkI)v74eHVpP0u_TpHN(t`PQdg7m;p#FOj>Yr{K zVdP*TlPH)%i-|lV)gU~%ibaroxzV&Y8QQtFc>*A)mNw`AURZr!EsfArh$f7O%O zY$(6ZK-XV7i;-{$FYvbuckeDN*t@rYOp96+67~6_MbuujvnimmzSjchx=VQ{mUvM5 zVd!6NE&jK9b800d%J0}vxp8AZyl!XX{cMp^!a zFHrqN5*OjExET8PTv%3oX`2@<`wD-8y}+Mj7qg+boT)=k4lgio<4N!peuOx@6KVMu zJ_Cvl>hSgB(k|H?qASCik)!;Q(;Ahw#g$#??u{FF$MLsf(qm%M(*<4Zk1;Vn{wM{Q zy=~vm6kx_Sh?T^H?@XrQ;3&(#NS>pWW(&=-QrXisgY9f253f|@)7i#V&2A&R@YXZK z@WJlTJJ2T)T<@4u+{8zb5E{D~b()|Za32V~f@cLBxS7hG)@-%)w#r#B(~v8Z*|=JU zkO&Qtc-Pw-8#jcMg4+h@cdAZyh>ynctiMLc;qi_hb&wIOG~gV_KB@`Q#Y)`yyJLADW6_Q&q=(0lpk zc=qkNvteCHW8(?@>OVpZnU2pzQ{AgITXC&3b6d~UIuq~lxe9mK&3a)S6G|m}7CS_e z?A>k5R~SU{#4%(vPLQbwnyJg3SK6iCalnkw5Jgu(=H?>wk+>2hhv$6|-+FZXq zYN-6Zp@DA3YwR>>Gt$Nb8=^9|s_4uanYqpv?aT(Vjr64H zZNY3IJxvDN5sO-N=IUhzLsg>aw)n;n&)Ud^*>~a}d<_p|+^mNze3R`fTS_@g#sK^A zV*KNsppPK9@Z7mfJm|+)kZGLgDcy|`%^_+HwkK`=W%J_S&~8^Lj;=~ftTM>l46Kb@ zkb85(!*9A#DD*%04+TOmz<=<6c>WeI-l%OxvJ9$m3fozZMN5g3_*;&(enE3!?um2f zvhaI9UMSkN>-_m$yNbx{nE4BIYZoqvR-yvb=$PIXQALlP%HjAb95;v$Q?cgh=kVUn z(0kA)0bK8qu5HLe@KET$JNa7m}iG14ZH^FA925Tn=2g8bUmoDXE%D)90rFala zX-=}BIna{oz_zG*SkwL-8%B=8rWV>m(3wOEDbk-rPoCF7ET6M*0@5Q|<0mtal_fF2 zSCZ+R=pp|>XP(>~E6YKvnS&$&);!3%!3{@4exi75L$miwc#m6j9^B*MnJlrJ{8qq> zUJE(KC>S>lT$Xc~`}W*7?#;p6@q8v+E#`1&nVdaQilKKL3^=BdUoR%gk#rNK*Nd75((A_X`sW6_t2-d5@)GNu??i@2$Q zjC??Cyd~jxbhYzK#3%3|OwnTyigP`xG-3O|F5&iW8OZsvBRI0Gu^eKWA#gn=M9f%0 z>>D8xHTQ8qj9jHN&c2^Y=}A+L)F4Y)u+~JTC@m)m1nF`9g6d}n{k|e3<5v9bePXF( zSTC~;GNyPDfrA)h1N3_ZAUG6n_|5$g=AA*RH-DHS1d$JM$XZH$gGGjx8Izeav<9}J z2CdSxPQlB148!Kn`rVFNm~$ik&i}+>)zZGk1~3jxk>CX-I`9*wzZU=KBQ(AnJQMs* z?b?-rXZ-m579X*+4k7jv%;Cf}e%+tA`D&UJ;UcgDwe4{uwpz z{To*m%m7!|WPoIFCXQ@AKRSwd=pc-*o;n(A<}b&ZNpojF@Y%{rd4c*q2R2Cw$Pd#D zf$96byuQWV|B_Hjl==1WN$Uu+$8;jQLF)7bsnk^EOlT#Hszn3zzQy#@!U!#B8pD|j z*t;#j;+bS)p&?9?VG&2yj+S6|Lzq;CC@~SE+ENrusir~$p2jGm!MqMsg=7y)H7|VS ziWfFuwbu4e+nDNIFgOVp-U+5xPJA*Xz<(B0<7{D+9DUDLZ^NI*g}(zE4+IB)i%X}3 zy#XVH=Q$`tI(tqYxIF(GR0&ykv7&dhR&aRzCf&VuN@GU&sG)tzWX5RFD z`c}k}hn_3l;JJ-s$8NyFK1fw@AH1#;VwI;rQ(jSagx>dD(FcZZ96x>|mh>?;kjR?o=o(QAMvPHjnHpk~mYR6M`UPzZRz)s zU@XQ|p|=BkrD7|~o7IG^*lB#)qBbs3zCo?Z*rKs4D7A}c+QTv{Z3B(FpGdiXxg*h9nI0?LVA4PL`St4m{PCmjkfU5 zP?9eC$>ok)(R|VzFa9_twg^s=rYhlKALKVIxB$KAT=VEQGFrV}h-9>+l4%e+yHG5= z8f&Hm&w-Wa^7APco+5aV25f-NVrK}hwnT=EeCD2QXTBW--~C?@q4Z|_kKv%4_QC&n9SR4ecgHYgw@giTeteX4eQ_jao(bN z$NTjB{=X~6w7f7MJlV7ozX%wU1|syYsKCqk{Ysb*pWUec4GVC0pnoXL)qRQYe(~zt zbcIUImA*0faImH^(Hh(_{kQrve{9mur>K1?#VEKAOA9aO{ZcH+i5F zAHfW2GDW)kbqREwduQKBIl%b|POC5gMTU$KsvbUZaR zKR-3~c$1E-#I78rTiCqBf`h>}!)<(R-n8Unpxow;ZN-Om!Kl{tqAY|+;dIbtybnFf zFM%W8yOf-rolHL#;tW8~QaSEV`0WSS_v6pM{P4psKmYj27n-mz%@=G2MlicEn8T>% z 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 j`A04N-}BnZ+0fD1-O + + + + + + 보고서_2026-06-29_스테이션바_UI개선 + + + + + +
+

보고서_2026-06-29_스테이션바_UI개선

+
+

작업 보고서 — 주행영상 플레이어 스테이션바/UI 개선

+
    +
  • 작성일: 2026-06-29
  • +
  • 대상: 스테이션(측점) 기반 주행영상 플레이어 (GhiVideo)
  • +
  • 범위: 하단 스테이션바 마커 정확도 개선, 영상 컨트롤 UI 재구성, 표시 일관성 정리
  • +
+
+

1. 개요

+

하단 스테이션바의 측점 마커 표시 정확도 문제 2건을 수정하고, 영상 화면의 표시 옵션 컨트롤을 재생바로 통합·정리하여 조작 동선을 단순화했습니다. 또한 활성 상태 색상을 통일해 UI 일관성을 높였습니다.

+
+

2. 버그 수정 (정확도)

+

2-1. 역(驛) 재진입 시 마커 누락 수정 ★

+
    +
  • 현상: 대전조차장처럼 드론이 같은 역을 두 번 지날 때, 한쪽 통과(예: 측점 162k080)에 역 마커(동그라미)가 표시되지 않음.
  • +
  • 원인: 역 마커를 '측점선 투영값' 한 가지 방식으로만 탐지 → 종점/조차장처럼 선형이 복잡한 구역에서는 투영 오차로 일부 통과가 인정 범위(±20m) 밖으로 벗어나 누락.
  • +
  • 조치: 측점 기준 탐지에 좌표 근접 탐지를 합집합으로 보강(중복은 자동 제거). 누락되던 재진입 통과까지 마커가 정상 표출.
  • +
  • 효과: 동일 역 다중 통과 구간의 마커 신뢰도 향상.
  • +
+

2-2. 측점값 라벨 겹침 정리

+
    +
  • 현상: 근접 통과 시 측점값(예: 157k990) 텍스트가 서로 겹쳐 판독 불가.
  • +
  • 조치: 라벨이 시각적으로 겹치면 먼저 나온(좌측) 값만 표시하고 뒤엣값은 숨김(아이콘·연결 점선은 유지).
  • +
  • 효과: 측점값 가독성 확보.
  • +
+
+

3. UI/UX 개선 (영상 컨트롤 재구성)

+

기존에 '화면표시 옵션' 창에 흩어져 있던 표시 토글을 하단 재생바로 통합하여, 영상을 보면서 바로 켜고 끌 수 있도록 동선을 개선했습니다.

+

3-1. 표시 토글을 재생바로 이동

+
    +
  • 선형(중심선), 드론 궤적, 좌측 노선 패널 토글을 재생바 버튼으로 이동.
  • +
  • 기존 '화면표시 옵션' 창의 중복 버튼은 제거(기능은 동일, 단일 위치로 일원화).
  • +
  • 상태를 설정 저장소로 통합 → 어디서 켜도 즉시 반영되고 새로고침해도 유지.
  • +
+

3-2. 항상 표시 항목 정리

+
    +
  • 드론 GPS·절대고도 정보: ON/OFF 버튼 제거하고 상시 표시로 변경(요청사항).
  • +
  • 경로 표고 / 투명도 슬라이더: 드론 궤적 ON/OFF와 무관하게 항상 노출.
  • +
+

3-3. 재생바 요소 배치 정리

+
    +
  • 최종 순서: 좌측 패널 → 선형 → 드론궤적 → 드론 위치정보(GPS·고도) → 시설등급
  • +
  • 드론 궤적 버튼과 드론 위치정보를 인접 배치, 시설등급 옵션을 그 우측에 배치.
  • +
  • 잘 쓰지 않는 'POI 겹침제외' 버튼은 숨김(기능/상태는 보존, 기본 ON).
  • +
+

3-4. 활성 상태 색상 통일

+
    +
  • 제각각이던 활성(ON) 색(파랑/초록/빨강/하늘색)을 앰버(겨자·오렌지 계열)로 통일 → UI 일관성 향상.
  • +
+
+

4. 운영 메모 (배포 방식)

+
    +
  • 사용자 화면(포트 55000)은 빌드 결과물을 서빙하는 구조로, 소스 수정 후 클라이언트 빌드 → 새로고침해야 반영됩니다. (이번 작업 중 빌드 누락으로 변경이 보이지 않던 이슈를 확인·정리)
  • +
  • 데이터(드론 프레임/POI)는 폴더 선택으로 로드되므로, 새로고침 시 폴더 재선택 필요.
  • +
+
+

5. 검증

+
    +
  • 모든 변경 TypeScript 타입검사 + 프로덕션 빌드 통과.
  • +
  • 역 재진입 마커 표출은 실제 화면(55000)에서 표시 확인 완료.
  • +
+
+

6. 향후 참고(선택 개선안)

+
    +
  • 측점값 라벨 폭 추정 상수는 폰트 변경 시 재조정 필요.
  • +
  • 활성 색조(앰버 명도)는 가독성에 따라 미세 조정 가능.
  • +
  • 'POI 겹침제외'는 현재 숨김 상태이므로, 재노출이 필요하면 토글 복원 가능.
  • +
+ + diff --git a/docs/보고서_2026-06-29_스테이션바_UI개선.md b/docs/보고서_2026-06-29_스테이션바_UI개선.md new file mode 100644 index 0000000..0b82257 --- /dev/null +++ b/docs/보고서_2026-06-29_스테이션바_UI개선.md @@ -0,0 +1,71 @@ +# 작업 보고서 — 주행영상 플레이어 스테이션바/UI 개선 + +- **작성일**: 2026-06-29 +- **대상**: 스테이션(측점) 기반 주행영상 플레이어 (GhiVideo) +- **범위**: 하단 스테이션바 마커 정확도 개선, 영상 컨트롤 UI 재구성, 표시 일관성 정리 + +--- + +## 1. 개요 + +하단 스테이션바의 **측점 마커 표시 정확도 문제 2건을 수정**하고, 영상 화면의 **표시 옵션 컨트롤을 재생바로 통합·정리**하여 조작 동선을 단순화했습니다. 또한 활성 상태 색상을 통일해 UI 일관성을 높였습니다. + +--- + +## 2. 버그 수정 (정확도) + +### 2-1. 역(驛) 재진입 시 마커 누락 수정 ★ +- **현상**: 대전조차장처럼 드론이 같은 역을 두 번 지날 때, 한쪽 통과(예: 측점 162k080)에 역 마커(동그라미)가 표시되지 않음. +- **원인**: 역 마커를 '측점선 투영값' 한 가지 방식으로만 탐지 → 종점/조차장처럼 선형이 복잡한 구역에서는 투영 오차로 일부 통과가 인정 범위(±20m) 밖으로 벗어나 누락. +- **조치**: 측점 기준 탐지에 **좌표 근접 탐지를 합집합으로 보강**(중복은 자동 제거). 누락되던 재진입 통과까지 마커가 정상 표출. +- **효과**: 동일 역 다중 통과 구간의 마커 신뢰도 향상. + +### 2-2. 측점값 라벨 겹침 정리 +- **현상**: 근접 통과 시 측점값(예: 157k990) 텍스트가 서로 겹쳐 판독 불가. +- **조치**: 라벨이 시각적으로 겹치면 **먼저 나온(좌측) 값만 표시하고 뒤엣값은 숨김**(아이콘·연결 점선은 유지). +- **효과**: 측점값 가독성 확보. + +--- + +## 3. UI/UX 개선 (영상 컨트롤 재구성) + +기존에 '화면표시 옵션' 창에 흩어져 있던 표시 토글을 **하단 재생바로 통합**하여, 영상을 보면서 바로 켜고 끌 수 있도록 동선을 개선했습니다. + +### 3-1. 표시 토글을 재생바로 이동 +- **선형(중심선)**, **드론 궤적**, **좌측 노선 패널** 토글을 재생바 버튼으로 이동. +- 기존 '화면표시 옵션' 창의 중복 버튼은 제거(기능은 동일, 단일 위치로 일원화). +- 상태를 설정 저장소로 통합 → 어디서 켜도 즉시 반영되고 **새로고침해도 유지**. + +### 3-2. 항상 표시 항목 정리 +- **드론 GPS·절대고도 정보**: ON/OFF 버튼 제거하고 **상시 표시**로 변경(요청사항). +- **경로 표고 / 투명도** 슬라이더: 드론 궤적 ON/OFF와 무관하게 **항상 노출**. + +### 3-3. 재생바 요소 배치 정리 +- 최종 순서: **좌측 패널 → 선형 → 드론궤적 → 드론 위치정보(GPS·고도) → 시설등급** +- 드론 궤적 버튼과 드론 위치정보를 인접 배치, 시설등급 옵션을 그 우측에 배치. +- 잘 쓰지 않는 'POI 겹침제외' 버튼은 숨김(기능/상태는 보존, 기본 ON). + +### 3-4. 활성 상태 색상 통일 +- 제각각이던 활성(ON) 색(파랑/초록/빨강/하늘색)을 **앰버(겨자·오렌지 계열)로 통일** → UI 일관성 향상. + +--- + +## 4. 운영 메모 (배포 방식) + +- 사용자 화면(포트 55000)은 **빌드 결과물을 서빙**하는 구조로, 소스 수정 후 **클라이언트 빌드 → 새로고침**해야 반영됩니다. (이번 작업 중 빌드 누락으로 변경이 보이지 않던 이슈를 확인·정리) +- 데이터(드론 프레임/POI)는 폴더 선택으로 로드되므로, 새로고침 시 **폴더 재선택** 필요. + +--- + +## 5. 검증 + +- 모든 변경 TypeScript 타입검사 + 프로덕션 빌드 통과. +- 역 재진입 마커 표출은 실제 화면(55000)에서 표시 확인 완료. + +--- + +## 6. 향후 참고(선택 개선안) + +- 측점값 라벨 폭 추정 상수는 폰트 변경 시 재조정 필요. +- 활성 색조(앰버 명도)는 가독성에 따라 미세 조정 가능. +- 'POI 겹침제외'는 현재 숨김 상태이므로, 재노출이 필요하면 토글 복원 가능. diff --git a/docs/보고서_2026-06-29_스테이션바_UI개선.pdf b/docs/보고서_2026-06-29_스테이션바_UI개선.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8577bbd9376fec0b1e5781826edbdd9d0d7a6692 GIT binary patch literal 51416 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­ z*}6VopMiRxDE$1TujBKUxJCRw$dz-dhC(~|(;X7t(`lOaF!X!HdEQQZ_O0Q2!GEEv zCyWfo8CQuO)pAysNv%N%+t;z=(mAt|(F~ZPRn=2jPRolb)R40P#`&f506FT)aJh}N zm`6*IW)C@~;EGBdQRLvnJ&7K0?!99z1oS#6<7a=S~LM@9sD@tbi zYwe~NGAK7OYF9D54=#haaG-M_xd;c<@&#SENpw`5eURv0h=1+lBQks4{2u$`AvPWh z>-Z&z{5nfAB_W^&dPcC-sen&D$m%|8x&#d zG1KX=SZn#p;6;cJi%bx4i1TCZMPjQ2xB8o!j*S}1y^Eci_AD}IZSp||P;2rvdQu)b zFA~rLO2EYkw@&1a8s|xON)#!Y(J+7J7_QNdUL5;RUCxOoqxJ$Bm0m8@4TaZ0n-3({Lm==uhf%I&%4G0HDLUw|!eAb+qMhCYW5x2B(&^t8ejSwS`D*(6i^ z#yh__c;=dT7%-8zi5zU>gUDQFRrzol5Zm+hfYlijWDxt~Y3IM2HQ0g!E4MTjN;m(n zMmv6MiSV{hJK1!mhnz2z&YT5lRi*qh>`?Iz;81sCde@Nk`f}5IG_%$ImUv40lR*0D zq|DFj07CGeR*gErg6=kvLyl}y%5|V$LJ-*6e7|z%n~k>*HXXbV?_BB~h4-F2G~)3T%ooMWWMmApYUBcjq@hr}k!^21*`fL+M` zIb0+qz&kM13ryA1N3H`=_5umH?*TXRgg@bJL+2G>y3s=T}bX?v1pg9Mu3mEspoPj*Gk;MeY+P>!fR#x zXV-H1DNi$N(Xr|>I=JaB6F27$KcOH04&KEjLvey*xJhE_7Gm>9VR-U1k_NsT^!WQ4 z5YYW;a=;Zsd%t-9%AozCS;0X~Y8-h+GEF?_Z|m*4)Gh}qi(BSf+$-jF#@6fo^4S{l z!-CEZ<=-4)2cTN=8>kz)o*C8fWfO3)EL+ZQL3@kA^f*wz%`GH}(1RS)O&qmLpHL^( z{F>aZ$^pb{WwYl5^uArldSN-5Z~`YVDT@kGb_=6EQV$&)!~WMBa8UpOFNlMj8Wg1K z=(%lG#^_;a%irxCB6uyo7vT`)s9;}O8(u!u5gpGPfhRN^9qF9k#@nkv6pm{~30s}rUB)8L zeFm7z-?sCpfmZF@0~v6e1dFY+21MOIIN=07%C765Ce%mU*VU3}rW2)7Gtyk@?{sS~ z9$FuthZnYOnL9Y#2HN%w4s~zEaOM)!BB+e`^&zlp+=RMCck5+74z?v16z!T;AOyD*QRdf$C2DDrQKe zdtsIQfp+wlfqD)~GXbwTk19@OxQ^{GA|6rqLbDKKdV|>>f?-f6JLCpJuW^|{{q*WC zA%Acu+ z5&x`Z-Z_KURve(altQT>?rQqK_I;91U4L3rS$3ZzsQkE0_9&LSEPwW&zO@Ir(0bU{ z2~I4*jg^jUBvXj*d}8c}cDLL>C0>3Dd*H4iP}*ux-;2F{A21igHEGD5ZX3=u1bcwX z5~LAF@#-&5qa)@%6)I-2VCT;l2(TZy1BzMMCD?VYi1WU#%zD6*QDc6WR>d7Toh|Yv z_H`HF&^boGq{`y357GrODAlo#Id0B$Nfz2d28&|F{24Jcb<)o3R${0)t{4MZ$FOfAq5k{Z1HGl-$7rnrH2>DD?qKxB))F+AcHCwJkLlsl(G<=eTU z=l)%f_WHMRv*8(=bwm4vih5I1cx@dh1?=?+Kc&ruzwci>~%K0>p?bI@4&D4Uw4;@gKYVP4V{7ki^Jc2>5;HF;@ZE zCfTG3#NZB2n-xla$q}+Q=GE2h8IQRc5BK07?lU0KcdtiXm*KiB-0VH3GG@&Oa{P7~ z+8iMZqfXogMXWo4b%RSu(3fXvGrYUbexXS4kFqrNJDYT~sH=w-Z}VTSk7d4?ax&Lt z3B_Gw0_Z>f{rX)B8Rm$0)kv-4&Fg0XGNF)8vSaEvzy0dI{ tm78hU@VDPq~PmwsPPB$8r#@6NpB&`k5Rlh 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 literal 0 HcmV?d00001 diff --git a/docs/특허_스테이션기반_주행영상_플레이어.html b/docs/특허_스테이션기반_주행영상_플레이어.html index 1856cec..2bd3325 100644 --- a/docs/특허_스테이션기반_주행영상_플레이어.html +++ b/docs/특허_스테이션기반_주행영상_플레이어.html @@ -58,7 +58,7 @@ td code,p code,li code{background:#0c0f14;border:1px solid var(--line);border-radius:5px;padding:1px 6px;font-size:12.5px;color:#9ad0ff;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace} .badge{display:inline-block;font-size:11.5px;font-weight:700;padding:2px 9px;border-radius:999px;border:1px solid transparent} - .b-high{background:rgba(126,231,135,.13);color:var(--good);border-color:rgba(126,231,135,.35)} + .b-high{background:rgba(36, 70, 39, 0.13);color:var(--good);border-color:rgba(126,231,135,.35)} .b-mid{background:rgba(255,204,102,.13);color:var(--mid);border-color:rgba(255,204,102,.35)} .b-low{background:rgba(255,123,114,.13);color:var(--bad);border-color:rgba(255,123,114,.35)} .chk{color:var(--warn);font-weight:600} diff --git a/scripts/md2docs.sh b/scripts/md2docs.sh new file mode 100755 index 0000000..5c13fea --- /dev/null +++ b/scripts/md2docs.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# md2docs.sh — 마크다운(.md) 보고서를 베이스로 HTML + PDF 를 생성한다. +# 결과물: <입력>.html (자체완결, CSS 인라인), <입력>.pdf (A4, 한글 임베드) +# +# 사용법: scripts/md2docs.sh docs/보고서_xxxx.md +# +# 의존성(이미 환경에 설치됨): +# - pandoc : md -> html (--self-contained 로 CSS 인라인) +# - weasyprint : html -> pdf (pydyf 0.10 호환, py3.8 anaconda) +# - 한글 폰트 : ~/.local/share/fonts 의 Malgun Gothic (Windows 폰트 복사) +# 최초 1회 환경구성이 필요하면 scripts/README 또는 메모(report-doc-toolchain) 참고. +set -euo pipefail + +MD="${1:-}" +[ -z "$MD" ] && { echo "사용법: $0 <파일.md>"; exit 1; } +[ -f "$MD" ] || { echo "파일 없음: $MD"; exit 1; } + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CSS="$ROOT/docs/assets/report.css" +BASE="${MD%.md}" +HTML="$BASE.html" +PDF="$BASE.pdf" +TITLE="$(basename "$BASE")" + +export PATH="$HOME/.local/bin:$PATH" + +echo "[1/2] HTML 생성: $HTML" +pandoc "$MD" -f gfm -t html5 -s --self-contained -c "$CSS" \ + --metadata title="$TITLE" -o "$HTML" + +echo "[2/2] PDF 생성: $PDF" +python3 -m weasyprint "$HTML" "$PDF" + +echo "완료:" +ls -la "$MD" "$HTML" "$PDF"