diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index ddc71d5..77d9e85 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -1,4 +1,4 @@ -name: Claude Code Review +name: Manual Review Checklist on: pull_request: @@ -6,48 +6,22 @@ on: permissions: contents: read - pull-requests: write jobs: - claude-review: + review-checklist: runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get PR diff - id: diff + # The previous third-party action no longer exists. This job records the + # required human gate; a green checklist job does not certify code review. + - name: Record required release review run: | - git fetch origin ${{ github.base_ref }} - DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' | head -c 50000) - echo "diff<> $GITHUB_OUTPUT - echo "$DIFF" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Claude Code Review - uses: anthropics/anthropic-cookbook/.github/actions/claude-code-review@main - if: ${{ steps.diff.outputs.diff != '' }} - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - github_token: ${{ secrets.GITHUB_TOKEN }} - model: claude-sonnet-4-20250514 - max_tokens: 4096 - continue-on-error: true + cat >> "$GITHUB_STEP_SUMMARY" <<'CHECKLIST' + ## Human review required + This job publishes a checklist only. It does not perform or approve code review. - - name: Fallback - Basic Review Comment - if: failure() - uses: actions/github-script@v7 - with: - script: | - const diff = `${{ steps.diff.outputs.diff }}`.slice(0, 1000); - if (diff) { - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `## 🔍 Code Review Pending\n\nClaude code review action encountered an issue. Please ensure:\n- \`ANTHROPIC_API_KEY\` secret is configured in repository settings\n- The PR contains reviewable code changes\n\n
\nFiles changed\n\n\`\`\`\n${diff}\n\`\`\`\n
` - }); - } + - Review changed behavior and failure/recovery tests. + - Check CI and the candidate commit used for native verification. + - Exercise affected runner/crew journeys on the Release iOS build. + - For release work, complete docs/release/V1-RELEASE-PACKAGE.md gates. + - Obtain owner approval before publishing or changing production services. + CHECKLIST diff --git a/App.tsx b/App.tsx index 88b1711..2b2cdae 100644 --- a/App.tsx +++ b/App.tsx @@ -11,21 +11,18 @@ import { View, ActivityIndicator, StyleSheet } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import * as Sentry from '@sentry/react-native'; // Theme & Fonts import { ThemeProvider, useTheme } from './src/theme'; import { useFonts } from './src/hooks/useFonts'; // Data Providers -import { AuthProvider } from './src/context/AuthContext'; import { MoverProvider } from './src/context/MoverContext'; import { EventProvider } from './src/context/EventContext'; import { CheckpointProvider } from './src/context/CheckpointContext'; import { GearProvider } from './src/context/GearContext'; import { CrewProvider } from './src/context/CrewContext'; import { DropBagProvider } from './src/context/DropBagContext'; -import { SubscriptionProvider } from './src/context/SubscriptionContext'; // Navigation import AppNavigator from './src/navigation/AppNavigator'; @@ -62,16 +59,6 @@ function AppContent() { ); } -const SENTRY_DSN = process.env.EXPO_PUBLIC_SENTRY_DSN; - -if (SENTRY_DSN) { - Sentry.init({ - dsn: SENTRY_DSN, - enabled: !__DEV__, - tracesSampleRate: 0.2, - }); -} - // Root component with all providers function App() { return ( @@ -79,23 +66,19 @@ function App() { - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -103,7 +86,7 @@ function App() { ); } -export default SENTRY_DSN ? Sentry.wrap(App) : App; +export default App; const styles = StyleSheet.create({ root: { diff --git a/README.md b/README.md index c09bbd0..3e48dea 100644 --- a/README.md +++ b/README.md @@ -1,145 +1,44 @@ # UltraEdge - +UltraEdge is an open-source race preparation app for ultramarathon runners and crews. The V1 release scope is a **free planner on one device**, with PDF sharing for crew handoffs. Release readiness and device verification are tracked in [the release package](docs/release/V1-RELEASE-PACKAGE.md). -**UltraEdge** is an open-source mobile application designed specifically for ultra marathon runners and their support crews. Plan your races, manage aid stations, coordinate your crew, and optimize your performance - all in one comprehensive app. +## Plan your event -> Created by an ultra runner, for ultra runners. +- Create events and checkpoints with distances, cutoffs, access details and notes. +- Organize gear and weights, food and hydration supplies, and drop bags. +- Keep crew contacts and assign roles for each event. +- Import GPX routes and inspect the route and elevation profile. +- Export an event's race plan as a PDF to share with your crew. -## Recent Updates +Editable planning data lives on the device. Export a PDF before travel and keep a copy outside the app. PDFs are readable copies, not restorable backups. Deleting the app can remove local plans. Map backgrounds may require a connection; download/import your route before heading out. -- **2025-04-08**: Fixed profile data backup issue by adding missing columns to the Supabase database schema +Cloud sync, automatic backups, crew invitations/notifications, live tracking/predictions, and paid subscriptions are outside V1. Nutrition and hydration are packing/notes tools, not a timed fueling prescription. Supabase and RevenueCat integration code remains in the repository for future work; its presence does not mean those services are enabled or verified for release. -## Latest Features +## Development -### Race Planning & Management -- **Create detailed race plans** for ultra marathons and endurance events -- **Customize distance units** (miles/kilometers) and elevation units (feet/meters) -- **Track key race metrics** including distance, elevation gain, and cut-off times -- **Edit and update race plans** as your training and strategy evolve +The app uses Expo SDK 54, React Native 0.81.5, React 19 and TypeScript. Use Node.js 20 as in CI, npm, and Xcode for local iOS builds. -### Aid Station Management -- **Configure comprehensive aid station details**: - - Precise location and distance markers - - Cut-off times and pacing calculations - - Available supplies and nutrition options - - Medical support availability - - Required equipment checks -- **Edit existing aid stations** to refine your race strategy - -### Crew Coordination -- **Manage your support crew** with detailed profiles and contact information -- **Assign crew members to specific aid stations** for seamless race-day coordination -- **Add crew instructions** with meeting points, driving directions, and responsibilities -- **Optimize crew logistics** to maximize their effectiveness during your event - -### User Experience -- **Personalized user profiles** with customizable preferences -- **Dark mode support** for comfortable viewing in any lighting condition -- **Intuitive navigation** designed for quick access during race planning and execution -- **Responsive design** that works across different device sizes - -### Premium Features -- **Cloud synchronization** of all race data across devices -- **Automatic backup** to secure cloud storage after every edit -- **Restore race data** from cloud backups -- **Advanced analytics** and performance insights -- **Unlimited race plans** and aid station configurations -- **Priority support** and early access to new features - -## Screenshots - -*Note: Screenshots will be added as the app development progresses.* - - - -The app includes screens for: -- Home dashboard with upcoming races -- Race creation and editing -- Aid station setup and management -- Crew coordination and assignment -- User profile customization -- Premium subscription management - -## Technical Details - -- Built with **React Native** for cross-platform compatibility (iOS and Android) -- Implements **React Navigation** for seamless app navigation -- Uses **React Native Paper** for beautiful, consistent UI components -- Stores data locally using **AsyncStorage** for offline functionality -- Integrates with **Supabase** for secure cloud storage and authentication -- Implements **RevenueCat** for subscription management and in-app purchases - -## Getting Started - -### Prerequisites - -- Node.js (v14 or newer) -- npm or yarn -- Expo CLI - -### Installation - -1. Clone the repository -```bash +```sh git clone https://github.com/beaux-riel/UltraEdge.git cd UltraEdge +npm ci +npm start ``` -2. Install dependencies -```bash -npm install -# or -yarn install -``` - -3. Configure Supabase (for premium features) -```bash -# See instructions in /supabase/README.md +```sh +npm run typecheck +npm test -- --runInBand +npm run lint ``` -4. Configure RevenueCat (for subscription management) -```bash -# Update API keys in src/context/RevenueCatContext.js -``` +For the native iOS development build, use `npm run ios` after setting up Xcode. Release work uses the committed iOS project and [Fastlane](fastlane/README.md). Read the lane before running it: even a build can install pods and change signing settings. The beta lane uploads externally and requires release-owner approval. EAS is not the verified release route. -5. Start the development server -```bash -npx expo start -``` +Never commit credentials or copy production secrets into test fixtures. Backend migrations and billing setup require a separate validated deployment plan; do not apply legacy migrations to production as a development setup step. ## Contributing -UltraEdge is an open-source project, and we welcome contributions from the community! Whether you're an ultra runner with ideas for new features, a developer looking to contribute code, or someone who wants to improve documentation, we'd love your help. - -### Ways to Contribute - -- **Feature Development**: Help build new features or enhance existing ones -- **Bug Fixes**: Identify and fix issues in the codebase -- **Documentation**: Improve README, add code comments, or create user guides -- **Testing**: Test the app in different environments and report issues -- **Design**: Suggest UI/UX improvements or create assets - -### Getting Started with Contributing - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. - -## Contact +Prefer small changes tied to an athlete journey. Test failure and restart behavior for persistence changes, and verify critical journeys in release-mode iOS: JavaScript checks alone do not certify device behavior. Report what you exercised, the build/device, and what remains unverified. Preserve existing user data. -Beaux - [GitHub](https://github.com/beaux-riel) +## License and contact -Project Link: [https://github.com/beaux-riel/UltraEdge](https://github.com/beaux-riel/UltraEdge) +MIT; see [LICENSE](LICENSE). Maintained by [Beaux](https://github.com/beaux-riel). Report reproducible issues in [GitHub Issues](https://github.com/beaux-riel/UltraEdge/issues). Include app version and reproduction steps; do not post private routes, contacts or credentials. diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png index 0314e0c..f1ef53b 100644 Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png index c414dda..f1ef53b 100644 Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png index 685187b..f1ef53b 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png index ec24653..f1ef53b 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png index c87dfb1..f1ef53b 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index f387b90..c7d27a5 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ - #ffffff - #ffffff + #171C20 + #171C20 #023c69 - #ffffff + #171C20 \ No newline at end of file diff --git a/app.json b/app.json index 278518f..ce4a510 100644 --- a/app.json +++ b/app.json @@ -9,22 +9,21 @@ "userInterfaceStyle": "automatic", "newArchEnabled": true, "splash": { - "image": "./assets/splash.png", + "image": "./assets/icon.png", "resizeMode": "contain", - "backgroundColor": "#ffffff" + "backgroundColor": "#171C20" }, "assetBundlePatterns": [ "**/*" ], "ios": { "supportsTablet": true, - "bundleIdentifier": "com.beaux.ultraedge", - "usesAppleSignIn": true + "bundleIdentifier": "com.beaux.ultraedge" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" + "backgroundColor": "#171C20" }, "package": "com.beaux.ultraedge" }, @@ -32,9 +31,7 @@ "favicon": "./assets/favicon.png" }, "plugins": [ - "expo-apple-authentication", "expo-secure-store", - "@sentry/react-native", "expo-font", "expo-web-browser" ], diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png index a611f57..91d5426 100644 Binary files a/assets/adaptive-icon.png and b/assets/adaptive-icon.png differ diff --git a/assets/alpine-backdrop.md b/assets/alpine-backdrop.md new file mode 100644 index 0000000..bec633e --- /dev/null +++ b/assets/alpine-backdrop.md @@ -0,0 +1,3 @@ +# Alpine backdrop + +Generated with OpenAI image generation on September 5, 2026 for UltraEdge's approved field-guide design. Fictional alpine landscape, decorative only; not a photograph or map of Fat Dog. Bundled locally so the home screen requires no image network request. Source PNG retains content provenance metadata. diff --git a/assets/alpine-backdrop.png b/assets/alpine-backdrop.png new file mode 100644 index 0000000..d69c1c9 Binary files /dev/null and b/assets/alpine-backdrop.png differ diff --git a/assets/favicon.png b/assets/favicon.png index 8a9aaa4..2f1ae93 100644 Binary files a/assets/favicon.png and b/assets/favicon.png differ diff --git a/assets/icon.png b/assets/icon.png index 8a9aaa4..f1ef53b 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/splash.png b/assets/splash.png index a611f57..f1ef53b 100644 Binary files a/assets/splash.png and b/assets/splash.png differ diff --git a/docs/design/app-icon.md b/docs/design/app-icon.md new file mode 100644 index 0000000..ae196c6 --- /dev/null +++ b/docs/design/app-icon.md @@ -0,0 +1,9 @@ +# App icon + +The field-guide identity now uses a trail-lime alpine silhouette with a negative-space ascending trail on graphite. Generated using the built-in imagegen tool, then resized with macOS sips. + +Production assets: `assets/icon.png` and the matching native `ios/UltraEdge/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png`; both are opaque 1024×1024 PNGs. Android's adaptive foreground has extra safe-area padding with a graphite background configured in app.json. The web favicon uses the same mark at 48×48. + +Generation prompt: Create one production app icon for UltraEdge, a premium ultramarathon race-planning app. Square full bleed opaque cool graphite #171C20 background. Center a bold geometric alpine ridge / ascending trail emblem in trail lime #C3E76B. Two sharp mountain summits with an angular ascending negative-space trail cut through the lower form as one cohesive compact symbol. Premium outdoor equipment brand, disciplined Swiss graphic design, crisp silhouette with generous clear space for OS masks. No text, letters, border, rounded outer corners, shadows, glow, gradients, texture, mockup, or scenery. + +Validation: Apple's actool successfully compiled the native asset catalog for iPhone and iPad Simulator. Native and Expo icon files are byte-identical; dimensions and lack of alpha verified. The 60×60 rendering was visually inspected. Android configuration updated, but no Android runtime build was tested. No app logic changed; no new TestFlight upload in this change. diff --git a/docs/design/field-guide.md b/docs/design/field-guide.md new file mode 100644 index 0000000..cf6cccf --- /dev/null +++ b/docs/design/field-guide.md @@ -0,0 +1,23 @@ +# Field guide redesign + +Approved by Beaux on September 5, 2026 after review of the Home / Race Plan concept. + +The shared palette now uses cool graphite, off-white and trail lime, with native system typography and bundled monospaced numerical figures. Primary buttons use dark text on lime in both themes. Selected form controls, list actions and headers were adjusted to retain contrast. Existing theme preferences remain respected. + +Home emphasizes the nearest future active event, then an undated active plan, then another saved event. Past dates are explicitly labeled. Preparation rows link to the real course, checkpoints and event crew assignment. The hero mountain is a bundled generated illustration, not race geography. Names with an em dash retain their qualifier as subtitle text. + +Race detail now places real GPX course data immediately after rounded presentation-only distance/ascent metrics. Stored precision is unchanged. Preview distance markers are hidden to keep the course visible; full-screen markers remain configurable. GPX measurements remain labeled separately from event figures. Metric cells reflow at accessibility text sizes; display headings adapt and body text continues scaling. + +## Verification + +- TypeScript passes. +- Jest: 122 passed, three existing todo, 20 suites. +- ESLint: zero errors; 1,445 warnings remain in the repository. +- Release-mode iOS Simulator build succeeds. +- iPhone 17 Pro dark: Home, Fat Dog race overview, GPX map preview and native PDF share sheet exercised. +- iPhone SE light/accessibility-large: home, historical race title and reflowing metrics exercised. Final home subtitle refinement additionally checked at standard size in dark mode. +- Real historical Fat Dog fixture retained, without inventing confirmed 2027 details. SE fixture seeder backed up and preserved unrelated data. + +The first automation run used an exact PDF button selector that failed; the existing icon contributes to the accessibility label. The wildcard selector completed the export successfully. The local Watchman daemon stalled initial checks; use `--watchman=false` for Jest and `ULTRAEDGE_DISABLE_WATCHMAN=1` for native release builds. + +This is a local redesign candidate. No new TestFlight upload or store/production changes are included. This is focused visual smoke coverage, not renewed verification of every V1 journey or a complete VoiceOver audit. Existing production-release limitations remain. diff --git a/docs/release/BUILD-4.md b/docs/release/BUILD-4.md new file mode 100644 index 0000000..3acbf6f --- /dev/null +++ b/docs/release/BUILD-4.md @@ -0,0 +1,9 @@ +# UltraEdge 1.0.0 build 4 + +Packages the approved graphite / trail-lime field-guide redesign and matching alpine app icon for internal TestFlight testing. + +Build number incremented from 3 to 4. App Store Connect reported builds 1–3 as VALID and IN_BETA_TESTING, each with usesNonExemptEncryption=false, immediately before this release. The native Info.plist now carries that existing declaration forward; this visual update does not change encryption behavior. + +Validation: 122 Jest tests passed (three existing todo), TypeScript passed, signed device archive and App Store export succeeded. codesign --verify --deep --strict passed. Archive metadata confirms build 4 and the declaration; the actual archived 120px icon was decoded and visually inspected. The redesign's release Simulator and native PDF verification are documented in docs/design/field-guide.md. + +Artifacts, upload and Apple processing receipt: ../reports/UltraEdge-build4 in the Astra workspace. Distribution remains internal; no external beta review or production release is requested. The historical Fat Dog rehearsal remains a separate fixture, not preloaded user data. diff --git a/docs/release/BUILD-VERIFICATION.md b/docs/release/BUILD-VERIFICATION.md new file mode 100644 index 0000000..869446b --- /dev/null +++ b/docs/release/BUILD-VERIFICATION.md @@ -0,0 +1,25 @@ +# Reproduce local release verification + +Use the clean release checkout, Node 22 and Xcode 26.6 (the versions used for this run). Keep secrets out of command arguments and logs. The committed iOS project is authoritative; do not regenerate it with Expo prebuild. + +```sh +npm ci +npx jest --runInBand --watchman=false +npm run typecheck +npx eslint . --ext .ts,.tsx,.js,.jsx --quiet +cd ios +pod install --deployment +cd .. +ULTRAEDGE_DISABLE_WATCHMAN=1 SENTRY_DISABLE_AUTO_UPLOAD=true xcodebuild \ + -workspace ios/UltraEdge.xcworkspace -scheme UltraEdge \ + -configuration Release -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,id=YOUR_SIMULATOR_UDID' \ + -derivedDataPath /tmp/ultraedge-release-derived \ + CODE_SIGNING_ALLOWED=NO ONLY_ACTIVE_ARCH=YES build +``` + +The Watchman overrides avoid an observed local daemon hang without resetting a shared daemon or changing other projects. The environment flag changes Metro file discovery only, not application behavior. A successful simulator build is not a signed archive, physical-device test, TestFlight upload or App Store approval. + +Install the resulting `Build/Products/Release-iphonesimulator/UltraEdge.app` with `xcrun simctl install`. Use a disposable simulator; preserve any existing user test data. Run the smoke matrix and record the exact commit and artifact checksum. The fictional fixture instructions are in `fixtures/README.md`; seeding is not proof that creation forms work. + +The optional broken third-party Claude workflow has been replaced by an explicitly manual review checklist. A passing checklist job does not represent an automated code review. Local checks have not been run by remote CI until these commits are pushed at the approved gate. diff --git a/docs/release/BUILD7-FEEDBACK.md b/docs/release/BUILD7-FEEDBACK.md new file mode 100644 index 0000000..2234bf1 --- /dev/null +++ b/docs/release/BUILD7-FEEDBACK.md @@ -0,0 +1,27 @@ +# Build 7 athlete feedback + +Implemented September 6, 2026 on `design/field-guide`. + +## Behavior + +- Event start and elapsed-duration fields use separate numeric hours/minutes controls. Clock hours are 0–23; durations support multi-day races. Event submission rejects malformed or zero durations. Checkpoint cutoffs and local/live actual reports use date and time pickers, retain the calendar date, and pass through existing chronology validation. Times are explicitly in the device timezone. +- Race operations opens to **Race planning** from **Plan aid-station stops**. All stations expose their planned minutes, presets and Save action. This projection excludes actual reports and actual start corrections. The **Race day** entry point has the timer and actual reports. A finish-goal calculator shows required moving speed after subtracting planned stops; it does not silently change the saved moving-time target. +- Each race can have named mandatory requirements with quantities and linked race gear. Packed status counts active assigned gear worn/carried by the athlete. Removing/retiring gear makes the requirement incomplete; it does not remove the requirement. Rules are user-entered from the official guide, not a bundled assertion of any event’s current regulations. Printed plans include the requirement checklist. +- Gear creation/editing and drop-bag creation/editing accept photos from the photo library. Gear and bag details display them. Save is blocked while a form photo is copying. Photos are copied into app documents and store relative references, so transient picker files and app sandbox relocation do not break them. +- A race can attach/replace/remove an official PDF, keep it offline, and open/share it through the native share sheet. The file is validated and copied before the event reference is saved. The exported planning PDF names the guide and reminds users to share its original separately. +- Brand selection includes common brands, searchable selection, Other, and custom brands from existing inventory. Unlisted existing brands remain editable. + +## Compatibility and limits + +The existing moving-time interpretation is preserved: planned stops extend elapsed finish time. Existing plans require no destructive migration. Optional event/bag fields are absent on old records. Previously entered invalid prose must be corrected using structured controls when saving that time field. + +Attachments are device-local, not uploaded to live team rooms. Crew receive the original PDF through the explicit share action. Removing an attachment detaches it from the record; unused copied files may remain in app documents until app deletion. Photos of reusable bags belong to the individual bag, not its contents template. Physical-device gallery/file-provider acceptance remains useful alongside simulator checks. + +This change does not upload another TestFlight build. + +## Validation + +- TypeScript and iOS Release simulator build pass. +- 178 Jest tests pass, with three pre-existing TODOs. New tests exercise two-versus-ten-minute station timing, original-plan isolation from actual reports, mandatory quantity/packing/deletion behavior, numeric/native date/time controls, sandbox relocation, file type/size rejection and copy failures. +- ESLint has no errors; existing style-warning backlog remains. +- Native simulator evidence is in workspace `reports/UltraEdge-build7-feedback/`. Planning navigation, saving 2/10-minute stops, and separate race-day controls pass. Brand selection, photo-library import, gear photo cold relaunch, drop-bag photo save/display, mandatory quantity matching/packed status, PDF import and sharing after cold relaunch also pass. Read-only saved-data assertions verify these records and copied files survive app update. Native flows were staged; selectors required explicit keyboard dismissal and taps on file thumbnails. Logs preserve failed selector attempts as well as passing reruns. diff --git a/docs/release/CREW-GPX-FIXES-2026-09-05.md b/docs/release/CREW-GPX-FIXES-2026-09-05.md new file mode 100644 index 0000000..9c6875b --- /dev/null +++ b/docs/release/CREW-GPX-FIXES-2026-09-05.md @@ -0,0 +1,11 @@ +# Crew roles and GPX checkpoint fixes + +Crew creation from a race now advances directly to role selection. The new member is selected and shown first. Edit roles is available on the race and on each race assignment in crew detail. Roles remain independent across races. + +GPX import reads waypoints and named route/track points, including namespaced files, and saves them with the course in a recoverable journaled operation. Coordinates, names, description and elevation are retained. Course mileage is projected onto the closest segment. Existing checkpoint data is preserved and same-name/same-coordinate markers are deduplicated. Ordinary track samples do not become checkpoints. Import checkpoints from GPX can recover markers from an already attached file. + +Distances are estimates, especially for loops/out-and-back courses. Imported markers use Other until the athlete confirms their type; access permissions and cutoffs are not invented. The user's specific file was not supplied. Proprietary extension-only checkpoint formats and waypoint-only files without course geometry are not covered. Failed saves retain staged GPX files to permit journal recovery. + +Validation: 25 Jest suites pass (141 tests, 3 pre-existing TODO); TypeScript passes; changed production files lint clean after removing a redundant try/catch. iOS Hermes export succeeds at /tmp/ultraedge-crew-gpx-export. GPX tests cover 20 markers, interpolated distance, metadata, namespaces, invalid coordinates, repeat import, preservation, interrupted persistence recovery and deleted races. Crew tests cover direct creation handoff, choosing multiple roles, editing and cross-race independence. + +No new native simulator acceptance or TestFlight upload performed in this task. Verify the user's original GPX and physical-device flow on the next candidate. diff --git a/docs/release/FATDOG-GAIA-IMPORT-2026-09-05.md b/docs/release/FATDOG-GAIA-IMPORT-2026-09-05.md new file mode 100644 index 0000000..566a51f --- /dev/null +++ b/docs/release/FATDOG-GAIA-IMPORT-2026-09-05.md @@ -0,0 +1,12 @@ +# Fat Dog Gaia import verification + +Verified the complete user-supplied Gaia GPX document saved with an .xml extension. Fixture: `fixtures/fatdog-120-gaia.xml`. + +- 8,434 route samples; 15 named waypoints (13 aid stations plus Start and Finish). +- Computed route distance: 122.825753 miles. Waypoints sort into course order rather than XML order. +- Full importer-to-local-storage regression verifies all 15 names, estimated start/finish distances, elevation conversion, source descriptions and stable checkpoint IDs on reimport. +- Removed filename extension rejection from the picker. The importer validates GPX XML content before staging or saving; unrelated XML is rejected. +- Access flags remain manual; source descriptions are preserved, including explicit no-crew access. This historical file does not establish next year's logistics. +- 143 tests passed, 3 todo, across 25 suites. TypeScript and git diff checks passed. +- Jest initially stalled with Watchman; rerun with --watchman=false passed in 1.7 seconds. +- No native UI verification or TestFlight upload performed in this follow-up. diff --git a/docs/release/FATDOG-KML-IMPORT-2026-09-05.md b/docs/release/FATDOG-KML-IMPORT-2026-09-05.md new file mode 100644 index 0000000..b0b00e6 --- /dev/null +++ b/docs/release/FATDOG-KML-IMPORT-2026-09-05.md @@ -0,0 +1,7 @@ +# Fat Dog KML import + +The second user export is KML with an XML filename. Its 136 LineString sections contain exactly the same 8,434 route samples as the prior GPX; its 15 Point placemarks produce identical checkpoints, descriptions, elevations and estimated distances. + +Added content-based KML normalization before staging a course. The saved file is GPX so existing previews, reload and checkpoint recovery use the same parser. XML text is serialized safely. Supports a single course placemark with ordered LineString sections (including MultiGeometry) and Point checkpoints. Rejects invalid coordinates, point-only files, competing course placemarks and section gaps exceeding 50 meters. Does not fetch KML links or icons. KMZ and gx:Track are not supported. + +Validation: 148 tests passed, 3 todo across 26 suites; TypeScript and diff checks passed. Original KML and GPX compare exactly in regression tests; each imports into persistence and reimports without duplicates. iOS production JS export succeeded with ULTRAEDGE_DISABLE_WATCHMAN=1. Native device acceptance and TestFlight upload have not been performed for these changes. diff --git a/docs/release/LIVE-COLLABORATION.md b/docs/release/LIVE-COLLABORATION.md new file mode 100644 index 0000000..47f176e --- /dev/null +++ b/docs/release/LIVE-COLLABORATION.md @@ -0,0 +1,35 @@ +# Live race collaboration candidate — September 6, 2026 + +Implemented with private team rooms on the existing UltraEdge Supabase project (`lgmbgsgzytifqjquqqor`, Canada Central). The project was paused; its restore request succeeded, database/auth/REST became healthy, and the additive migration in `supabase/collaboration/001_live_rooms.sql` was applied. Guest authentication was enabled. Legacy account, premium and cloud-backup features remain disabled. + +## Workflow + +The race operations card offers **Live team sharing**. The owner publishes a snapshot of race timing, station stops and logistics. Crew open **Events → Join / open a live team race**, enter a display name and the private invitation code. Local timing observations are not uploaded as if authored by shared participants: use the live room for team observations. Local plan changes are published explicitly by the owner, with revision checks. + +Each installation has its own Supabase guest identity with a session in secure storage. The owner is the runner; invited participants report as crew. Guest access is tied to that installation, without email/password recovery or owner transfer. Reloading team races recovers server rooms for the same retained identity. A new phone requires a new crew invitation. This limitation is explained before sharing. + +Only members can fetch a room or submit their own observations. Owner controls publication, membership and closing the room. Removing a member rotates the invitation code. Shared room deletion cascades its data. The panel also supports leaving, removing a cached copy and deleting the guest identity and associated server data. Lifecycle changes wait for in-flight sync before clearing storage so a delayed response cannot restore deleted cached data. + +## Synchronization and estimates + +Observations save to a durable local outbox before network upload. Requests carry unique IDs and per-author/checkpoint/kind revisions. Retries are idempotent; stale corrections require refresh and a new correction. A failed request leaves pending evidence visible. The user can explicitly discard a pending queue to resolve conflicts. No last-write-wins overwrite of another person's report occurs. + +The open room checks for updates every 10 seconds while foregrounded, and on returning to the foreground. Requests time out after 15 seconds. This is foreground polling, not continuous background synchronization or push notifications. Pending reports do not change the shared projection until accepted. Last successful sync and pending counts are visible; offline copies remain readable. + +All phones use an absolute published start time. Each phone displays that instant in its own timezone. Individual reports remain visible. Multiple crew observations are averaged first, then weighted equally with the runner observation. Contradictory chronological observations suppress the ETA projection while retaining the evidence for correction. Best/expected/slow progress and station ETAs use the same distance-based model as local operations. + +## Data boundary + +Uploads use an explicit field allowlist for race timing and checkpoints. Contact phone numbers, emails, personal notes and GPX files are excluded. Display names, vehicle/cargo labels and station duties are shared. Private tables have RLS enabled and no direct anon/authenticated grants; the membership-checked RPC is the access path. Snapshot structure/size, membership, report author, checkpoint, observation time and plan/report revisions are validated server-side. In-app privacy/support text and unpublished website drafts describe optional sharing and deletion. Public privacy publication and App Store privacy answers still need to match this candidate before public release. + +## Verification + +- 170 unit/workflow tests pass, with 3 existing todos. TypeScript passes. +- `node scripts/test-live-collaboration.cjs`: 14 checks against the live backend, using three disposable guest identities and a synthetic room. Covers nonmember/table denial, crew publication denial, retry deduplication, stale corrections, author spoofing, invalid departure/checkpoint, plan conflicts, revocation/invitation rotation, leave and close. Those scripted identities are deleted afterward. +- Outbox tests simulate network loss, a successful server write followed by lost local acknowledgement, revoked access, and cache removal during an in-flight sync. +- Two separate iOS simulators created/joined a real backend room. Runner 11:00 and crew 11:10 observations produced 11:05 in the shared view. Owner cold-relaunch acceptance and final evidence are in workspace `reports/UltraEdge-operations/`. +- iOS Release simulator build succeeds. Keychain testing requires simulated entitlements: pass `CODE_SIGN_ENTITLEMENTS=/tmp/ue-simulator-keychain.plist` with the real app identifier and keychain group to Xcode. Xcode embeds these in the simulator entitlement section. Do not add restricted simulator entitlements directly to an ad-hoc code signature: macOS refuses to launch it. Distribution signing is unchanged. + +Not uploaded to TestFlight in this task. Physical-phone, native offline/reconnect and background behavior acceptance remain separate from the simulator and mocked-network checks above. + +Final native checks: owner cold restart retained the 11:05 shared average; owner stopped sharing and deleted its collaboration identity; crew deleted its identity. Both UI deletion flows passed. Supabase now reports ACTIVE_HEALTHY. diff --git a/docs/release/RACE-OPERATIONS.md b/docs/release/RACE-OPERATIONS.md new file mode 100644 index 0000000..4649275 --- /dev/null +++ b/docs/release/RACE-OPERATIONS.md @@ -0,0 +1,40 @@ +# Race operations candidate + +Implemented locally; not uploaded to TestFlight. Live shared team rooms are implemented and verified separately; see LIVE-COLLABORATION.md. The existing account-services boundary remains disabled. + +## Features + +- Race operations entry on event detail; checkpoint detail opens its stop/timer editor. +- Optional per-checkpoint support, first-aid and pacing-start duties for event crew. +- Target moving duration (HH:MM) plus planned stop minutes drives expected finish and station arrivals/departures. +- Best/expected/slow travel windows with configurable 0–50% variation, default 10%. Fixed stops are not scaled. +- Position preview at a chosen number of hours after start, plus expected current position during an explicitly started race. These are distance-based estimates, not location tracking. +- Actual start clock and editable start time. Runner/crew arrival/departure reports can be entered as local YYYY-MM-DDTHH:MM or recorded now. Each source has one editable report per station/event kind; the two sources receive equal weight. Individual reports remain visible and removable; discrepancies over ten minutes are flagged. +- Latest observed checkpoint anchors future ETAs. Observed dwell is subtracted before recalibrating travel pace; missing dwell uses the planned duration. Arrival without departure retains the planned stop. +- Event vehicles, optional crew owner, travelling crew, whole-bag and item/gear cargo allocations. A person/item is assigned to one vehicle; bag items inherit the bag vehicle unless individually overridden. +- PDF uses the calculated arrivals and includes windows, stops, duties, vehicle manifest and time reports. +- Current app icon reused for native launch branding on graphite, including iOS storyboard and Android launch assets. + +## Persistence and validation + +`@ultraedge/race-operations` stores event-keyed records using the existing serialized local-plan operation lock. Event deletion journals removal of the operations record. Loading/saving excludes deleted checkpoint, crew and cargo references. Existing event packing/templates remain separate. + +Invalid durations, invalid actual starts and reversed chronological reports are rejected. Report edits replace only their source; saves read current storage under the lock. Projections require a valid start, positive total distance and target, and nondecreasing checkpoint distances within the course. + +## Limits + +Times use the device timezone. Estimates do not account for terrain, fatigue, closures, or real GPS position. An unrecorded stop uses its planned duration. The local editor records reports on this device. The separate live team room exchanges individually authored reports across phones; local plan updates must be published explicitly. First-aid duty is a logistical label, not qualification verification. Confirm crew access with the race guide. + +The target-as-moving-time interpretation was explicitly communicated to the user as an assumption. Existing target values are preserved, and the event form labels this meaning. + +## Verification + +- TypeScript check passes. +- Jest: 170 passing tests, 3 pre-existing todos across 29 suites. +- New cases cover ultra durations, stop propagation, two-source averaging/correction, observed dwell, arrival-only anchors, invalid chronology, missing distances, scenario progress, colocated stations, serialized writes, orphan cleanup and event isolation/deletion. +- iOS Release simulator build succeeds with `ULTRAEDGE_DISABLE_WATCHMAN=1`; the machine-wide Watchman daemon otherwise stalls bundling/test discovery. Jest uses `--watchman=false`. +- Native acceptance evidence is stored in workspace `reports/UltraEdge-operations/` (updated as checks finish). + +Native checks passed: editing and saving a 30-minute station stop, creating a vehicle, recording runner 11:00 and crew 11:10 arrival reports, and cold-relaunch persistence of their 11:05 average. PDF export verification is recorded separately in the workspace report. + +Native PDF export succeeded: the same 11:05 average, 30-minute stop and recalculated 16:40 finish appear in both the timeline and checkpoint table. Native station support, vehicle owner, travelling crew and whole-bag cargo selections were verified in persisted storage. diff --git a/docs/release/SITE-AND-OWNER-CHECKLIST.md b/docs/release/SITE-AND-OWNER-CHECKLIST.md new file mode 100644 index 0000000..b689ed0 --- /dev/null +++ b/docs/release/SITE-AND-OWNER-CHECKLIST.md @@ -0,0 +1,30 @@ +# Support/privacy publication draft — owner gate + +The static files in `site/` are local drafts, not published pages. They contain no scripts, analytics, external fonts, invented contact email, or unverified custom domain. Relative links allow hosting the directory at a GitHub Pages project subpath. No deployment workflow or remote configuration has been changed. + +## Owner details and metadata to supply + +| Field | Required owner decision/value | +| --- | --- | +| Public support contact | Confirm Beaux monitors the canonical GitHub issue tracker; supply a private email/form for sensitive support and privacy requests if available. Update both pages with that real contact before release. | +| Policy operator | Confirm the legal person/entity responsible for the app and the appropriate public contact details; add them to the privacy page. | +| Hosting | Approve an actual publicly accessible HTTPS host and deployment approach. GitHub Pages is a candidate, not an assumed enabled service. | +| Support URL | Actual deployed URL to `site/index.html`, verified anonymously on a phone. | +| Privacy URL | Actual deployed URL to `site/privacy.html`, verified anonymously on a phone. | +| App Review contact | Owner’s real name, email and telephone in App Store Connect; do not publish these in the site without authorization. | +| Copyright | Owner-confirmed year and rights-holder text for App Store metadata. | +| Store availability | Owner-confirmed territories, seller/trader declarations and release timing. | + +Support and privacy must also be reachable in the app once final URLs exist. Apple requires the privacy policy link both in the listing and within the app: [App Review Guidelines, privacy](https://developer.apple.com/app-store/review/guidelines/#privacy). + +## Candidate evidence and publication checks + +The draft reflects `App.tsx` mounting only local planning/profile providers, `ACCOUNT_SERVICES_ENABLED = false`, local AsyncStorage/file persistence, native `react-native-maps`, system file/photo selection, and explicit PDF sharing. It does not promise offline map backgrounds, editable-plan restoration, or that device backups never contain app data. + +Before publication, verify the exact release binary/network behavior, including legacy GPX paths, native SDK initialization, OS diagnostics and photo handling. Update the policy if actual behavior differs. Do not infer the App Store privacy answers solely from this draft: [Apple’s app privacy guidance](https://developer.apple.com/app-store/app-privacy-details/) distinguishes on-device processing from collection and requires evaluating third-party services. Final disclosure decisions remain pending this evidence and owner review. + +Confirm the support tracker is enabled and appropriate for public support; a public issue tracker is not a private contact channel. The current pages instruct users not to post sensitive details and to request private contact first. The owner must provide an effective private route before a sensitive request can be handled. + +Confirm policy operator/contact and wording, supported version/date, working relative and external links, mobile readability and accessibility, and no private information in examples. Record deployed URLs and checks here after the separately approved publication. Update app links and App Store Connect fields at their authorized gates. + +Deleting the app removes local app data; offloading preserves data. The draft distinguishes app deletion from deleting shared PDFs and system backups. There is no single clear-all-data control in the current app. Do not claim it exists. diff --git a/docs/release/V1-RELEASE-PACKAGE.md b/docs/release/V1-RELEASE-PACKAGE.md new file mode 100644 index 0000000..ba865a1 --- /dev/null +++ b/docs/release/V1-RELEASE-PACKAGE.md @@ -0,0 +1,79 @@ +> Historical local-only build package. Superseded for the new collaboration candidate by RACE-OPERATIONS.md and LIVE-COLLABORATION.md. Do not reuse its local-only privacy or feature claims for the new candidate. + +# UltraEdge V1 release package + +Draft for the free local-planner release. See [build 3 verification](VERIFICATION-2026-09-05.md) for completed local checks and remaining gates. The table below is the full TestFlight/device matrix, whose statuses remain NOT RUN until that exact uploaded candidate is tested. This file is preparation, not evidence of App Store submission or a completed device test. Every test below starts **NOT RUN**; replace its status only with a recorded result and candidate build identifier. + +## Release boundary + +One-device race preparation: events, checkpoints/cutoffs, gear/weights, food and hydration packing and notes, drop bags, event-specific crew contacts/roles, imported GPX and elevation, and PDF sharing. No payment is required. Editable plans do not sync between devices. PDFs cannot restore editable plans. Map backgrounds are not guaranteed offline. This app is not live tracking, turn-by-turn navigation, an emergency service or a timed fueling coach. + +Before submission, verify all account and purchase entry points are absent from the local V1 candidate and that no auth, billing, cloud upload or diagnostics client starts unexpectedly. If authentication remains enabled, recovery, logout, deletion and account isolation become mandatory release gates with dedicated test accounts. + +## Store copy draft + +**Name:** UltraEdge Endurance + +**Subtitle:** Race plans for runners & crews + +**Promotional text:** Prepare your next ultra with checkpoints, gear, drop bags and crew details in one place. Share a PDF race plan before race day. + +**Description:** + +Prepare for the long miles with UltraEdge, a free race planner for ultramarathon runners and their crews. + +Build your event plan with checkpoints, distances, cutoffs and notes. Organize the gear you will carry, the supplies you will pack, and the drop bags you will send ahead. Keep crew contacts and event roles together so the right information is easy to find. + +Import a GPX route to review its shape and elevation. Export your race plan as a PDF and share it with your crew before race day. + +Your editable plans are stored on this device. Save a copy of your PDF outside the app before travel; PDFs are readable copies and cannot restore an editable plan. Map backgrounds may need an internet connection. This version does not include cloud sync, automatic backups, live tracking or crew notifications. There are no subscriptions or in-app purchases. + +**Keywords draft:** ultra,ultramarathon,running,trail,endurance,race,planner,crew,dropbag,GPX,gear + +All copy must match the tested candidate. Do not publish screenshots containing real crew phone numbers, personal routes, or private health/profile details. + +## App Review notes draft + +UltraEdge is a free local race preparation tool. No review account or purchase is needed for the intended V1 candidate. Data remains on the device; the user can explicitly share a PDF using the system share sheet. + +Suggested review path: finish or skip the introduction, create an event in Events, add checkpoints, assign gear and crew, add a drop bag, import a GPX file from Files, then export the event PDF. Route map backgrounds may need network access; the imported route/elevation and saved planning details are the offline scope. + +Before sending these notes, run this path on the exact uploaded build and update names/instructions to match it. Provide a non-sensitive GPX sample to the reviewer through an approved accessible URL or attachment. No sample URL is published by this draft. + +## Smoke-test record + +Record candidate commit, build number, archive SHA-256, iOS/Xcode versions, device name/OS, tester/date, install type, network state, and screenshots/log locations. Test both a small iPhone and a physical iPhone; include iPad if the candidate supports it. Existing TestFlight build 2 is not evidence for new changes. + +Use fictional crew contacts and a realistic overnight 100-mile/161-km event: at least 12 checkpoints, several cutoffs after midnight, mixed crew access/pacer points, three drop bags, 20 gear items, food/water notes, four crew members with multiple roles, long names and multiline notes. Include a multi-day event and miles/km variants. Keep an original fixture copy to detect lost values. + +| Journey | Expected result | Status | +| --- | --- | --- | +| Clean install → onboarding Skip and final CTA, then cold restart | Both routes enter planner without account/payment; introduction stays dismissed | NOT RUN | +| Onboarding with VoiceOver, largest text and rotation | Text scrolls, actions remain reachable, current step understandable | NOT RUN | +| Create/edit realistic event and checkpoints; cold restart offline | Values, units, date, cutoff text and ordering remain correct | NOT RUN | +| Overnight cutoffs, miles/km and feet/meters | Display and PDF preserve meaning; no unannounced unit conversion or date ambiguity | NOT RUN | +| Gear, supplies, drop bags and crew assignment/edit/removal | Correct event and checkpoint relationships; no duplicate or missing roles | NOT RUN | +| Upgrade legacy crew-role data, including custom roles | Existing assignments survive restart; interrupted migration is recoverable | NOT RUN | +| Delete populated event and restart | Related assignments/checkpoints/bags/routes do not orphan or affect another event | NOT RUN | +| Storage read/write failure and rapid edits | User sees failure, prior data survives, retry does not replace it with empty state | NOT RUN | +| GPX valid, malformed, very large, no elevation, cancelled picker | Clear result; bad replacement retains old route; UI remains responsive | NOT RUN | +| Imported GPX → airplane mode → force quit/reopen | Saved route/elevation usable; missing map background is handled honestly | NOT RUN | +| Full PDF export with long notes, contacts, roles and multiple pages | All expected data present and legible; omissions/errors disclosed; system sharing works | NOT RUN | +| Export PDF then cancel share sheet; retry | No false failure/success, duplicate operations or stuck loading state | NOT RUN | +| Background/foreground, denied photo access, low connectivity | No crash, blocked navigation or lost edits | NOT RUN | +| VoiceOver, large text, dark mode, smallest supported phone/iPad | Labels, focus, contrast, form validation and primary actions usable | NOT RUN | +| Fresh install network observation and native artifact inspection | No unexpected cloud/auth/billing/diagnostic traffic; permissions/privacy match candidate | NOT RUN | +| Reinstall/data removal and local-data explanation | App makes device-only persistence and non-restorable PDF clear; no backup promise | NOT RUN | + +## Remaining release requirements + +- Capture current screenshots from the verified candidate: event list, event plan/checkpoints, drop bag/gear, crew roles, GPX/elevation, PDF. Include only screens actually exercised, at required sizes for supported devices. +- Publish and verify a reachable support page and privacy policy with owner approval. GitHub Issues is a development contact, not proof of a completed support/privacy listing. Confirm a support contact Beaux will monitor. +- Audit the actual release binary and configuration for data handling, third-party SDKs, required-reason API declarations and permission strings. Complete App Store privacy answers from that evidence, including any enabled diagnostics; do not assume local planning alone means no collection. +- Complete age rating, category, content rights, availability, review contact, screenshots and review notes in App Store Connect at the approved external gate. +- Confirm current Apple SDK/signing requirements with official sources when preparing the artifact. Record the reproducible archive and run the matrix on the exact TestFlight candidate after approved upload. +- Require no unresolved core-flow failure. Obtain separate approval to push/merge, mutate production/store configuration, upload TestFlight, and submit/release. + +## Athlete-focused follow-up + +Treat midnight/date/unit ambiguity, omitted crew instructions in PDFs, and silent lost plans as release blockers when reproduced. Confirm realistic PDF legibility on paper as well as a phone. Route replacement must never erase a usable offline route on failure. Free-text nutrition notes should remain clearly distinct from prescribed fueling targets. Do not expand V1 into navigation, live coordination or performance predictions to cover a documentation gap. diff --git a/docs/release/VERIFICATION-2026-09-05.md b/docs/release/VERIFICATION-2026-09-05.md new file mode 100644 index 0000000..84ba7c0 --- /dev/null +++ b/docs/release/VERIFICATION-2026-09-05.md @@ -0,0 +1,54 @@ +# Build 3 verification — September 5, 2026 + +**Ready for an approved internal TestFlight upload; not approved for App Store release.** + +Candidate source: `4e687ad` on `release/v1-readiness`, based on canonical main plus the four PR #17 commits. This source has not been pushed, merged, or checked by remote CI. Subsequent documentation-only commits do not alter the candidate binary. + +## Artifacts + +- App version 1.0.0, build 3, bundle ID `com.beaux.ultraedge`. +- Xcode 26.6 (17F113), iOS 26.5 SDK, committed native project. +- Release simulator build, signed distribution archive, and local IPA export all succeeded. +- Existing Apple Distribution identity and App Store provisioning profile used; no signing assets created and no provisioning update requested. +- `codesign --verify --deep --strict` passed on archived app. +- IPA SHA-256: `6e709ec1daefba316a48bdf5510b30a8d3117f0db5d559765d158d0763bdc18d`. +- Final simulator JS bundle SHA-256: `ec64f2a57c29f7fbc7ceb9989e930a9cb1902456289724e7a136a70feb6d774f`. Archive bundle SHA-256: `a441681456d8064bc084d62eafbe62259f8ed828342e9fe08d51558da1fb68c2`. +- Durable local artifacts and evidence: sibling workspace directory `../reports/UltraEdge-build3/`, including IPA, full archive/dSYMs, native PDF and screenshots. Build logs remain local; they are not committed because build environments can contain private configuration. +- The local export logged an unavailable Xcode account token but still succeeded with the existing signing identity/profile. This does not certify upload credentials or server acceptance. + +## Automated evidence + +All **122 tests passed across 20 suites**, with **3 existing TODO tests**. TypeScript passed. Full repository ESLint completed with **zero errors**; warnings remain. Diff whitespace check passed. Date/PDF cases also passed with explicit Vancouver and Tokyo time zones. + +Coverage now includes storage read/write failures, journal recovery, partial relationship deletion, crew migration retry, overlapping saves/conflicts, deleted-parent rejection, profile/weight recovery and conversions, malformed GPX/import rollback, sandbox relocation, PDF print/share failure, list error/retry behavior, and disabled service startup. + +A hung machine-wide Watchman caused the first local Jest/bundler attempts to wait. Tests used `--watchman=false`; Metro used `ULTRAEDGE_DISABLE_WATCHMAN=1`. No shared daemon was reset. The obsolete third-party review action is replaced with an explicitly manual checklist, which is not automated code review. + +## Native observations + +On iPhone 17 Pro simulator, iOS 26.5: + +- Release launch showed onboarding and the planner; no black screen or Metro server required. +- Completed all four onboarding slides and the final action; cold launch retained completion. +- Created `Native smoke 50K` through the actual event form. Input/save navigation worked. A test needed to dismiss the keyboard by tapping a label; that was an automation adjustment. +- Loaded the fictional overnight 100-mile fixture (12 checkpoints, three crew with multiple roles, 20 gear allocations, three bags). This exercises rendering and persistence, not all creation forms. +- Reproduced the calendar-date bug as September 18 for a September 19 race, fixed it, then asserted **Saturday September 19** on the rebuilt native screen. +- Generated a five-page PDF and opened the native share sheet. Reviewed every rendered page and extracted text: route, profile, all expected records and packing notes are present without horizontal clipping. +- Reproduced GPX unavailability when installing an app update moved its sandbox. The repaired build resolves the same saved URI against the new Documents directory. Reinstallation and PDF export then passed **without re-importing, reseeding, or rewriting that saved URI**. +- Dismissed the share sheet and exported again successfully. +- Opened Profile → Privacy & Support and confirmed the local policy content renders. +- Deleted the populated fictional event through the UI; after cold launch the unrelated manually created event remained. Direct storage inspection confirmed zero fixture event/checkpoint/crew-assignment/gear-allocation/bag records, while reusable three crew and 20 gear inventory items remained. + +Additional native layout checks on the immediately preceding release build (same onboarding implementation): iPhone SE 3rd generation passed Skip/relaunch, then all slides/final action with the largest accessibility text size and dark appearance; iPad mini passed all slides/final action/relaunch. These are limited layout checks, not complete VoiceOver/device-matrix certification. + +## Remaining gates and limitations + +1. Owner approval to push the release branch and upload build 3 to **internal TestFlight**; then remote CI and Apple processing must pass. Nothing has been pushed, merged, uploaded, or submitted. +2. Physical iPhone testing of the exact TestFlight binary: airplane-mode cold start, native GPX picker (valid/malformed/cancel/large), create/edit checkpoint/crew/bag/gear journeys, legacy upgrade, PDF save/share to another app, denied permissions, VoiceOver and full large-text/rotation matrix. These have **not** been certified by this simulator run. +3. Support/privacy pages exist as local drafts and an offline in-app screen. Public hosting/URLs, operator/private contact, App Store listing/screenshots/age rating/privacy answers/review contact still need owner decisions and publication at the approved gate. +4. No production backend/billing changes were made. Auth, cloud sync, purchases, Sentry and related promises are disabled for this free local V1. Re-enabling them requires the original backend/account/billing release gates. +5. Native traffic observation and final privacy answers remain unverified. Absence of JS client startup does not certify all OS/native SDK network behavior. +6. PDF pagination polish: a long gear note can span pages, and checkpoint headers are not repeated on the continuation page. All fixture text is retained. +7. Previous GPX versions/cache files can remain in local Documents after replacement or event deletion. They are not referenced by deleted plans; complete local removal currently requires Delete App. No editable-plan backup/restore exists. + +This evidence supports a beta candidate, not a claim that the production definition of done is met. diff --git a/docs/release/WORKFLOW-QA-2026-09-05.md b/docs/release/WORKFLOW-QA-2026-09-05.md new file mode 100644 index 0000000..3005bc3 --- /dev/null +++ b/docs/release/WORKFLOW-QA-2026-09-05.md @@ -0,0 +1,38 @@ +# Athlete workflow QA — September 5, 2026 + +Scope: local iOS race planning, shared inventory and crew, reusable drop bags, race-specific vest packing, persistence and export. This work does not enable cloud sync or submit an App Store release. + +## Changes + +- Saved drop-bag templates live outside any race, with independent item copies when applied. Race and checkpoint are never stored on the template. Deleting the source race preserves templates; deleting templates preserves created bags. +- Race gear now supports quantity, In vest/Worn placement and Packed state independently for each event. PDF export includes these quantities and packing states. +- Creating gear or crew from a race offers an explicit assignment step with the new record selected. Crew selection preserves that selection after asynchronous loading. Creating from an empty selector avoids duplicate stale screens. +- Drop-bag gear selection now uses a native modal with a bounded scroll area above the form. The old absolute overlay was covered by the fixed Save footer; native testing exposed empty bags even though automation reported successful taps. The picker also dismisses the keyboard, handles the first item tap and blocks saving behind the open picker. + +## Automated checks + +133 tests pass across 24 suites; three pre-existing example TODOs remain. TypeScript passes. ESLint has no errors; the repository retains its existing large warning backlog. Tests cover provider remounts, shared-profile reuse, independent event quantities/roles, race/profile/gear deletion cleanup, template retention, write failure and retry, malformed storage preservation, assignment handoff and picker interaction. + +Commands: `npm test -- --runInBand --watchman=false`, `npm run typecheck`, `npm run lint`. + +## Native evidence + +The parent agent uses a dedicated disposable iPhone 17 Pro simulator on iOS 26.5 (8A6FDAAE-A2ED-4F28-B841-40B9DE915338). All fictional races and records are entered through forms; no fixture seeding. The prior failed run exposed the keyboard bug and is retained in workspace reports. After the modal fix, native bag creation, template reuse, cold relaunch and PDF sharing passed. The saved data was separately read and asserted; successful taps alone were insufficient evidence in the earlier failed run. + +Reproduction: `docs/release/maestro/workflow-all.yaml` followed by `docs/release/scripts/verify-native-workflow.py`. Native build uses `ULTRAEDGE_DISABLE_WATCHMAN=1` to avoid the machine-wide Watchman stall. + +## Release limits + +This is simulator and automated-test evidence, not a physical-device TestFlight acceptance pass. These changes are not in the already distributed build 4. The next release still needs a signed candidate and real-device acceptance of these workflows. GPX document import, route basemap availability, upgrade migration on a physical phone, and broad device/accessibility coverage were not re-certified by this targeted pass. + +## Verified outcomes + +- Created two races, a drop-bag checkpoint, gear and a crew profile through native forms. +- First race has two packed flasks in the vest and Alex as pacer. Second race reuses the same flask/profile with quantity one, unpacked state and driver role. +- Saved a one-item bag template; second-race copy retains the item, uses new item IDs and has no checkpoint inherited from the first race. +- Relaunch preserves records and packed state. Read-only simulator storage assertions pass. +- Native PDF opens in the iOS share sheet. The one-page document was extracted and visually checked: pacer role, flask quantity two, in-vest/packed state, bag contents and checkpoint name are present. + +Evidence lives in workspace `reports/UltraEdge-workflow-qa/`: `bag-final.log`, `reuse-final.log`, `storage-verification.log`, `QA-Alpine-race-plan.pdf`, `native-pdf.txt`, screenshots and `simulator-bundle-sha256.txt`. Creation/checkpoint steps appear in `native-final.log`, which also records the pre-modal failure; this is a staged fix-and-rerun record, not a claim that the original uninterrupted flow passed. + +The final native create-from-race shortcut also passes for a new crew member and headlamp (`handoff-final.log`); both are confirmed assigned in saved data. diff --git a/docs/release/fixtures/README.md b/docs/release/fixtures/README.md new file mode 100644 index 0000000..087ea32 --- /dev/null +++ b/docs/release/fixtures/README.md @@ -0,0 +1,32 @@ +# Fictional overnight release fixture + +Moonridge 100 is a **fictional** 100-mile, 28-hour rehearsal starting Saturday September 19, 2026 at 05:00 and ending Sunday at 09:00. Nothing in these files is a real course, contact, or fueling prescription. The synthetic GPX is a straight northbound line with nine elevation waves, 2,001 points and approximately 100 miles. It is for rendering tests only and must not be used for navigation. + +The JSON contains one event, 12 checkpoints, three crew members with event-specific multi-role assignments, 20 gear items and allocations, and three checkpoint-linked bags. The final bag is intentionally empty. Crew details use reserved example.com email addresses and fictional 555 phone numbers. Overnight instructions, a newline, a long event name, and literal ``/`&` text exercise PDF output. + +## Seed a disposable release simulator + +Terminate UltraEdge first so in-memory AsyncStorage cannot overwrite the fixture. Obtain its **data** container using `xcrun simctl get_app_container com.beaux.ultraedge data`, then pass that returned path explicitly: + +```sh +python3 docs/release/fixtures/seed-simulator.py --container '/absolute/CoreSimulator/Devices/.../data/Containers/Data/Application/...' +``` + +The script refuses paths outside a CoreSimulator application data container and requires its metadata identifier to be `com.beaux.ultraedge`. It does not launch, terminate, select, or reset a simulator, contact a service, or modify onboarding. Existing unrelated records remain; re-seeding replaces only fixture identities. Before writes, it backs up the native storage directory and any prior fixture GPX inside a timestamped `fixture-backup-*` directory in the supplied container. Re-seeding intentionally resets changes made to the fictional fixture. + +Storage format was verified against installed AsyncStorage 2.2.0 `ios/RNCAsyncStorage.mm`: `Library/Application Support/com.beaux.ultraedge/RCTAsyncLocalStorage_V1/manifest.json`; values are JSON-encoded strings, with values longer than 1,024 UTF-16 units in UTF-8 MD5(key) files referenced by null manifest entries. Checkpoints use an event-ID map; other fixture collections use arrays. The script resolves the local GPX URI against the supplied container. + +## Expected native/PDF assertions + +- Event remains September 19 in America/Vancouver; distance reads 100 mi, climb 12,400 ft, goal 28:00 and cutoff 32:00. +- Twelve checkpoints appear in increasing distance, from Cedar Base at 0 to Dawn Meadow at 100 miles. +- Arrival labels preserve Saturday/Sunday overnight handoff distinction. Moonrise Camp notes retain spare lighting and warm-layer instructions. +- Alex River has Crew Chief / Driver; Jordan Vale has Pacer / Finish logistics. All three fictional contacts appear in the PDF. +- Twenty allocated gear items appear. Spare batteries have quantity two and the literal note `Replace batteries at Moonrise Camp .` renders visibly without becoming HTML. +- Three named bags appear at North Fork, Moonrise Camp, and Owl Hollow. The empty reserve bag remains in the PDF with “No items listed.” +- Route map and elevation profile appear; start and finish and checkpoint markers align along the synthetic line. Inspect pagination, clipping, and long notes in the generated PDF and share sheet. +- Cold-start offline and verify all records and the local route persist. +- For failed-route testing, use a copy of the fixture and replace the route with malformed XML through the picker: prior route must remain. If the saved file is unavailable, PDF export must ask before proceeding and the resulting PDF must state “Course Route Unavailable.” +- Deleting the fictional event must remove its relationships and bags without deleting reusable crew or gear inventory; inspect after relaunch. + +These are expected assertions, not a claim of native test completion. Fixture seeding bypasses create/edit UI and therefore cannot certify those input journeys. diff --git a/docs/release/fixtures/fatdog-120-gaia.kml b/docs/release/fixtures/fatdog-120-gaia.kml new file mode 100644 index 0000000..80b40d2 --- /dev/null +++ b/docs/release/fixtures/fatdog-120-gaia.kml @@ -0,0 +1,1917 @@ + + + + + + + + + + + + + + + + + + + + Fatdog 120 + Course map and Aid Station locations + + Cathedral Aid Station + Minor Aid Station + #122 + + -120.19227837213974,49.07446625991966,2141 + + + + Trapper Aid Station + Major Aid Station. Relay Exchange + #127 + + -120.38960857193335,49.03914731330056,1810 + + + + Start Line + 120 mile Start Line + #132 + + -120.08882705187045,49.13258117993627,852 + + + + Ashnola Aid Station + Major Aid Station, Crew Accessible, Relay Exchange, drop bag + #137 + + -120.33304949618375,49.06464645198196,1190 + + + + Heather Aid Station + Minor Aid Station + #142 + + -120.76196713470051,49.14183265670292,2046 + + + + Finish Line + Major Aid Station at finish, hot food, first aid + #147 + + -120.82648332566515,49.06210011221765,1244 + + + + Windy Joe's Aid Station + Major Aid Station, Relay Exchange + #152 + + -120.79805168888316,49.063938931299106,1192 + + + + Strawberry Flats Aid Station + Major Aid Station, Crew Accessible, Relay Exchange, Drop Bags, Pacer Exchange + #157 + + -120.88369329514545,49.066172333717674,1370 + + + + Pasayten Aid Station + Minor Aid Station, Drop Bags, no crew access + #162 + + -120.58141030483819,49.15188623977437,1012 + + + + Blackwall Peak Aid Station + Major Aid Station, Crew Accessible, Relay Exchange, Drop Bags, Pacer Exchange + #167 + + -120.77033605523553,49.078533912621054,1796 + + + + Grainger Creek Aid Station + Minor Aid Station + #172 + + -120.93385887609338,49.23995290695365,953 + + + + Nicomen Aid Station + Major hike - in aid station (no crew access) + #177 + + -120.860660161733,49.226888750510426,1791 + + + + Bonnevier Aid Station + Major Aid Station, drop bags, crew access + #182 + + -120.60530650481667,49.14892179692842,1021 + + + + Calcite Aid Station + Major Aid Station, drop bags, no crew access + #187 + + -120.5529181373999,49.11191116480336,1396 + + + + Hope Pass Aid Station + Major Aid Station, drop bags, crew access + #192 + + -120.78094721781834,49.27025851632118,1609 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fatdog 120 + + + + 0 + clampToGround + -120.088838,49.132515,851.8495999999386 -120.089205,49.13245,852.6443200000434 -120.089329,49.132438,853.599523840069 -120.089485,49.132447,855.1471999999535 -120.089589,49.132477,856.5440000000588 -120.089707,49.132549,858.5959999999923 -120.089837,49.132634,859.5018553600423 -120.090088,49.132736,859.7518336001073 + + + 0 + clampToGround + -120.090088,49.132736,859.7518336001073 -120.090087,49.132736,859.7671264000688 -120.089837,49.132634,859.5018553600423 -120.089707,49.132549,858.5959999999923 -120.089589,49.132477,856.5440000000588 -120.089485,49.132447,855.1471999999535 -120.089329,49.132438,853.599523840069 -120.089205,49.13245,852.6443200000434 -120.088838,49.132515,851.8495999999386 -120.088815,49.132458,850.9436383999773 -120.088783,49.132321,849.1512905599872 -120.088768,49.132243,848.2340940800298 -120.088765,49.132164,847.790400000011 -120.08882,49.132103,847.5708000000027 -120.088854,49.13205,847.3799999999984 -120.088868,49.131978,847.1207999999878 -120.08894,49.131737,852.5490111999321 -120.08895,49.131701,853.5476079999897 -120.088912,49.131679,853.8519059199965 -120.088849,49.131686,853.3791945599385 -120.088777,49.131662,853.8760329600581 -120.088734,49.131623,856.0271452801512 -120.088724,49.131577,858.7023859199929 -120.088743,49.131562,859.682504639827 + + + 0 + clampToGround + -120.088743,49.131562,859.682504639827 -120.088775,49.131536,861.3939360001707 -120.089004,49.131536,863.4104012802308 -120.089376,49.131571,865.6678796799433 -120.0898,49.131582,871.8484799998027 -120.090224,49.131571,879.2137516800129 -120.090825,49.131501,884.728983999982 -120.091355,49.131443,891.9305656000633 -120.091567,49.131339,898.6044404800786 -120.091956,49.131362,901.0968268798529 -120.092398,49.131432,902.4032000001648 -120.092786,49.131559,900.8402729599619 -120.093228,49.131628,907.5343999999596 -120.094232,49.131695,911.9331903999948 -120.094907,49.131698,913.7591999999963 -120.095508,49.131698,917.3039999999619 -120.096038,49.131744,918.0009497600694 -120.096038,49.131744,918.0009497600694 + + + 0 + clampToGround + -120.096038,49.131744,918.0009497600694 -120.096515,49.131663,925.6567783998107 -120.096915,49.131539,936.9060824001699 -120.096928,49.131394,946.4330892800623 -120.097505,49.131258,962.5281551998196 -120.098159,49.131316,964.046000000074 -120.098495,49.131362,963.0498751998429 -120.099095,49.131235,975.2050680002397 -120.10005,49.13105,989.4595999999209 -120.101234,49.130669,999.3741238400038 -120.101711,49.130345,1005.0560000001739 -120.101905,49.130056,1014.8624543998245 -120.102153,49.129836,1020.3440723200858 -120.102259,49.129616,1026.712629760006 -120.1024,49.129073,1039.5161920000462 -120.102418,49.128865,1046.6224000001043 -120.102488,49.128738,1051.6048000001101 -120.102736,49.128622,1055.6800000000244 -120.102824,49.128483,1058.9723436799377 -120.102807,49.128321,1063.2944000000157 -120.102877,49.128217,1066.411999999997 -120.103048,49.128163,1067.5459999999734 -120.103566,49.128078,1072.0972780799323 -120.104061,49.127928,1073.1861593600438 -120.104591,49.127581,1077.9857518400258 -120.105086,49.127038,1089.2557267200255 -120.105298,49.126494,1097.2227475199236 -120.105265,49.12611,1107.507448000024 + + + 0 + clampToGround + -120.105265,49.12611,1107.507448000024 -120.105376,49.125395,1128.7951999998768 -120.105469,49.125286,1129.2559999999412 -120.105589,49.125219,1127.335600000087 -120.105719,49.125204,1124.7976000002036 -120.105969,49.125201,1120.916800000034 -120.106062,49.125238,1119.2464000000055 -120.106121,49.125281,1118.0148259199873 -120.10628,49.125421,1116.3261695999026 -120.106646,49.125722,1122.511999999838 -120.107074,49.126003,1134.9180771199726 -120.107393,49.126159,1142.521600000015 -120.107958,49.126391,1148.783730880019 -120.108079,49.126413,1150.3131777601084 -120.108186,49.126415,1152.6340272000086 -120.108265,49.126398,1155.1389935999905 -120.108336,49.126353,1158.106679039794 -120.108358,49.126302,1160.1294700798157 -120.108305,49.126228,1161.0806784000988 -120.107611,49.125654,1162.367618240156 -120.107574,49.125543,1163.5127999999872 -120.107595,49.125432,1165.489200000011 -120.107647,49.125364,1167.4044000000345 -120.107748,49.125276,1170.4646860800165 -120.107918,49.125144,1173.5239999999621 -120.108039,49.124893,1176.041284160107 -120.108168,49.124649,1181.0636947202083 -120.108794,49.124,1197.1008000002012 -120.109196,49.123754,1207.2737587198367 -120.109589,49.123557,1217.8109739199606 -120.10994,49.123424,1226.3380095998502 -120.110211,49.123276,1231.3435072001212 -120.110473,49.123147,1237.7202382399755 -120.110679,49.122959,1241.333673440104 -120.110732,49.122719,1240.0220166399215 -120.110847,49.122782,1243.374052160177 -120.111113,49.123082,1251.454952640071 -120.111148,49.12329,1255.7247167999647 -120.11106,49.123903,1268.00745279984 -120.110971,49.124829,1285.19189136017 -120.111042,49.12506,1289.9548223999398 -120.111272,49.125199,1293.349722239978 -120.111572,49.125257,1296.9598163199657 -120.111766,49.125141,1302.6076000000746 -120.111926,49.124921,1309.663663359911 -120.112085,49.124852,1314.6731999998724 -120.112208,49.124898,1317.1103999998613 -120.112544,49.125048,1321.2318284800022 -120.112844,49.125083,1326.62676607999 -120.113357,49.124944,1335.761176319908 -120.113551,49.124887,1339.266768480006 -120.113657,49.124713,1343.1728539200867 -120.11371,49.124447,1345.550174399898 -120.113746,49.1241,1349.554112000267 -120.113958,49.123741,1361.0342777598432 -120.114364,49.123059,1381.1927820797841 -120.114523,49.12277,1388.2462448002962 -120.114594,49.122169,1391.431370239871 -120.114629,49.121961,1392.3543929598072 -120.114718,49.12181,1395.3623999998795 -120.114912,49.121706,1400.7375411200967 -120.115495,49.121509,1414.7064103999153 -120.115972,49.121162,1419.8210534399605 -120.116556,49.120908,1431.9520000000898 -120.117224,49.120286,1442.211810559755 + + + 0 + clampToGround + -120.117224,49.120286,1442.211810559755 -120.118729,49.118884,1475.3120083200765 -120.119083,49.118514,1481.9245014400858 -120.119118,49.118329,1480.7599331201036 -120.119242,49.118259,1483.0514924800468 -120.119613,49.118167,1493.1038481600283 -120.120037,49.118005,1499.6341575998238 -120.12055,49.117739,1504.1736239998736 -120.121133,49.117253,1518.8313931199846 -120.121345,49.116952,1514.6547904001147 -120.121416,49.116675,1506.2025439999159 -120.121557,49.116443,1503.5388281597102 -120.121751,49.116316,1506.3184646399966 -120.122016,49.116223,1510.553922559998 -120.122529,49.116119,1523.347505119911 -120.122776,49.116039,1530.6363385600753 -120.122971,49.115772,1531.3263769602027 -120.123554,49.115437,1544.9278499199552 -120.124773,49.114916,1568.162800000141 -120.12571,49.114465,1592.2639999999399 -120.126063,49.114315,1607.6297336000878 -120.126399,49.114003,1617.3304937601192 -120.126541,49.113783,1622.680290240102 -120.127336,49.113239,1630.337100799999 -120.127618,49.113135,1632.260292799992 -120.128024,49.113071,1629.0115545600004 -120.128415,49.11305,1623.451880000041 -120.128626,49.113146,1627.2328000000193 -120.128658,49.113443,1641.2656844798423 -120.128691,49.11372,1654.5584608000524 -120.129049,49.113923,1664.615163839985 -120.129797,49.114061,1674.0803963199642 -120.130333,49.114167,1678.8223228801453 -120.130675,49.114295,1684.6300399999486 -120.131537,49.114678,1696.5885708799271 -120.131667,49.114763,1698.3016476798584 -120.131862,49.114891,1703.2830713599628 -120.132019,49.114884,1705.7845868799732 + + + 0 + clampToGround + -120.132019,49.114884,1705.7845868799732 -120.13209,49.114881,1706.5713712000966 -120.13235,49.114795,1710.0675599999806 -120.132887,49.114721,1716.1488000000377 -120.13435,49.114582,1732.4463999999405 -120.136204,49.114497,1744.9113945600325 -120.137391,49.114263,1746.3961686399368 -120.140546,49.113795,1752.5483456001537 -120.143701,49.113209,1749.2320867198314 -120.144026,49.113114,1751.2138259199965 -120.144286,49.113007,1751.1276822401114 -120.144384,49.11289,1747.24693120002 -120.144563,49.112847,1746.2428000000693 -120.144839,49.11289,1747.7167168000078 -120.145034,49.112879,1747.7063948799675 -120.145262,49.112784,1747.0009446399763 -120.145441,49.112645,1745.6101456001093 -120.145587,49.112571,1746.1841321601414 -120.14588,49.112571,1749.5710592000794 -120.146124,49.112634,1752.0224000000007 -120.147083,49.112986,1758.498799999982 -120.147295,49.113007,1759.2619999999988 -120.147425,49.112986,1759.729999999994 -120.147571,49.112879,1761.022399999989 -120.148222,49.112613,1771.0413545600568 -120.148915,49.112308,1776.3673728000256 + + + 0 + clampToGround + -120.148915,49.112308,1776.3673728000256 -120.149116,49.112219,1779.0764000001268 -120.150043,49.111868,1798.6731999999547 -120.150775,49.11157,1807.9999999999195 -120.15123,49.111293,1814.989745600015 -120.151571,49.111102,1817.8028000000884 -120.151994,49.110825,1820.7527040000089 -120.152336,49.110452,1821.9638988801469 -120.152612,49.110218,1823.4895372802025 -120.153523,49.109771,1839.4518006402334 -120.154125,49.109537,1850.6159999998897 -120.154661,49.109366,1859.9829590401412 -120.155019,49.109122,1864.800601279851 -120.155377,49.108919,1871.150800000039 -120.155848,49.108749,1877.254209920229 -120.156385,49.108685,1884.8560000000534 -120.157995,49.108515,1898.5634279999363 -120.158385,49.108515,1902.07835599988 -120.158613,49.108504,1905.4780000000778 -120.159133,49.108334,1909.5226908799498 -120.159458,49.108195,1912.1584000000523 -120.160369,49.108036,1918.797119360017 -120.160613,49.107961,1920.9396000000002 -120.160759,49.107866,1921.8323999999957 -120.160938,49.107599,1922.9718169600237 -120.16128,49.107259,1925.1407008000438 -120.161589,49.106993,1926.6367259201 -120.162093,49.106748,1928.5697894400344 -120.162321,49.106609,1928.0884000000292 -120.162499,49.106333,1925.986075679963 -120.162695,49.106109,1923.3530647999096 -120.163004,49.105896,1920.3917286400438 -120.163443,49.105736,1922.338462080018 -120.164126,49.105534,1923.9513606399214 -120.165036,49.105098,1919.1654771199526 -120.165459,49.104895,1917.2807999999623 -120.166402,49.104161,1907.9646371199847 -120.166469,49.104122,1907.5988252799687 + + + 0 + clampToGround + -120.166469,49.104122,1907.5988252799687 -120.166728,49.103969,1905.66280000001 -120.166858,49.103799,1903.8187043200328 -120.167102,49.103256,1899.577600000012 -120.167248,49.103107,1899.4803865600136 -120.167427,49.103,1899.0846400000332 -120.167752,49.102904,1900.1775999999754 -120.167996,49.102745,1901.9064000000244 -120.168289,49.102532,1903.8484140799815 -120.168695,49.102287,1902.6307735999355 -120.169004,49.102042,1899.819199999965 -120.169134,49.101787,1897.1820000000162 -120.169199,49.101435,1892.7610776000483 -120.169248,49.101158,1892.0494246399956 -120.169297,49.101084,1892.5337939199967 -120.169525,49.100945,1895.8779999999113 -120.16985,49.100754,1900.427999999964 -120.170321,49.100338,1904.5326659199936 -120.170435,49.100221,1905.6132000000252 -120.1705,49.100072,1907.2224000000294 -120.170419,49.099817,1909.9763999999827 -120.170061,49.098763,1918.1260000000277 -120.170012,49.098465,1920.0724768000182 -120.170061,49.098262,1921.5699932800173 -120.170126,49.098049,1923.5479999999407 -120.170061,49.09774,1927.9294655999843 -120.169606,49.097006,1937.9168000000018 -120.169476,49.096878,1939.0432268800548 -120.169476,49.096729,1941.127891839955 -120.169687,49.096559,1942.387600000003 -120.170549,49.096079,1947.9582563199617 -120.170777,49.095973,1948.7927356799992 -120.170907,49.09576,1951.057200000015 -120.170842,49.095568,1952.8967999999747 -120.170744,49.095366,1954.7256000000132 -120.170793,49.095174,1956.6020000000112 -120.171362,49.094312,1964.2832000000062 -120.171509,49.094131,1965.4324000000015 -120.171752,49.094035,1966.3071999999925 -120.172143,49.093875,1968.5296000000608 -120.1725,49.093747,1972.5323999999903 -120.172663,49.093481,1976.147485760009 -120.172761,49.093236,1977.6615641600054 -120.172891,49.092991,1980.3747262400748 -120.173118,49.09281,1984.1016000000157 -120.173525,49.092725,1988.0810999999633 -120.173932,49.092661,1990.6123539199934 -120.174289,49.092523,1993.6589302400023 -120.174549,49.092331,1994.9031964799958 -120.174631,49.092203,1995.0464747200065 -120.175411,49.092012,2002.1955999999775 -120.175915,49.09182,2006.4034240000328 -120.176338,49.09166,2012.3152000000352 -120.176712,49.091575,2016.316399999976 -120.177005,49.091564,2018.9002271999416 -120.177233,49.091628,2021.4227980800142 -120.177525,49.09166,2025.0900000000101 -120.177786,49.091607,2026.0359580799889 -120.178111,49.091479,2027.8752000000059 -120.178323,49.091437,2028.7896000000248 + + + 0 + clampToGround + -120.178323,49.091437,2028.7896000000248 -120.178696,49.091362,2031.4319820800245 -120.178973,49.091255,2033.5696000000169 -120.179038,49.091128,2034.9520000000566 -120.179217,49.091064,2036.8711999999266 -120.179623,49.091053,2040.0476737600216 -120.180127,49.091064,2045.507199999941 -120.180501,49.091074,2051.3376780801213 -120.181103,49.091011,2057.3125236799533 -120.181867,49.090947,2071.264315039942 -120.182225,49.090915,2075.1350599999887 -120.182567,49.090851,2078.0021403200135 -120.182941,49.09068,2082.5155104000055 -120.183412,49.090521,2090.9740000000356 -120.18403,49.090329,2097.462800000083 -120.184876,49.090201,2109.6430150400265 -120.185591,49.09002,2116.974412799987 -120.185868,49.089828,2120.0894284800297 -120.186079,49.089711,2123.93600000004 -120.186974,49.089392,2136.94791168009 -120.187592,49.089147,2142.614400000026 -120.188014,49.088977,2147.43199999997 -120.18834,49.0887,2156.4153599999404 -120.188632,49.088348,2162.7359999999385 -120.188876,49.088274,2163.6507110400225 -120.189478,49.088061,2166.0447676799326 -120.189966,49.087954,2168.65354943997 -120.190193,49.08788,2169.9587999999885 -120.190454,49.087635,2172.0280000000084 -120.19086,49.08706,2175.303935999996 -120.191189,49.086529,2179.9086337599815 -120.191875,49.085433,2190.492699999962 -120.191729,49.084109,2188.168585439912 -120.191663,49.081906,2178.5470268800186 -120.191752,49.081624,2176.153599999898 + + + 0 + clampToGround + -120.191752,49.081624,2176.153599999898 -120.192079,49.080586,2164.4602902399984 -120.19186,49.079861,2158.2390784000586 -120.191345,49.079438,2150.5259999999826 -120.191028,49.079007,2146.9987801600078 -120.191039,49.078656,2144.4428486400243 -120.191017,49.078512,2142.8087161600442 -120.190973,49.078397,2141.30435824003 -120.191192,49.077694,2133.1056000000044 -120.191455,49.076933,2128.689405599963 -120.191948,49.076216,2128.527999999929 -120.191773,49.076094,2128.39480000001 -120.191543,49.075893,2129.2503999999603 -120.191663,49.075635,2131.6781752000593 -120.191915,49.075305,2134.586387999923 -120.192134,49.07491,2139.094399999911 -120.192266,49.074451,2140.965480640089 -120.192671,49.074731,2148.3918940800622 -120.192956,49.074917,2149.37131007997 -120.193149,49.074947,2150.4360000000147 -120.193306,49.074903,2152.200000000063 -120.193424,49.074705,2156.06527359991 -120.193387,49.074555,2159.231012800093 -120.193335,49.074481,2160.232820800091 -120.193334,49.074442,2161.0624211199492 -120.193551,49.074437,2164.3162004799137 -120.193597,49.074427,2165.233209759955 -120.193457,49.074188,2168.731008639925 -120.193564,49.074167,2171.1438035198403 -120.193779,49.074173,2173.6398196800183 -120.193994,49.074161,2175.508519360091 -120.193998,49.074047,2178.1468182399617 -120.193947,49.073943,2180.2088478399655 -120.194036,49.073885,2182.122614400025 -120.194223,49.073852,2184.1773273598965 -120.194624,49.073922,2187.124298239973 -120.194665,49.073851,2190.3059016000907 -120.194424,49.073587,2194.3268230398457 -120.194456,49.073558,2195.986307840106 -120.194686,49.073596,2199.8018124800174 -120.194704,49.073561,2201.567131520114 -120.194531,49.073396,2203.321806079853 -120.194516,49.073341,2204.7981804798274 -120.19468,49.073377,2208.10258880012 -120.194729,49.073359,2210.1123636797724 -120.194622,49.073137,2211.960853759861 -120.194566,49.072729,2214.1152851199467 -120.194987,49.072361,2221.8370561599636 -120.195153,49.072107,2225.990656320037 -120.195387,49.072163,2230.35879999989 -120.195437,49.071925,2232.8852279999487 -120.195802,49.07156,2243.463660799979 -120.196531,49.071897,2258.5732748798437 -120.197057,49.072474,2271.122000000009 -120.197819,49.072741,2278.445199999951 -120.198218,49.072998,2283.473658879941 -120.198709,49.073046,2292.4115676799097 + + + 0 + clampToGround + -120.198709,49.073046,2292.4115676799097 -120.198834,49.073058,2294.1585388800063 -120.199287,49.073253,2295.8332742399516 -120.199634,49.073234,2300.5183737600337 -120.200037,49.073374,2304.1015004799037 -120.200501,49.07364,2304.1994720001176 -120.201,49.073962,2310.347359999885 -120.202025,49.074624,2322.8064000000873 -120.203334,49.076016,2325.0015782399955 -120.205115,49.07655,2332.2600000000275 -120.206574,49.078223,2317.795199999955 -120.207411,49.078771,2311.4563777600306 -120.209449,49.07915,2271.1753839999305 -120.210522,49.079066,2267.4058220800325 -120.21081,49.079225,2264.967639999986 -120.211916,49.079497,2253.6562259199127 -120.212648,49.079838,2253.0975999999664 -120.213469,49.07996,2256.1288000000304 -120.214437,49.079864,2266.9561638399628 -120.217008,49.079122,2262.952808960004 + + + 0 + clampToGround + -120.217008,49.079122,2262.952808960004 -120.217185,49.079071,2263.069349600024 -120.218096,49.079114,2253.4592000000775 -120.219063,49.079108,2240.7260198400245 -120.219917,49.079193,2226.5032737601573 -120.220706,49.079092,2214.565617919782 -120.221584,49.078906,2205.2561356800416 -120.222137,49.078389,2207.531999999911 -120.222486,49.077835,2216.1623647999327 -120.223064,49.077265,2214.9076448001883 -120.223519,49.076626,2211.868761280134 -120.223877,49.076291,2214.2856000000256 -120.225259,49.075247,2210.9858900801323 -120.226072,49.074719,2213.4604665599577 -120.226779,49.074133,2210.092453120063 -120.227267,49.073649,2211.1044615999795 -120.227902,49.073201,2209.417446080063 -120.228438,49.072456,2212.045285120082 -120.229154,49.072056,2198.48071296005 -120.229674,49.07147,2194.560073599975 -120.230138,49.071348,2189.308142080048 -120.23191,49.07139,2159.1230080000023 -120.232626,49.071454,2161.2696761599914 -120.23378,49.071486,2162.7254272000264 -120.234159,49.071447,2159.310399999943 + + + 0 + clampToGround + -120.234159,49.071447,2159.310399999943 -120.234951,49.071364,2151.7312000000284 -120.235772,49.071326,2150.4860108800044 -120.236073,49.071023,2145.185599999948 -120.236301,49.07025,2133.6492000000435 -120.236228,49.069515,2129.4236832000456 -120.236894,49.069073,2121.6887999999744 -120.237642,49.068823,2111.4861132800906 -120.237749,49.068728,2109.715189760064 -120.238846,49.068391,2097.724000000022 -120.24013,49.068226,2090.7128352000095 -120.242927,49.068146,2080.4860000000335 -120.243009,49.068178,2079.8451999999634 -120.243732,49.067965,2076.8362752000535 -120.243846,49.067912,2075.9002060799567 -120.244855,49.067374,2068.738820799996 -120.245855,49.066735,2065.4536239999984 -120.246001,49.066708,2065.1179686399882 -120.247009,49.066399,2061.8124593599673 -120.248213,49.066117,2053.5391999999465 -120.249367,49.06584,2046.044073600008 -120.25018,49.065733,2050.4539999999724 -120.251221,49.065456,2047.500616960024 -120.252115,49.065051,2047.4868000000545 -120.253262,49.064753,2051.060639679964 -120.25429,49.064183,2056.37960000001 + + + 0 + clampToGround + -120.25429,49.064183,2056.37960000001 -120.254636,49.063991,2060.62309695995 -120.255579,49.063597,2068.2161124799786 -120.256205,49.063176,2073.4940000000465 -120.256726,49.062755,2083.0823151999343 -120.25714,49.062547,2085.4075999998977 -120.257319,49.062457,2085.9914646399966 -120.257636,49.062265,2088.8641984000624 -120.257897,49.062105,2091.2839223999417 -120.258303,49.061615,2099.371999999976 -120.25849,49.061087,2101.5606448000176 -120.258905,49.060469,2101.8406544000322 -120.258905,49.0604,2102.5570399999965 -120.259328,49.059995,2101.8505824000254 -120.259824,49.059313,2093.36101952009 -120.260092,49.058828,2089.821800960002 -120.260759,49.058184,2078.3889382400725 -120.260978,49.057614,2075.002672640081 -120.261165,49.057502,2072.223999999922 -120.261523,49.057427,2068.077200000055 -120.262352,49.057347,2061.735999999974 -120.262775,49.057097,2060.573707999953 -120.26345,49.056793,2053.868367999948 -120.264035,49.056335,2049.2780439998755 -120.264231,49.05601,2047.1656000000628 -120.264336,49.055951,2046.2907999999939 -120.26445,49.055813,2046.3777360000258 -120.264792,49.055322,2048.3856089600276 -120.264865,49.055051,2047.8496295999978 -120.265027,49.05447,2042.2618576000148 -120.265036,49.054171,2036.346734719897 -120.26497,49.054097,2034.244399999968 -120.264897,49.054059,2033.286800000058 -120.264491,49.053585,2022.646554399884 -120.264523,49.053532,2020.382141439904 -120.264588,49.053521,2019.5656499201111 -120.26484,49.053505,2017.8560640000305 -120.265117,49.053212,2005.8999206401315 -120.265214,49.05317,2005.7087808000588 -120.26558,49.052509,1984.4250976000328 -120.266223,49.051837,1970.2851939198492 -120.266475,49.051464,1960.9578240001017 -120.266647,49.050965,1950.1404000000487 -120.267,49.050549,1932.7592799998026 + + + 0 + clampToGround + -120.267,49.050549,1932.7592799998026 -120.267061,49.050478,1931.188847679905 -120.268531,49.049761,1922.4625212798035 -120.268696,49.049636,1922.7817164801281 -120.268669,49.049284,1917.019943680089 -120.268377,49.048881,1907.9494710400515 -120.26826,49.048501,1898.1692000000128 -120.268499,49.047868,1885.3857785601572 -120.268611,49.047064,1874.0106835201072 -120.269131,49.046438,1857.0020988800595 -120.270352,49.046323,1842.9055596799749 -120.271552,49.046682,1825.773626879802 -120.27169,49.046807,1830.3488735998685 -120.271775,49.046817,1828.346135999627 -120.273458,49.047451,1820.1320000000426 -120.273437,49.047506,1824.1766777598682 -120.273601,49.047503,1820.4135862399487 -120.273989,49.047538,1809.4924000002052 -120.275656,49.048039,1798.6559513601912 -120.276139,49.048133,1791.6987830399858 -120.276219,49.048233,1794.4430198402024 -120.276967,49.048394,1786.5246681601134 -120.27752,49.04846,1777.9031680000094 -120.27864,49.048526,1764.5832000001842 -120.278778,49.048609,1768.6691999999211 -120.27881,49.048665,1771.8109040000304 -120.279011,49.048588,1766.1094252801558 -120.279075,49.048547,1763.2517639999246 -120.279165,49.04854,1762.3995360001468 -120.27933,49.048394,1751.0387808000946 -120.279569,49.048293,1743.2828923200916 -120.279818,49.048251,1739.7294038400005 -120.280031,49.048286,1740.8013167998693 -120.280169,49.048279,1740.470584800049 -120.280891,49.048307,1738.4060804801231 -120.281109,49.048324,1737.350544640058 -120.281146,49.048303,1735.1035465596565 -120.281241,49.048341,1735.6876230401615 -120.281321,49.048324,1732.8705644799438 -120.281661,49.048331,1727.9387999998494 -120.282096,49.048414,1728.7840422401366 -120.282574,49.048275,1712.1571999998937 -120.28345,49.048334,1711.0455999997941 -120.283686,49.048304,1709.2235302400745 + + + 0 + clampToGround + -120.283686,49.048304,1709.2235302400745 -120.283715,49.0483,1708.9011199998708 -120.285648,49.048153,1683.9821612799888 -120.28577,49.048133,1681.8794992000023 -120.285823,49.048115,1680.4850824002006 -120.286747,49.048021,1667.8004470400078 -120.286864,49.048039,1669.5697203200355 -120.287777,49.047882,1665.3796000000227 -120.288743,49.047555,1649.2131896000965 -120.289019,49.047541,1648.180800000072 -120.289635,49.047437,1641.3759848000477 -120.290161,49.047499,1636.797026880102 -120.290771,49.047371,1627.1159999999081 -120.291817,49.047398,1617.071600000096 -120.292619,49.047478,1611.4303999999254 -120.292985,49.047409,1607.8072104000616 -120.293245,49.047214,1601.3859999998795 -120.293527,49.047186,1602.6057702401176 -120.293649,49.047196,1604.003599999989 -120.294015,49.047096,1600.547200000145 -120.294105,49.047033,1597.8255999999565 -120.29419,49.047012,1597.0251136000984 -120.294487,49.046748,1588.5272000000623 -120.294997,49.046601,1583.6468000001098 -120.295077,49.046601,1583.5704699200894 -120.295486,49.046518,1579.7643859199666 -120.295693,49.046438,1575.2836000000714 -120.295767,49.046389,1572.7203999999426 -120.296335,49.046407,1568.8696976000622 -120.296829,49.046619,1566.9770830399514 -120.297137,49.046675,1562.7347959998951 -120.298114,49.046734,1543.8872000000529 -120.298135,49.046793,1545.2083999999913 -120.298214,49.0468,1543.9879999999007 -120.29866,49.047071,1543.7804288001034 -120.300301,49.047468,1524.4492614399953 -120.300981,49.04752,1513.8548000001383 -120.301235,49.047569,1508.5299863999326 -120.301549,49.047711,1505.3780000001107 -120.301957,49.048011,1508.2224500800658 -120.302945,49.048387,1504.1716000000665 -120.303131,49.048411,1501.1654180801665 -120.303667,49.048641,1496.517999999974 -120.304118,49.048915,1492.2190623999668 -120.304766,49.049183,1482.5555331199716 -120.305376,49.049323,1474.399600000086 -120.306035,49.049716,1475.7262624000073 -120.306247,49.04989,1475.8466463999875 -120.307213,49.050255,1465.7770447999496 -120.308095,49.050784,1459.9048000001253 -120.30863,49.051294,1457.519200000208 + + + 0 + clampToGround + -120.30863,49.051294,1457.519200000208 -120.308737,49.051396,1456.3476000001053 -120.309942,49.052002,1444.0686732799186 -120.309979,49.052065,1446.105579199984 -120.31084,49.052402,1443.2436672000613 -120.311631,49.052597,1437.4411627197915 -120.313951,49.052844,1415.6894937599582 -120.314848,49.052973,1404.8093164801044 -120.316584,49.053554,1393.5391999998967 -120.317359,49.053536,1381.7226899200728 -120.317837,49.05395,1380.5209040000573 -120.318888,49.054444,1365.0239897599233 -120.318904,49.054493,1365.927818239917 -120.318957,49.054476,1363.7577094401101 -120.319812,49.054757,1353.6197414400501 -120.320077,49.055015,1351.9152624000294 -120.320131,49.055005,1351.1146223999494 -120.320709,49.055394,1340.4862275200962 -120.320927,49.055843,1336.9998654400624 -120.320991,49.055916,1335.72308223986 -120.321325,49.056226,1329.9392719999987 -120.322132,49.056702,1326.8336000000327 -120.322429,49.05681,1326.9593103999796 -120.323804,49.057029,1319.4703507200384 -120.324909,49.05728,1305.0086591998233 -120.326263,49.057419,1291.779046879958 -120.327399,49.057656,1280.0713932799474 -120.328253,49.058143,1271.20584095996 -120.328731,49.058978,1260.923485439869 -120.328853,49.059983,1246.3384929601307 -120.328588,49.060863,1233.0035142400773 -120.328765,49.06147,1223.7255359999956 + + + 0 + clampToGround + -120.328765,49.06147,1223.7255359999956 -120.328779,49.061517,1222.7246985599581 -120.329273,49.062011,1216.4244000000272 -120.330213,49.062523,1211.5377089600142 -120.33128,49.062791,1206.8282592000357 -120.331688,49.063493,1198.3965446399864 -120.3324,49.064258,1192.0935999998997 -120.332538,49.064437,1188.9362675200205 -120.332804,49.064534,1188.1217382399836 -120.333067,49.06464,1190.9976095999546 + + + 0 + clampToGround + -120.333067,49.06464,1190.9976095999546 -120.333371,49.064241,1188.4772000000344 -120.334011,49.06364,1195.0331552000036 -120.334411,49.063469,1195.1027865599772 -120.334992,49.063306,1192.9072678399907 -120.335229,49.063174,1194.184020799922 -120.336457,49.062198,1192.584255679972 -120.337161,49.061778,1198.9011183998584 -120.337561,49.061533,1203.6963999999125 -120.337728,49.061391,1204.4775999999865 -120.337834,49.06118,1203.1488000000081 -120.338096,49.060155,1195.4588895999927 -120.338258,49.059814,1195.9408000000033 -120.338379,49.059635,1195.7744431999836 -120.338513,49.059441,1196.0080203200012 -120.339218,49.058476,1202.184800000009 -120.339317,49.058334,1202.5411999999787 + + + 0 + clampToGround + -120.339317,49.058334,1202.5411999999787 -120.339334,49.058309,1202.637229759978 -120.339515,49.058149,1203.2540000000208 -120.340138,49.057829,1205.1779999999649 -120.340502,49.057636,1207.104000000014 -120.340704,49.057479,1208.5979987200249 -120.340973,49.057145,1209.3645784000219 -120.341603,49.056235,1211.770800000023 -120.342198,49.055294,1211.0872000000136 -120.342512,49.054893,1209.229599999999 -120.343139,49.053872,1213.6858713599656 -120.343245,49.053758,1214.6636831999804 -120.34344,49.053564,1215.0 -120.344597,49.052847,1213.8197212799607 -120.344863,49.052664,1214.6120294400425 -120.34493,49.052506,1214.528313600043 -120.34499,49.052341,1214.948606399978 -120.345015,49.052084,1215.1891296000451 -120.345029,49.051874,1215.2867241599672 -120.345325,49.051581,1218.0011160000395 -120.345405,49.051524,1219.0979135999949 -120.345505,49.051533,1220.390335200032 -120.345762,49.051736,1226.7087999998512 -120.346105,49.051937,1234.7887791999037 -120.346276,49.051938,1236.7249715200319 -120.34643,49.051878,1237.853001599998 -120.347097,49.05124,1251.9224576000925 -120.347093,49.051204,1252.4904742400436 + + + 0 + clampToGround + -120.347093,49.051204,1252.4904742400436 -120.347025,49.050518,1250.4944080000778 -120.347094,49.049955,1257.0927999999822 -120.347278,49.04943,1261.1321248000697 -120.347693,49.048508,1272.4451097601977 -120.348198,49.047869,1284.3892035199194 -120.349118,49.047018,1302.7807859201364 -120.349605,49.046001,1316.3263191999224 -120.34975,49.045291,1316.5047600000037 -120.349952,49.044854,1319.3044236800508 -120.349949,49.044639,1317.4092131198847 -120.350061,49.04424,1320.3324511998771 -120.350053,49.044217,1320.285654080053 + + + 0 + clampToGround + -120.350053,49.044217,1320.285654080053 -120.350061,49.04424,1320.3324511998771 -120.349949,49.044639,1317.4092131198847 -120.349952,49.044854,1319.3044236800508 -120.350201,49.044463,1322.2471364799637 -120.34993,49.043529,1328.2796000000167 -120.350291,49.04224,1356.042953599979 -120.350255,49.041696,1364.2409408000367 -120.350832,49.04179,1378.8482623999273 -120.351193,49.042121,1383.2981748798043 -120.351736,49.042663,1395.3557305601257 -120.35179,49.042706,1396.502540799759 -120.351829,49.042663,1398.4270518398741 -120.352175,49.042305,1412.2826800000685 -120.352222,49.04227,1413.650924799929 -120.352275,49.042288,1414.7113920001389 -120.352641,49.042545,1422.473431200148 -120.352691,49.042559,1423.746846239821 -120.352741,49.042532,1425.0579475198606 -120.352871,49.042322,1432.0441049597207 -120.353214,49.042074,1450.1044345597832 -120.353339,49.040343,1483.7904100801893 -120.353452,49.040099,1493.7772000002087 -120.354569,49.041063,1517.7360628799315 -120.354875,49.040322,1541.0072400001904 -120.355091,49.040465,1545.992397600133 -120.355217,49.039164,1575.2325004798836 -120.355474,49.038996,1581.4224000000104 -120.35556,49.038962,1583.6308223999142 -120.35564,49.038992,1585.7920895998568 -120.356679,49.03998,1607.4823999999942 -120.357309,49.040535,1620.0268824000177 -120.357382,49.040536,1622.64110592006 -120.357436,49.040492,1624.2911155202478 -120.357906,49.040063,1635.412690880004 -120.35805,49.039614,1640.2896000001015 -120.358021,49.03857,1640.4899999998877 -120.358183,49.038428,1640.6412409599995 -120.359699,49.038026,1655.6421900801743 -120.360446,49.037933,1662.883190720005 + + + 0 + clampToGround + -120.360446,49.037933,1662.883190720005 -120.36072,49.037899,1665.4452512000973 -120.362164,49.037627,1681.1626291201324 -120.362777,49.037425,1681.8336439999212 -120.363228,49.03739,1685.0736000001052 -120.364149,49.037544,1693.3619999998914 -120.364888,49.03752,1696.2447999999981 -120.366332,49.037437,1699.8879999999572 -120.366693,49.037284,1698.2805164799806 -120.367378,49.036881,1695.647200000023 -120.368172,49.036881,1699.2778905600142 -120.369165,49.036881,1702.765599999987 -120.369706,49.036609,1704.1491999999826 -120.370236,49.036193,1709.54880000006 -120.370825,49.035876,1713.3707999999806 -120.371601,49.035746,1721.0197724800187 -120.372113,49.03579,1728.2206783999557 -120.372521,49.035911,1737.3668622400005 -120.373267,49.036086,1745.911187519926 -120.37391,49.036337,1754.519796800087 -120.37474,49.036633,1763.10311360004 -120.375408,49.036621,1764.1297414399423 -120.375769,49.036834,1766.4838316799676 -120.376725,49.037071,1766.366799999972 -120.378259,49.037059,1767.2840835199936 -120.379576,49.037165,1775.7471616000214 -120.38054,49.037338,1784.2550591999332 + + + 0 + clampToGround + -120.38054,49.037338,1784.2550591999332 -120.381489,49.037508,1793.201600000064 -120.382968,49.03791,1802.364870399931 -120.383636,49.038135,1806.690374399939 -120.384177,49.038147,1806.9752000000465 -120.385368,49.038088,1803.4672000000273 -120.386812,49.03836,1809.2879999999707 -120.38837,49.038736,1808.0311999999822 -120.389399,49.038972,1809.110149120025 -120.391059,49.039375,1812.1876000000016 -120.391726,49.039434,1811.9247999999998 -120.392394,49.039505,1811.8719999999762 -120.392935,49.039599,1814.2352000000074 -120.393873,49.039812,1814.4071999999733 -120.394938,49.040179,1816.8019398400354 -120.395624,49.040404,1817.8176000000335 -120.39658,49.040569,1818.111900799982 -120.396959,49.040699,1819.5163999999872 -120.397248,49.040936,1821.444101120023 -120.397663,49.040995,1821.9912824000116 -120.398204,49.041007,1821.9162931199867 -120.398535,49.04113,1822.154632000022 + + + 0 + clampToGround + -120.398535,49.04113,1822.154632000022 -120.398871,49.041255,1823.035999999997 -120.399305,49.041291,1822.472104800008 -120.399756,49.041397,1824.3856492800355 -120.400315,49.041314,1827.6926736000373 -120.400983,49.041267,1831.5247745599515 -120.401524,49.041125,1831.7363999999989 -120.401867,49.040829,1831.419898559981 -120.402119,49.040605,1831.5009447999903 -120.402588,49.040392,1833.8582681599573 -120.403328,49.040072,1835.2400000000143 -120.403779,49.039848,1836.6417446400023 -120.404122,49.039777,1837.1654902400032 -120.404501,49.039812,1837.6463999999837 -120.404862,49.039954,1838.5854700800178 -120.405475,49.040297,1841.3259320000407 -120.405692,49.040285,1842.5908287999753 -120.405908,49.040132,1842.1957324799607 -120.406396,49.040132,1847.490234880006 -120.406811,49.040155,1850.4059367999969 -120.40719,49.040309,1852.983361600002 -120.407785,49.04064,1857.6600000000528 -120.408074,49.040652,1859.71745408001 -120.408651,49.040735,1862.4356000000075 -120.40903,49.040889,1864.4007999999997 -120.409283,49.041232,1866.6881382400013 -120.409391,49.041291,1866.772198240006 -120.409625,49.041279,1866.3472600000098 -120.410149,49.041172,1866.9748000000368 -120.410329,49.041137,1867.1863999999935 -120.410528,49.041255,1868.035999999997 -120.410618,49.041468,1869.5696000000132 -120.410979,49.041728,1871.0329875199986 -120.411466,49.041788,1869.8764556799379 -120.411791,49.041977,1871.021200000063 -120.412783,49.042509,1872.1289908800363 -120.413757,49.042852,1877.4536665600535 -120.414191,49.043029,1880.62597455996 -120.414696,49.043443,1885.7108691200192 -120.415839,49.044554,1890.3820457599809 + + + 0 + clampToGround + -120.415839,49.044554,1890.3820457599809 -120.416374,49.045075,1893.053599999983 -120.416897,49.04517,1891.6782703999884 -120.417366,49.045418,1893.7707155199842 -120.418196,49.045667,1896.8076467200196 -120.419098,49.045797,1896.6076000000035 -120.419568,49.045738,1896.6057446399961 -120.420163,49.045762,1897.7160000000613 -120.42074,49.045809,1899.5619999999717 -120.421444,49.045832,1898.9817523199617 -120.422184,49.045702,1898.5815999999845 -120.422617,49.045631,1897.8503999999919 -120.423014,49.045454,1895.4184000000143 -120.423366,49.04531,1893.1143999999995 -120.423682,49.045347,1892.498399999997 -120.424151,49.045454,1893.268799999996 -120.424475,49.045371,1893.8181160000274 -120.424945,49.045442,1896.575599999992 -120.425594,49.045619,1897.2284000000075 -120.426641,49.045892,1899.3495148800257 -120.42711,49.046093,1903.1267999999893 -120.427489,49.046436,1907.0836838399541 -120.42821,49.046897,1911.8292000000047 -120.428589,49.047181,1913.056987360003 -120.42904,49.047606,1912.95918080001 -120.429492,49.047784,1918.7743999999452 -120.429811,49.047894,1924.4986793600003 + + + 0 + clampToGround + -120.429811,49.047894,1924.4986793600003 -120.430177,49.04802,1928.2532000000133 -120.430448,49.04821,1930.6679999999722 -120.430664,49.0486,1934.2947839999795 -120.43126,49.049238,1937.79279999999 -120.431621,49.049759,1940.0 -120.432108,49.050114,1941.2416435200007 -120.432559,49.050397,1943.2123999999917 -120.433064,49.050539,1945.0018118400003 -120.433696,49.050681,1944.715991040008 -120.434291,49.050965,1948.0522376000183 -120.435031,49.051674,1951.997053760001 -120.435753,49.051887,1951.7932000000023 -120.43633,49.052112,1954.1222784000056 -120.436565,49.052396,1956.251200000012 -120.436835,49.052526,1956.5811999999946 -120.43755,49.051612,1955.035424000002 -120.437722,49.051486,1955.5197996799939 -120.438118,49.051242,1956.1188742400038 -120.438248,49.051178,1956.5259737600038 -120.438371,49.05114,1957.0316000000253 -120.438531,49.051122,1957.672399999993 -120.439073,49.05115,1957.6744160000083 -120.439345,49.051144,1958.2107871999997 -120.440005,49.051245,1959.0899999999754 -120.44025,49.051242,1960.0151999999928 -120.440425,49.051223,1961.2353160000232 -120.441944,49.050988,1976.0976000001538 -120.441963,49.050983,1976.3311918399513 + + + 0 + clampToGround + -120.441963,49.050983,1976.3311918399513 -120.442336,49.050893,1979.7991820799562 -120.44264,49.050774,1982.252454400026 -120.442974,49.050672,1985.7360000000663 -120.443255,49.050606,1988.2523775999505 -120.443767,49.050544,1992.8266540799098 -120.443957,49.050875,1991.1156599999647 -120.444272,49.051127,1993.4379097599665 -120.444413,49.051427,1993.3859999999938 -120.444636,49.051564,1995.062876160042 -120.444697,49.051637,1995.0383025600613 -120.44495,49.05203,1993.5428800000288 -120.445217,49.052321,1990.0100105600156 -120.445378,49.052724,1984.2176000001064 -120.445496,49.052833,1984.3898227200818 -120.445684,49.052935,1986.3097184000253 -120.445742,49.053043,1986.177661759878 -120.445756,49.053201,1984.672510720031 -120.445984,49.053443,1985.165860479931 -120.446008,49.053659,1983.8864000000667 -120.445977,49.053955,1983.9045064000024 -120.446037,49.054111,1985.2125332800267 -120.44598,49.054527,1984.4270784000562 -120.446049,49.054893,1983.4606692800116 -120.446074,49.055482,1978.5625772799988 -120.446051,49.055941,1972.1006766400221 + + + 0 + clampToGround + -120.446051,49.055941,1972.1006766400221 -120.446048,49.055996,1971.3362483200185 -120.446095,49.056278,1968.1744464000108 -120.446218,49.056506,1966.3793683200138 -120.446145,49.057071,1964.1220000000048 -120.446047,49.057346,1964.3176444799974 -120.445893,49.057488,1964.9228793599952 -120.445604,49.057597,1966.2357004799983 -120.445266,49.05763,1966.4256000000248 -120.445218,49.05769,1966.4687999999924 -120.445308,49.057799,1967.0 -120.4453,49.057932,1967.0 -120.445344,49.058009,1967.0 -120.445457,49.058107,1967.2509089599953 -120.445859,49.058643,1968.022399999992 -120.445895,49.059017,1969.700400000005 -120.445788,49.059198,1971.9223910400995 -120.445554,49.059347,1975.9624355200142 -120.444996,49.059831,1985.5607609600424 -120.444996,49.060005,1988.0223775999873 -120.444881,49.060199,1989.925117279987 -120.444886,49.0603,1991.0751359999986 -120.444961,49.060422,1992.2118086400133 -120.445142,49.060503,1993.243199999958 -120.445311,49.060735,1995.2496616000035 -120.445572,49.060993,1997.6992281600399 -120.446036,49.061221,2001.707600000083 -120.446227,49.061395,2005.5051999999387 -120.446444,49.061731,2010.9264000000253 -120.446479,49.061902,2013.3888000000497 -120.446377,49.062169,2016.382399999964 -120.446368,49.062421,2017.6404000000261 -120.446426,49.062557,2018.3936294400041 -120.446486,49.062621,2019.0897715199535 + + + 0 + clampToGround + -120.446486,49.062621,2019.0897715199535 -120.446714,49.062864,2022.961599999983 -120.446792,49.0631,2025.2224000000049 -120.446943,49.063359,2028.0815195200285 -120.446975,49.063539,2029.0418439999435 -120.447099,49.063829,2031.1056401599199 -120.447519,49.064272,2037.37839999998 -120.448258,49.064588,2041.3995123199622 -120.448798,49.064994,2043.0470675199686 -120.448873,49.065179,2043.8284000000656 -120.449418,49.065834,2048.6143999999013 -120.449803,49.066057,2052.690952160032 -120.450365,49.066683,2058.723726400024 -120.450591,49.067199,2065.3218652800574 -120.450626,49.067463,2068.6147999999584 -120.450976,49.068501,2080.0 -120.451023,49.068695,2080.2062056000027 -120.451139,49.069169,2085.126443360017 -120.451312,49.070229,2096.870993919984 -120.451567,49.070713,2102.7794321599526 + + + 0 + clampToGround + -120.451567,49.070713,2102.7794321599526 -120.451599,49.070774,2103.6804329600177 -120.451759,49.071313,2111.1183999999935 -120.451976,49.071638,2114.1122764800134 -120.45194,49.072058,2119.603199999929 -120.452002,49.072374,2124.5060140800147 -120.452724,49.07362,2149.1661951999668 -120.45306,49.073987,2153.695097600034 -120.453241,49.074309,2156.5674435199294 -120.453994,49.07532,2169.398400000033 -120.454419,49.076042,2174.685599999997 -120.454485,49.076615,2180.0311559999127 -120.454733,49.077128,2185.900360960013 -120.454766,49.077312,2188.7053363199516 + + + 0 + clampToGround + -120.454766,49.077312,2188.7053363199516 -120.454923,49.078192,2197.755678720035 -120.455003,49.078545,2201.2834295999774 -120.455075,49.07866,2202.527999999992 -120.455631,49.079449,2201.9819254400268 -120.456658,49.080199,2212.4736000000457 -120.457207,49.080782,2215.130799999998 -120.457472,49.080935,2210.678585600103 -120.457548,49.081019,2208.0410009598913 -120.457546,49.081348,2210.8536473602194 -120.457941,49.081793,2204.343240479807 -120.458189,49.082172,2203.339656319875 -120.458311,49.082576,2198.44640256008 -120.45838,49.083346,2193.59507839985 -120.45857,49.084325,2187.6060000001357 -120.458791,49.084945,2180.5103999999083 -120.458884,49.085058,2177.6880000000765 -120.458973,49.085322,2173.817611519996 -120.459309,49.085658,2166.1879999998837 -120.459362,49.085817,2163.8984000000373 -120.459623,49.086104,2162.961888640054 -120.459869,49.086188,2161.84920000004 + + + 0 + clampToGround + -120.459869,49.086188,2161.84920000004 -120.45992,49.086206,2161.5143999999646 -120.46073,49.086327,2147.9615183999713 -120.461624,49.086501,2136.0979929599566 -120.462469,49.086524,2123.932682239969 -120.462628,49.086562,2122.2728000000825 -120.462803,49.086678,2121.5347139200785 -120.46323,49.086768,2117.871494400013 -120.463478,49.086768,2118.775127039955 -120.463916,49.086713,2119.981239040115 -120.464762,49.086742,2122.7147641600423 -120.465266,49.086672,2120.2831859200037 -120.4658,49.08669,2121.196159999859 -120.465982,49.086691,2119.1793670401444 -120.466158,49.086691,2116.865626240185 -120.466434,49.086772,2118.146400000148 -120.467144,49.086895,2123.8248991998785 -120.468152,49.086956,2117.135869439989 -120.468342,49.08702,2118.0489408000776 -120.468589,49.087145,2121.9632136000805 -120.468932,49.087199,2122.481250559917 -120.46956,49.087245,2125.6156640002355 -120.469711,49.087279,2129.0319732801872 -120.469961,49.087239,2126.207999999765 -120.470235,49.087292,2128.3319999998334 -120.470624,49.087322,2127.3320000000176 -120.470967,49.087287,2123.0941524802133 -120.471099,49.087264,2121.4260723198663 -120.471333,49.087261,2119.747512479872 -120.471506,49.087296,2119.7168230400434 -120.47167,49.087356,2120.6100415998967 -120.471888,49.087472,2120.3766348796867 -120.472065,49.087629,2123.9893007999676 -120.472432,49.087963,2126.2044000001383 -120.472529,49.088199,2131.1200000003096 -120.472754,49.088486,2130.556954240233 -120.473228,49.089028,2131.1609932798196 -120.473709,49.089275,2129.885436000031 -120.473951,49.089411,2130.928521439948 -120.474061,49.089323,2126.420489119931 -120.474673,49.089568,2127.4559814400354 -120.474892,49.089631,2127.621281919926 -120.475883,49.089642,2123.8363625599272 -120.476304,49.089691,2122.421250560105 -120.477145,49.089723,2114.150421600067 -120.477632,49.089803,2114.507411840168 -120.477698,49.089777,2112.481835839861 -120.478356,49.089932,2111.1639756798677 -120.478741,49.090024,2112.955998719987 -120.47931,49.090128,2116.1239999999925 -120.479701,49.090115,2116.904740799901 -120.480248,49.090053,2118.8843999999235 -120.480427,49.090061,2119.533569119947 -120.48091,49.089889,2115.8102207999405 -120.481359,49.089874,2118.2976073600507 -120.481953,49.089769,2116.7147999999033 -120.482727,49.089661,2117.3281782399567 -120.482987,49.089582,2115.3249846399917 -120.483222,49.089609,2117.004867840097 -120.483878,49.089573,2118.621083520074 -120.484117,49.089586,2120.316483519894 -120.484839,49.089491,2118.508400000079 -120.485331,49.08946,2117.2564000000652 -120.48551,49.089483,2117.27440000002 -120.485661,49.089519,2117.2861153601316 -120.485808,49.089627,2118.842736639948 + + + 0 + clampToGround + -120.485808,49.089627,2118.842736639948 -120.485979,49.089751,2119.93974431999 -120.486197,49.089902,2119.570171519976 -120.486436,49.090234,2121.4174579200794 -120.486607,49.090607,2119.673999999912 -120.486762,49.0909,2115.914304000038 -120.486842,49.091497,2106.5249260800574 -120.48687,49.091938,2101.3251648000223 -120.48678,49.092268,2096.9328000000555 -120.486717,49.092762,2092.7046921600067 -120.48639,49.093226,2088.928908799975 -120.486363,49.093357,2086.667200000056 -120.486456,49.093516,2083.553123839971 -120.486757,49.093727,2078.9623265600094 -120.487116,49.094089,2070.162800000056 -120.487682,49.094536,2062.6895999999524 -120.488997,49.094822,2052.5485993600173 -120.489258,49.09482,2050.6202751999076 -120.489643,49.094773,2045.7174654400508 -120.490174,49.094657,2040.720278719993 -120.490377,49.094652,2038.7365996801204 -120.49061,49.094673,2036.1527999999507 + + + 0 + clampToGround + -120.49061,49.094673,2036.1527999999507 -120.49078,49.094689,2034.0504000000406 -120.491072,49.094767,2032.366903039918 -120.492187,49.09482,2024.9450335999327 -120.492546,49.094898,2021.1296083199281 -120.492891,49.095193,2018.638800000016 -120.493838,49.095703,2010.7587574400459 -120.494294,49.096106,2008.5795654400288 -120.494561,49.096486,2004.5457078399616 -120.494661,49.096714,2000.0296000001172 -120.494701,49.096993,1995.3055999999096 -120.494803,49.097097,1993.967892639944 -120.494905,49.097471,1991.1893903999471 -120.494989,49.097618,1988.7959779199327 -120.495657,49.098459,1977.2707835200847 -120.495892,49.098696,1977.3887999999474 -120.496258,49.098869,1976.3120000000526 -120.49652,49.09889,1972.665887999867 -120.496724,49.098943,1970.331599999954 -120.497237,49.099151,1966.389399520001 -120.497645,49.099163,1960.8063999998385 -120.498313,49.099279,1955.1236000001277 -120.498665,49.099279,1951.7124536000372 -120.499179,49.099192,1946.5052000000758 -120.499353,49.099198,1944.7772000000389 -120.500145,49.099334,1938.5236527999132 -120.500711,49.099334,1930.2436969600462 -120.500844,49.099354,1928.5287999999405 -120.501039,49.099444,1927.2867999998534 -120.501734,49.09967,1922.6688288000998 -120.502207,49.099771,1918.0971999999665 -120.503114,49.099812,1904.955201280059 -120.503668,49.099942,1897.7699622398793 -120.503834,49.099913,1893.8709116800599 -120.503939,49.099829,1890.8159999999589 -120.504031,49.099832,1889.5344000000227 -120.504459,49.099959,1884.347600000024 -120.504619,49.100058,1882.3551900799268 -120.504818,49.100116,1878.8583999999967 -120.504907,49.100102,1877.3067999999494 -120.505009,49.100035,1875.7443999999896 -120.505142,49.099792,1873.2063999999104 -120.505301,49.099739,1870.7310494399574 -120.505633,49.099774,1866.3804950399453 -120.50577,49.099751,1864.6512624000231 -120.505836,49.099682,1863.6733900799798 -120.505823,49.099589,1863.134558879953 -120.505894,49.099534,1861.9191878400159 -120.506018,49.099516,1860.3495475200364 -120.506155,49.099537,1858.5958455998775 -120.506575,49.099696,1853.8012000000328 -120.506727,49.099722,1852.564000000014 -120.506974,49.099699,1850.7264000000093 -120.507496,49.09934,1844.1542143999359 -120.508691,49.098618,1829.8364000000377 -120.508757,49.098602,1829.2952265599831 + + + 0 + clampToGround + -120.508757,49.098602,1829.2952265599831 -120.509285,49.09847,1824.115999999945 -120.50974,49.09845,1820.1320000000426 -120.510306,49.098508,1815.851200000019 -120.510519,49.098601,1813.4535999999844 -120.510913,49.099015,1807.1226071999504 -120.511014,49.099064,1804.8572121599284 -120.511168,49.099067,1801.7485177600252 -120.511453,49.098824,1795.6123999999468 -120.511599,49.09858,1792.1332831999234 -120.51155,49.098152,1791.5930239999996 -120.511621,49.097853,1787.3946390399412 -120.511576,49.097604,1782.804195839859 -120.511601,49.097509,1780.164740640072 -120.511647,49.097474,1778.541026880004 -120.511984,49.097477,1771.708399999977 -120.512231,49.09754,1768.7060000000492 -120.513054,49.097922,1759.0690924800415 -120.513471,49.098485,1751.7558024001105 -120.513816,49.098777,1743.8744947198604 -120.51409,49.099316,1740.13482240012 -120.51475,49.100026,1724.9255999998406 -120.514843,49.100183,1720.321200000006 -120.515144,49.100472,1711.372065279881 -120.515374,49.10062,1708.2268352000474 -120.51548,49.100646,1707.3069632000347 -120.51556,49.100725,1706.326000000103 -120.515613,49.101014,1701.4049027199926 -120.515838,49.101237,1694.6836000000194 -120.515803,49.101417,1691.7013489600026 -120.515847,49.101724,1685.477109760104 -120.516139,49.101866,1681.2832470400012 -120.516392,49.10193,1679.175564799997 -120.516544,49.101933,1677.5356121599948 -120.516684,49.101918,1676.4404595200642 -120.517281,49.101652,1674.047600000044 -120.518501,49.101386,1658.602277440004 -120.518848,49.101281,1655.218399999939 -120.519171,49.101133,1652.8099707200195 -120.519715,49.100762,1650.3087999999525 -120.520415,49.100165,1645.2999999999688 -120.52122,49.09936,1628.5768319999681 -120.521273,49.099154,1625.606400000021 -120.52141,49.098977,1621.4339999999397 -120.521599,49.098883,1618.3600000000529 -120.521986,49.098931,1618.629079360004 -120.52222,49.098917,1618.5051904000043 -120.522623,49.098688,1611.3288000001098 -120.522919,49.098659,1608.2747518399283 -120.524636,49.098924,1594.6294963198645 -120.525402,49.099128,1587.3720000000171 -120.525995,49.099247,1582.1064856000312 -120.527527,49.099441,1562.4284052798566 -120.527952,49.099629,1560.5587116800161 -120.528631,49.099846,1553.689599999893 + + + 0 + clampToGround + -120.528631,49.099846,1553.689599999893 -120.52905,49.09998,1549.1342400000885 -120.530067,49.100362,1534.4339999998601 -120.530594,49.100611,1528.5056000001302 -120.531149,49.100782,1521.7516000000796 -120.531284,49.100843,1519.804059519974 -120.531642,49.101153,1515.8964179198551 -120.531983,49.101362,1511.334564160055 -120.532412,49.101542,1505.3992000000612 -120.532614,49.101683,1502.9198684800144 -120.532668,49.101775,1500.994527999933 -120.532727,49.101872,1498.8047257600542 -120.532815,49.10217,1490.8888079999829 -120.532713,49.102477,1485.6434910399728 -120.532559,49.102744,1483.3594278398443 -120.532502,49.103106,1476.4954924802519 -120.532515,49.103418,1468.060459199939 -120.532342,49.103802,1463.2115206399553 -120.53189,49.104384,1453.916799999819 -120.531806,49.104645,1448.5742447999821 -120.531381,49.105088,1448.2980000000534 -120.531428,49.105421,1440.351985919929 -120.53149,49.10556,1437.2836479999557 -120.531662,49.105772,1437.5658188800169 -120.531875,49.10585,1439.2599999998959 -120.531988,49.105844,1439.5084211199148 -120.532044,49.105831,1439.2935788800526 + + + 0 + clampToGround + -120.532044,49.105831,1439.2935788800526 -120.532844,49.105641,1437.7077273599004 -120.533636,49.105369,1442.1057753601851 -120.534335,49.105184,1446.830614400045 -120.534712,49.105164,1449.7606732800998 -120.535363,49.105017,1445.7156238398554 -120.535854,49.105028,1444.6434995198908 -120.536212,49.105088,1441.0319999999938 -120.537106,49.10534,1438.71587839999 -120.537872,49.105453,1431.617297919952 -120.539129,49.105928,1433.767599999896 -120.539686,49.106093,1430.9262979199939 -120.540292,49.106406,1430.1887539201266 -120.540434,49.106548,1431.5301427198006 -120.5407,49.106701,1430.2251999999994 -120.540846,49.106869,1430.6675699200139 -120.540925,49.107228,1432.7486719999415 -120.540925,49.107477,1434.8462479999637 -120.540824,49.107715,1440.857993599971 -120.540773,49.108158,1436.848399999918 -120.540691,49.10832,1437.75560000016 -120.540972,49.108468,1431.4599756801106 -120.541147,49.108699,1429.6367211201525 -120.541363,49.109347,1434.8378945599013 -120.541681,49.109821,1433.8239999999923 -120.541766,49.110126,1440.261214720119 -120.541886,49.110178,1440.041551359823 -120.542359,49.110155,1435.8119999999442 -120.542983,49.109987,1432.021824159932 -120.543523,49.109976,1432.1360000001146 -120.54366,49.110005,1432.79200000001 -120.543868,49.1101,1434.0304000000551 -120.544112,49.110151,1434.1089804800579 -120.544789,49.11012,1432.4233056000637 -120.545386,49.110288,1437.5806745599416 -120.545789,49.11032,1437.5442592000563 -120.546944,49.110005,1424.1964288000274 -120.5474,49.11001,1421.4040000001264 -120.54775,49.110207,1422.6654400001723 -120.548414,49.11041,1416.1697696001775 -120.548914,49.110662,1413.4293587200323 -120.549475,49.111321,1407.2837679999782 -120.549644,49.111678,1402.8119999999606 + + + 0 + clampToGround + -120.549644,49.111678,1402.8119999999606 -120.54975,49.111901,1403.2151600000002 -120.54983,49.111976,1403.107276799987 -120.549957,49.11202,1402.23829439995 -120.550095,49.112005,1401.7285560000407 -120.550171,49.111933,1400.8098372800075 -120.550191,49.111699,1396.8580366400322 -120.550293,49.111545,1394.7167975999878 -120.551138,49.112005,1399.0387024001304 -120.551561,49.112094,1397.5295999999278 -120.551984,49.112057,1395.2108985599984 -120.552313,49.111952,1393.4716889600363 -120.552835,49.111907,1396.0286312000562 + + + 0 + clampToGround + -120.552835,49.111907,1396.0286312000562 -120.55295,49.111897,1395.6985039999759 -120.552938,49.11185,1394.4238879999184 -120.552962,49.111805,1393.1530735999238 -120.553083,49.111763,1391.7098638400967 -120.553446,49.111728,1389.6711564799962 -120.553802,49.111691,1387.188799999999 -120.554201,49.111612,1384.7460000000456 -120.554597,49.111504,1382.8067769598972 -120.554863,49.111411,1379.6760587199035 -120.555142,49.111303,1376.5703369599912 -120.555711,49.111147,1372.7224000000908 -120.556042,49.111084,1371.6533171199465 -120.556325,49.111094,1371.647264000043 -120.556628,49.111158,1369.5696000000194 -120.557007,49.111242,1365.2299999999727 -120.557262,49.111335,1362.3140000000446 -120.557415,49.111425,1361.2702199998334 -120.557548,49.111477,1360.157187840061 -120.557714,49.111517,1358.3302915199165 -120.558057,49.111652,1357.6249254399659 -120.558364,49.111736,1357.528000000024 -120.558698,49.111803,1356.0003222399853 -120.559343,49.11202,1355.4470656000208 -120.559855,49.112173,1354.5608983999875 -120.560121,49.112242,1355.7204000000108 -120.560522,49.112134,1357.0848339200306 + + + 0 + clampToGround + -120.560522,49.112134,1357.0848339200306 -120.560605,49.112112,1357.5623999999905 -120.560891,49.112225,1360.4472760000117 -120.561045,49.11223,1361.6293359999038 -120.561277,49.112193,1364.2207745600435 -120.561543,49.112146,1367.365925760035 -120.561798,49.112067,1369.8645993599887 -120.56206,49.112014,1372.708332800074 -120.562234,49.112032,1375.3202355200165 -120.56244,49.112103,1378.9000927999045 -120.56265,49.112249,1382.8735439999973 -120.562856,49.112481,1385.8140614400236 -120.56315,49.113015,1385.610360000025 -120.563421,49.113737,1389.3101700799925 -120.563514,49.113977,1389.0324000000132 -120.563635,49.114194,1387.827999999964 -120.563792,49.114352,1386.6975999999531 -120.564055,49.114551,1385.556207200017 -120.564321,49.114757,1383.6392388799009 -120.564559,49.114929,1382.2515999999457 -120.564963,49.115222,1379.9979999999568 -120.565222,49.115462,1378.9287999999449 -120.565262,49.115518,1378.8999999999664 -120.565266,49.115587,1379.1272000000642 -120.565238,49.115859,1379.2448000000654 -120.56525,49.115917,1378.6975999999122 -120.565318,49.115964,1377.479999999943 -120.565488,49.116067,1374.290399999964 -120.565593,49.116303,1372.5955999999255 -120.565714,49.116596,1372.0344000000064 -120.565787,49.117037,1367.4452222399814 -120.565803,49.117542,1366.5294889599563 -120.565827,49.118163,1365.3404000000032 -120.565855,49.118517,1361.782400000041 -120.565811,49.118784,1360.0267590400574 -120.565783,49.119017,1358.6212000000453 -120.565641,49.119331,1358.07597615999 -120.565294,49.119902,1358.9416000000201 -120.565036,49.120483,1353.9151999999756 -120.564999,49.120655,1353.0071999999982 -120.565028,49.120803,1352.8094073600123 -120.56502,49.121006,1353.4775999999752 -120.564988,49.121165,1354.2803999999946 + + + 0 + clampToGround + -120.564988,49.121165,1354.2803999999946 -120.564911,49.121545,1355.6408000000356 -120.564854,49.121791,1355.6036000000163 -120.564765,49.122161,1352.453541600082 -120.564701,49.122449,1348.13317295995 -120.564632,49.12265,1346.719007999971 -120.564535,49.122787,1346.3256231999485 -120.564442,49.122975,1346.2951040000128 -120.564382,49.123115,1345.5720000000106 -120.564224,49.123281,1344.3768000000098 -120.564014,49.123432,1343.2887820799854 -120.563764,49.123572,1341.5631999999582 -120.563421,49.12367,1340.8364000000233 -120.563126,49.123749,1340.0144000000496 -120.562856,49.123855,1338.4880000000153 -120.56265,49.124034,1335.6700959999812 -120.56246,49.124185,1332.7240000000263 -120.56229,49.124404,1330.5352000000273 -120.562177,49.124645,1329.8372000000197 -120.562097,49.124906,1329.1858492799875 -120.56204,49.125168,1329.0 -120.561984,49.125487,1326.8476556799908 -120.561887,49.125844,1324.13794112 -120.56182,49.126035,1323.3212480000072 + + + 0 + clampToGround + -120.5618958044465,49.126046827431196,1322.7725276961874 -120.56171352433815,49.12628882213653,1323.022865073844 -120.56158718069523,49.12645655414849,1323.7190393554645 + + + 0 + clampToGround + -120.561591,49.126456,1323.685576319941 -120.561709,49.126718,1322.8757635200177 -120.561451,49.127384,1325.0 -120.561344,49.127539,1324.9773113600036 -120.561007,49.127919,1324.0 + + + 0 + clampToGround + -120.561007,49.127919,1324.0 -120.560484,49.128507,1325.354148479989 -120.5602,49.128997,1321.8324000000189 -120.560032,49.12944,1317.8052863999326 -120.559597,49.130017,1317.7420000000411 -120.559398,49.130495,1318.1047999999525 -120.559253,49.130736,1315.2096000000545 -120.558695,49.130937,1317.6452935998868 -120.558536,49.131138,1312.84492415997 + + + 0 + clampToGround + -120.558536,49.131138,1312.84492415997 -120.558467,49.131226,1311.3725270401246 -120.558494,49.131495,1306.7260000000674 -120.558722,49.131817,1304.4259999999463 -120.559119,49.132133,1305.8536820801025 -120.559629,49.132274,1304.6657558400389 -120.561304,49.133224,1315.1615999998864 -120.561434,49.133245,1314.1760431999826 -120.561773,49.133561,1315.5667999999716 -120.562239,49.133841,1313.1291999998966 -120.562298,49.133899,1313.3172700800112 + + + 0 + clampToGround + -120.562298,49.133899,1313.3172700800112 -120.562461,49.13406,1313.3556863999913 -120.563195,49.134631,1310.6736568000547 -120.563744,49.134823,1307.1747999999732 -120.564545,49.134979,1298.6872000000635 -120.565082,49.13517,1296.2010751999687 -120.565231,49.135277,1294.8354569600688 -120.565342,49.135425,1294.542663999961 -120.565444,49.135746,1294.283336960009 -120.565475,49.136125,1295.7899999998613 -120.565561,49.136337,1294.7311387199543 -120.565578,49.136647,1296.4601206399864 -120.565647,49.136887,1296.4083214400307 -120.565629,49.137356,1297.8053350399764 -120.565475,49.137828,1301.579999999954 -120.565591,49.138133,1300.525174880032 -120.56562,49.138509,1301.4220832000522 -120.565642,49.138574,1301.529576320034 + + + 0 + clampToGround + -120.565642,49.138574,1301.529576320034 -120.565873,49.139244,1301.57160000004 -120.565882,49.139536,1301.7462540799752 -120.56597,49.13979,1300.179952000134 -120.565969,49.140264,1294.220575360041 -120.566276,49.141233,1287.3232415999585 -120.566293,49.141554,1282.5240131199594 -120.566152,49.141936,1281.0867251199222 -120.566237,49.142052,1277.84147903998 -120.566273,49.14227,1275.2309584000834 -120.566442,49.142623,1270.1262732801247 -120.566641,49.142961,1266.7789470398664 -120.566758,49.143099,1264.7359513602048 + + + 0 + clampToGround + -120.566758,49.143099,1264.7359513602048 -120.566813,49.143164,1263.54547456011 -120.566884,49.143636,1264.5850060799517 -120.566952,49.143845,1265.9923999999637 -120.567238,49.143963,1259.9931999999192 -120.567699,49.144255,1255.0637847999124 -120.567927,49.144314,1251.4356000000666 -120.568163,49.144437,1249.7324000000672 -120.568346,49.144642,1249.9319385598887 -120.568411,49.14479,1249.6702224000423 -120.568593,49.144848,1246.749277439896 + + + 0 + clampToGround + -120.568593,49.144848,1246.749277439896 -120.568787,49.144909,1244.3470363199929 -120.568889,49.145013,1242.9512187200287 -120.569128,49.14516,1238.6158207999013 -120.569309,49.145363,1236.7695999999678 -120.569335,49.145455,1236.8200000000452 -120.569376,49.145601,1237.0663999999356 -120.569516,49.145816,1237.1024000001005 -120.569553,49.14603,1237.2589136000192 -120.569646,49.146113,1236.1247999999114 -120.569831,49.146188,1235.107210240085 -120.570143,49.146482,1233.2759999999726 -120.570296,49.146891,1230.9703571200228 -120.57077,49.147386,1224.8184288000496 -120.570845,49.147544,1223.5542527998502 + + + 0 + clampToGround + -120.570845,49.147544,1223.5542527998502 -120.570889,49.147638,1222.1999587201392 -120.571085,49.147848,1217.793036800083 -120.571301,49.148228,1213.639178879884 -120.571341,49.148437,1212.5968603199162 -120.57158,49.14879,1206.2280000000646 -120.57192,49.149028,1198.9847295998902 -120.572631,49.149122,1191.2246332799516 -120.572841,49.149218,1189.6447395200212 -120.57327,49.149305,1184.7975440000776 -120.573426,49.149367,1182.906192320024 -120.573695,49.149696,1177.0752000000034 -120.573943,49.149713,1173.3139326400178 -120.574023,49.149765,1171.6068000000223 -120.573974,49.149829,1171.6211999999348 -120.573735,49.149933,1173.9396000000415 -120.573723,49.149969,1173.7235999999984 + + + 0 + clampToGround + -120.573723,49.149969,1173.7235999999984 -120.573704,49.150023,1173.4691116799277 -120.574063,49.150086,1167.952114560046 -120.574616,49.150454,1158.3206054399038 -120.574869,49.150441,1153.2227121598683 -120.575098,49.150494,1148.5834195200764 -120.575346,49.150769,1146.2225619200647 -120.575656,49.150879,1139.428552960153 -120.575718,49.150931,1138.691984320117 -120.576196,49.151137,1130.7231180800602 -120.576832,49.151555,1123.8124000000498 + + + 0 + clampToGround + -120.576832,49.151555,1123.8124000000498 -120.577287,49.152077,1121.8448000000367 -120.577415,49.152507,1119.4654887999422 -120.577428,49.152969,1119.3044332800694 -120.577697,49.153572,1111.0355999999856 -120.5778,49.153983,1107.9752960000826 -120.577793,49.154419,1103.0924000000443 -120.57814,49.154939,1098.2131999998674 -120.5783,49.155305,1095.9262400000273 -120.578441,49.155362,1092.825040640025 -120.578563,49.155273,1089.1951999999383 -120.578557,49.155112,1088.76679999992 -120.578461,49.154965,1090.5206191998732 -120.578461,49.154323,1092.8302742399628 -120.578692,49.153912,1094.0621721599318 -120.578756,49.153592,1089.6310860800731 -120.578711,49.153168,1086.7739321600375 -120.578775,49.152995,1085.2166200001836 -120.579116,49.152723,1078.9331692800256 + + + 0 + clampToGround + -120.57905156427313,49.15268840501497,1080.2977895194538 -120.57944105131698,49.152599544348604,1072.4194359493963 -120.57983053696356,49.152510682372274,1065.103786672913 -120.58022002121281,49.15242181908604,1056.0158464770682 -120.58060950406475,49.15233295448984,1037.1180096160588 -120.58099898551937,49.152244088583736,1018.3144845758479 -120.58102279361032,49.152238656365796,1017.5778469107031 + + + 0 + clampToGround + -120.58102279361032,49.152238656365796,1017.5778469107031 -120.58141412551845,49.15215334036831,1012.7745542878744 -120.58180545607868,49.15206802304846,1013.5547194321551 -120.58201986891478,49.15202127638824,1014.6432347855864 + + + 0 + clampToGround + -120.58201986891478,49.15202127638824,1014.6432347855864 -120.58240165521806,49.1519191233373,1015.4129242247526 -120.58277627086989,49.15181888727628,1015.3565381405804 + + + 0 + clampToGround + -120.582786,49.151802,1015.5383999999617 -120.582841,49.151815,1015.3980000000092 -120.583164,49.15197,1014.0359167999969 -120.583353,49.151989,1014.2198436800087 -120.583881,49.15191,1015.1239999999968 + + + 0 + clampToGround + -120.583881,49.15191,1015.1239999999968 -120.584091,49.151879,1015.6203548800031 -120.584394,49.151916,1016.6321958400099 -120.585134,49.152174,1021.354433919923 -120.585297,49.152333,1021.3800121599674 -120.585465,49.152673,1018.1535999999911 -120.585469,49.152963,1017.5242251200435 -120.585461,49.153103,1018.0926596799416 -120.585517,49.15337,1021.3059999999252 -120.58532,49.153876,1017.0962943999047 -120.585104,49.154075,1016.0092960000071 -120.584893,49.154372,1016.6169203199488 -120.584444,49.155309,1017.7721798400144 -120.584161,49.155539,1015.0355524799858 + + + 0 + clampToGround + -120.584161,49.155539,1015.0355524799858 -120.584139,49.155556,1014.894159360016 -120.58343,49.155807,1007.7289904000492 -120.583372,49.155859,1007.3707999999775 -120.583357,49.155919,1007.4788000000374 -120.583359,49.155997,1007.7740000000083 -120.583382,49.156067,1008.1916000000027 -120.583565,49.156392,1012.7034591999129 -120.583628,49.156543,1014.6981318400002 -120.583617,49.156769,1017.118589919977 -120.583673,49.156833,1017.8857873599954 -120.583773,49.156899,1018.6045460800206 -120.584044,49.157019,1020.0952000000082 -120.584273,49.157054,1021.786223679976 -120.58438,49.156912,1022.0703999999441 -120.584546,49.156725,1022.8824000000561 -120.584708,49.15657,1023.0552000001273 -120.584891,49.156432,1023.3006995200316 -120.585035,49.156334,1023.0243024001156 -120.585297,49.156184,1022.6418323200645 -120.585526,49.156065,1021.0561247999486 -120.585793,49.155932,1020.8820748799133 -120.586019,49.155836,1021.1480332798911 -120.586248,49.155748,1025.2062592000204 -120.586393,49.155679,1026.959731360011 -120.586512,49.1556,1028.1679359999466 -120.586588,49.155527,1027.8258777602025 -120.586694,49.155397,1026.3877996797714 -120.586761,49.155283,1023.6599068796987 -120.586866,49.155062,1021.2816416000373 -120.587147,49.154309,1018.6266103999883 -120.587215,49.154179,1017.1978279999872 -120.587315,49.154036,1017.7066047999992 -120.587446,49.153877,1015.599840639999 -120.587592,49.153742,1015.293045760008 -120.587747,49.153623,1014.2630310400612 -120.587944,49.153496,1013.1536230400284 -120.588102,49.15341,1012.293388800008 -120.588287,49.153328,1011.2276025600003 -120.588522,49.153259,1012.3002460800062 -120.58874,49.153206,1013.8834048000208 -120.588994,49.153163,1015.23203488 -120.589169,49.153145,1015.6807047999905 + + + 0 + clampToGround + -120.589169,49.153145,1015.6807047999905 -120.589347,49.153126,1015.9110371200029 -120.589528,49.153111,1015.8192793600027 -120.589726,49.1531,1015.308576000018 -120.589896,49.153082,1014.6243571199916 -120.590079,49.153081,1014.3008510400039 -120.591103,49.153096,1014.8985484799427 -120.592716,49.153127,1017.536794879959 -120.594691,49.153166,1016.1777804799726 -120.595443,49.153166,1015.2950150399796 -120.595808,49.153155,1015.5059999999535 -120.596184,49.153134,1015.0909964800238 -120.596703,49.153091,1015.738619840078 -120.59692,49.153061,1016.8666495999882 + + + 0 + clampToGround + -120.59692,49.153061,1016.8666495999882 -120.597103,49.153035,1017.3065608000485 -120.597515,49.152962,1015.6528000000326 -120.597863,49.152888,1014.8939999999741 -120.59842,49.152724,1015.8127872000445 -120.598774,49.152604,1017.0447999999819 -120.599146,49.152445,1017.088662400029 -120.599507,49.152275,1015.1827880000701 -120.600048,49.151951,1013.1963999999923 -120.601336,49.151173,1013.6259788800028 -120.601427,49.151239,1014.5183999999691 -120.603372,49.150039,1016.0 -120.603668,49.149856,1016.7231999999972 -120.60524,49.148887,1020.9932000000072 -120.605363,49.148951,1021.7539999999656 + + + 0 + clampToGround + -120.605363,49.148951,1021.7539999999656 -120.605758,49.149159,1024.6937148799675 -120.606056,49.149217,1026.015299839997 -120.60622,49.149224,1026.4002175999094 -120.606579,49.149185,1026.1672296000613 -120.606907,49.149078,1027.1780563200514 -120.606954,49.149057,1027.3624755200212 -120.607194,49.148949,1029.0072550401069 -120.607278,49.148889,1029.207280319797 -120.607967,49.148403,1030.161600000085 -120.608681,49.147943,1033.924800000055 -120.608944,49.147774,1036.7421017597842 -120.609435,49.147519,1048.0676744001873 -120.609611,49.147407,1049.390692160041 + + + 0 + clampToGround + -120.609611,49.147407,1049.390692160041 -120.609973,49.147176,1054.0354777599337 -120.610383,49.146605,1057.8099063999805 -120.610821,49.146109,1063.8592000000249 -120.611189,49.145811,1070.3402558398868 -120.611371,49.145559,1073.5212014401427 -120.611456,49.145373,1076.4160000001439 -120.611533,49.145108,1078.5767999997975 -120.611638,49.14484,1080.4183295999974 -120.611892,49.144454,1085.91651583996 -120.612687,49.143656,1097.6855999998702 -120.612947,49.14345,1099.7552000001433 -120.613288,49.143216,1101.6416000000095 -120.613548,49.14311,1102.6591999999218 -120.613878,49.143052,1105.7535999999275 -120.614128,49.143029,1106.4384774399477 -120.614291,49.143048,1107.3387999999838 -120.614705,49.143201,1109.7567367999384 -120.614911,49.143271,1111.5817022399733 -120.615032,49.143296,1112.477268479981 -120.615308,49.143331,1116.1326860801166 -120.615691,49.143343,1118.4345684800696 -120.615973,49.143387,1119.4088590398926 -120.616171,49.143443,1120.0056811199754 -120.616382,49.143553,1121.792811839866 -120.616914,49.143908,1125.2230809602247 -120.61755,49.144303,1124.8772000000858 -120.617987,49.144549,1126.7148955199525 + + + 0 + clampToGround + -120.617987,49.144549,1126.7148955199525 -120.618341,49.144748,1125.777438719989 -120.619508,49.145414,1132.582196480087 -120.619764,49.145687,1138.7444921600286 -120.620109,49.145987,1140.749599999968 -120.620401,49.146196,1141.3327641600545 -120.620527,49.146242,1141.0871606400585 -120.620647,49.14628,1142.293231999995 -120.620809,49.146275,1143.4104199999715 -120.620951,49.146247,1145.8327246401 -120.621029,49.146187,1147.6266737598526 -120.621106,49.14607,1149.2791999999613 -120.621121,49.145943,1147.936125600117 -120.621214,49.145622,1152.445399679762 -120.621221,49.145362,1157.256745920234 -120.621214,49.14504,1161.5890623997782 -120.621175,49.144703,1161.7694319997545 -120.62115,49.144235,1168.7813199999587 -120.621157,49.143897,1176.3164238398356 -120.621143,49.143085,1178.9272000001179 -120.621107,49.142469,1183.021148319895 -120.621112,49.142271,1183.84416191992 -120.621138,49.142146,1184.0320000000193 -120.621203,49.141973,1184.1903999998751 -120.621603,49.141295,1191.9314695998871 -120.621836,49.140867,1194.9026835200207 -120.622079,49.140542,1196.7927225600288 -120.622268,49.140341,1196.5320915200643 -120.622494,49.140221,1197.8960150399869 -120.622836,49.140039,1198.2616000000817 -120.623055,49.139917,1196.3079951998698 -120.623163,49.139866,1194.6583999999295 -120.623256,49.139816,1193.0384000001059 -120.623832,49.139704,1188.3708697600664 -120.626044,49.139696,1186.6484070400077 -120.627085,49.139459,1180.536000000036 + + + 0 + clampToGround + -120.627085,49.139459,1180.536000000036 -120.627994,49.139252,1179.2361510399764 -120.629735,49.139098,1170.0337711999127 -120.63011,49.13914,1171.2899839999354 -120.630812,49.139318,1174.1813593600632 -120.631434,49.139515,1174.728000000129 -120.631863,49.139727,1174.6795139201051 -120.632248,49.139766,1172.6909747200368 -120.633492,49.139572,1163.9054950400234 -120.63476,49.139483,1159.3285695999527 -120.636045,49.139666,1157.4514288000044 -120.636747,49.139896,1153.4885235200097 -120.636847,49.14004,1155.123199999939 -120.637372,49.140792,1155.4770329599903 -120.637625,49.141075,1156.5500000000147 -120.637716,49.141163,1157.0934886400119 + + + 0 + clampToGround + -120.637716,49.141163,1157.0934886400119 -120.639335,49.14272,1162.119999999888 -120.639689,49.143233,1167.7488004799702 -120.640131,49.143545,1164.5551224000926 -120.640496,49.143905,1164.4779999999355 -120.640885,49.144082,1163.974692799981 -120.641304,49.144412,1164.5307059199774 -120.641605,49.144842,1168.4865263999743 -120.641708,49.144825,1167.2761439999915 -120.641828,49.144809,1166.4873580800088 -120.641992,49.144804,1165.7119999999995 -120.642213,49.14484,1165.3052000000291 -120.642479,49.144863,1163.671600000049 -120.642748,49.144861,1162.6059999999961 -120.642901,49.144823,1162.0884000000256 -120.643014,49.144747,1161.267600000026 -120.643141,49.144702,1160.7816000000253 -120.643218,49.144682,1160.5656000000333 -120.643349,49.144713,1161.0113275200372 -120.643531,49.144771,1163.3011999999458 -120.643614,49.14478,1164.0607999999465 -120.643702,49.144742,1164.0104000000483 -120.643742,49.144671,1163.2048000000577 -120.643709,49.144516,1160.7352000000515 -120.643702,49.144354,1159.328800000033 -120.643738,49.144266,1159.2711999999995 -120.643793,49.144233,1159.5484000000158 -120.643891,49.144245,1160.297199999967 -120.643971,49.144285,1161.0171999999652 -120.644069,49.144437,1162.2700000000254 -120.644127,49.144645,1164.9835015999547 -120.644186,49.144842,1168.1955884799572 -120.644244,49.145018,1171.1296000000023 -120.644331,49.14528,1173.023999999996 -120.644356,49.145365,1173.9419999999805 + + + 0 + clampToGround + -120.644356,49.145365,1173.9419999999805 -120.644379,49.145444,1174.795199999975 -120.644528,49.145682,1177.3656000000083 -120.644648,49.145835,1179.01799999998 -120.645099,49.146263,1182.9320795199765 -120.645219,49.146382,1183.930847680027 -120.645248,49.146489,1184.3604000000091 -120.645212,49.146713,1185.6277017599778 -120.645186,49.147018,1190.258400000043 -120.645183,49.147217,1194.5459999999612 -120.64523,49.147353,1197.101022400033 -120.645365,49.147501,1200.9527999999623 -120.645561,49.147639,1204.540415679984 -120.64571,49.147701,1206.5314431999157 -120.645881,49.147758,1209.5020000001077 -120.645969,49.14781,1211.5013455998906 -120.646121,49.147951,1214.1646001598954 -120.64674,49.148479,1223.2675999999296 -120.646773,49.148512,1223.5051999999694 -120.646816,49.148527,1223.9156000000075 -120.646864,49.148519,1224.4627999999307 -120.646998,49.148458,1226.1295999999684 -120.647337,49.148238,1228.1822102399567 -120.647501,49.14815,1228.0308000000707 -120.647697,49.148112,1229.7371999999064 -120.647908,49.148098,1232.3335999999867 -120.648079,49.148143,1235.2819999999097 -120.648221,49.148231,1238.2772000001237 -120.648457,49.148355,1242.780799999905 -120.648639,49.148424,1245.3012000000308 -120.648858,49.148517,1247.6664000000449 -120.64912,49.148581,1251.2378111999494 -120.649371,49.148607,1257.1235131200438 -120.649629,49.148574,1261.3904000000896 -120.649884,49.148481,1264.4296076799649 -120.650306,49.148286,1267.5890252799873 -120.650957,49.148041,1274.9930715200824 -120.65147,49.14787,1281.452000000006 -120.652176,49.147717,1291.2511999999424 -120.653126,49.147463,1300.1896000000752 -120.653814,49.14736,1311.1079999999083 -120.654847,49.147215,1324.0096000001695 -120.655469,49.147148,1331.19119999998 -120.65576,49.147146,1337.5995583999932 -120.655835,49.147135,1338.9061159998466 + + + 0 + clampToGround + -120.655835,49.147135,1338.9061159998466 -120.65595,49.147118,1341.4504160000781 -120.656124,49.14707,1344.3387456001308 -120.656415,49.14693,1345.9868879997732 -120.656845,49.146796,1350.39503040018 -120.657343,49.146684,1356.2353369599457 -120.657969,49.146603,1368.2222187198317 -120.658166,49.14661,1372.336000000017 -120.658249,49.146634,1374.9531999999062 -120.658333,49.146651,1377.2607999999123 -120.658431,49.146637,1378.0179999998236 -120.658602,49.14656,1376.87680000007 -120.658951,49.146472,1376.6501977601501 -120.659384,49.146425,1379.8789120000615 -120.660054,49.146382,1388.4703577601529 -120.660381,49.146351,1391.205200000151 -120.660611,49.146363,1394.1130027200675 -120.660862,49.146432,1398.9495833598646 -120.661087,49.146539,1403.9045067198754 -120.661248,49.146603,1406.704699519962 -120.66144,49.146618,1407.7911999998814 -120.662132,49.146599,1413.0768000000523 -120.662903,49.146584,1423.3342419199344 -120.663285,49.146565,1429.568000000097 -120.663678,49.146475,1433.604704000108 -120.663893,49.146411,1436.3116219200342 -120.664064,49.146406,1439.2979673599234 -120.664162,49.14643,1441.2144864000966 -120.664217,49.146484,1442.409599999915 -120.664235,49.14656,1443.2160000000788 -120.664228,49.146625,1443.583199999919 -120.66418,49.146746,1444.8781824000275 -120.664177,49.14681,1446.2064095999929 -120.664231,49.146851,1447.60062047997 -120.664311,49.146875,1448.7990000000063 -120.664908,49.146965,1457.6645087999702 -120.665905,49.147087,1472.8003999999403 -120.666258,49.147106,1477.5859500799934 -120.666644,49.147037,1484.2039891200852 -120.666858,49.146932,1487.4718835200863 -120.667128,49.146777,1491.2448697600648 -120.667186,49.146765,1491.9166384000177 -120.667251,49.146779,1492.8187999999095 -120.668154,49.147098,1507.970400000017 -120.668227,49.147129,1509.133200000023 -120.668256,49.147179,1509.7307999999975 -120.668194,49.14747,1513.901027199966 -120.66819,49.147527,1515.1794447998604 -120.668237,49.14757,1516.8882064001052 -120.668339,49.147577,1518.2844548799876 -120.668612,49.147541,1520.3503276797978 -120.669052,49.147474,1528.4679999999296 -120.669351,49.147415,1532.7880000001496 -120.669547,49.147325,1535.0651999999163 -120.66986,49.147113,1538.013145599951 -120.67025,49.146884,1540.2161599999279 -120.67077,49.146618,1543.4983455999832 -120.671127,49.146475,1542.3110680000639 -120.671618,49.146344,1544.181599999923 -120.67176,49.146308,1544.6783999999989 -120.671847,49.146308,1545.6179999999288 -120.671894,49.146332,1546.8167999999716 -120.671938,49.146372,1548.443999999956 -120.671989,49.146394,1549.4893513600002 -120.672225,49.146408,1551.7080000000167 -120.672287,49.146394,1551.6504000000086 -120.672371,49.146394,1552.2552000000164 -120.672418,49.146415,1553.3495999998624 -120.672513,49.14648,1556.3582495998567 -120.6726,49.146544,1559.1029759999683 -120.672702,49.146577,1560.7339401600257 -120.672797,49.146568,1560.8277798400688 -120.672873,49.146537,1559.9588190400593 -120.672953,49.146501,1559.0086091200344 -120.673055,49.146489,1558.8819208000846 -120.673241,49.146503,1560.347349920097 -120.673746,49.146584,1565.2290854398668 -120.6743,49.146739,1572.208191999899 -120.674725,49.146872,1576.6328000000963 -120.675242,49.147082,1579.2191999999477 -120.675689,49.147282,1582.191199999853 -120.676446,49.147722,1592.0236844800568 -120.676868,49.147922,1595.9480000000435 -120.677341,49.148134,1603.3169372799582 -120.67756,49.14821,1605.9879039999712 -120.677727,49.148238,1606.8780630399876 -120.677895,49.148253,1607.6867999999558 -120.678,49.148284,1608.73439999992 -120.678062,49.148331,1609.9959999999546 -120.678062,49.148391,1611.0843999999179 -120.677778,49.148531,1613.2698307199694 -120.677589,49.148607,1614.2416580799602 -120.677553,49.148667,1614.9956000000577 -120.677563,49.148738,1616.0540000000458 -120.677567,49.148814,1617.162800000009 -120.677505,49.148857,1617.5587999999811 -120.677422,49.148881,1617.8784252800458 -120.677181,49.148912,1618.3631468800772 -120.677036,49.148969,1619.2533433600272 -120.67685,49.149086,1622.2084639999148 -120.676727,49.14915,1624.3158319999673 -120.676617,49.149181,1625.370426079967 -120.676301,49.149141,1624.2608000000055 -120.676133,49.149145,1624.3759999999193 -120.675642,49.149212,1624.3919756799482 -120.675591,49.149226,1624.4637107199574 -120.675573,49.149245,1624.7902192000113 -120.675588,49.149269,1625.413258239957 -120.675657,49.149291,1626.3777270399603 -120.675773,49.149321,1627.9862473600633 -120.675879,49.149355,1629.5883999999785 -120.675922,49.14939,1630.7511999999076 -120.675908,49.149438,1632.0832000001258 -120.675846,49.149474,1632.6888518399821 -120.675773,49.149505,1632.5143512000443 -120.675777,49.149538,1633.2074908799445 -120.675817,49.149555,1634.1413927999129 -120.675959,49.149574,1635.4617993600596 -120.676152,49.149657,1638.3563999999424 -120.676646,49.149943,1645.2554006400364 + + + 0 + clampToGround + -120.676646,49.149943,1645.2554006400364 -120.676679,49.149962,1645.9500000000364 -120.676945,49.150112,1650.825206400018 -120.677167,49.150254,1655.93461727989 -120.677676,49.15049,1659.9503999999333 -120.678157,49.150761,1664.8120000000752 -120.678353,49.150861,1666.2279999999846 -120.678633,49.15093,1665.9652000000087 -120.678906,49.151037,1666.2447651200196 -120.679154,49.151171,1668.1699999999814 -120.679394,49.15128,1670.2448000000143 -120.679627,49.151354,1672.1062556799625 -120.679841,49.151385,1673.8271999999554 -120.680405,49.151418,1680.098401599902 -120.680944,49.151473,1688.7797644799703 -120.681067,49.151504,1691.1006067200008 -120.681158,49.151506,1692.616033919925 -120.6813,49.151447,1694.6761439998695 -120.681428,49.151373,1696.4077075199482 -120.681555,49.151342,1698.0874848000449 -120.681682,49.151309,1699.988399999902 -120.681733,49.151275,1700.7839999998885 -120.681766,49.151228,1701.2087999999414 -120.681788,49.151047,1700.592413439936 -120.681792,49.150909,1699.4014931200186 -120.681864,49.15079,1699.8395776001003 -120.682232,49.15048,1700.2704512000107 -120.68269,49.150131,1697.364574400004 -120.68316,49.149878,1696.0536000000363 -120.683444,49.149655,1693.5012000000088 -120.683633,49.149471,1690.8267999999612 -120.68396,49.149269,1688.2524895999122 -120.684437,49.149133,1689.0979999999954 -120.684801,49.149026,1687.8787849599423 -120.685081,49.149002,1688.2576000000854 -120.685354,49.149024,1688.6167999999045 -120.685634,49.149014,1687.193607040066 -120.686264,49.148881,1679.7444313600986 -120.686584,49.148886,1678.2382950399603 -120.686959,49.148945,1677.044215199931 -120.687304,49.14901,1674.3344831999782 -120.687843,49.149212,1669.8384806399624 -120.688243,49.149412,1666.754783359969 -120.68852,49.149626,1663.5079808000523 -120.688643,49.14979,1661.4991776000315 -120.688738,49.14999,1659.3424895999797 -120.688789,49.150216,1657.5971750400029 -120.688807,49.150402,1657.694330880063 -120.688847,49.150547,1657.7016892801 -120.689,49.150769,1656.7515200000812 -120.689149,49.150959,1657.4876273599994 -120.689302,49.15109,1658.1213728000928 -120.689404,49.151152,1658.3200000001127 -120.689538,49.151187,1657.8128275200143 -120.689746,49.151187,1656.0144000000087 -120.690302,49.151211,1654.1805571201096 -120.690881,49.151247,1653.8919999999248 -120.691663,49.151373,1657.4287550399822 -120.692373,49.151518,1654.6634131200258 -120.692582,49.151588,1654.2051993599302 -120.692762,49.151661,1654.9355587199323 -120.692941,49.151759,1655.9562817599242 -120.693068,49.151822,1656.220800000096 -120.693199,49.151856,1655.5403999999721 -120.693305,49.151896,1655.2596000000672 -120.693377,49.151932,1655.1024000000739 -120.693577,49.152082,1655.9575999999324 -120.693901,49.152363,1657.8228000000827 -120.694236,49.15262,1655.906227199914 -120.694414,49.152868,1655.2687961600907 -120.694651,49.153203,1652.5541942401433 -120.694731,49.15342,1651.4659999998987 -120.694738,49.153577,1652.470399999972 -120.694633,49.153855,1655.2092136000977 -120.694578,49.154041,1656.634399999823 -120.694585,49.154165,1656.4579999999103 -120.694661,49.154431,1653.8009326400957 -120.694691,49.154569,1653.2467996798541 -120.694814,49.154681,1650.82016864012 -120.695011,49.154788,1647.2360000001584 -120.69516,49.154928,1645.025599999958 -120.695247,49.155126,1644.1686588801308 -120.695295,49.155326,1644.1719632000013 -120.695318,49.155406,1644.5556396799914 + + + 0 + clampToGround + -120.695318,49.155406,1644.5556396799914 -120.695338,49.155476,1644.9315084799464 -120.695389,49.1555,1644.7191199999845 -120.695451,49.155504,1644.265859839961 -120.695546,49.155452,1642.963224320025 -120.695608,49.155395,1641.9680736000246 -120.695684,49.155383,1641.3867571200062 -120.69576,49.155409,1641.292886399945 -120.695786,49.155454,1641.770298239993 -120.69591,49.155485,1641.8437919999838 -120.696019,49.155485,1641.6444527999638 -120.696164,49.155431,1640.0338246400229 -120.696281,49.155405,1638.804912800048 -120.69639,49.155393,1637.9059407999005 -120.696587,49.155362,1637.6193577599092 -120.696674,49.155259,1635.5514153600286 -120.696728,49.155159,1633.1331859199236 -120.696838,49.155093,1631.5501046400218 -120.697049,49.155036,1630.461981440107 -120.697216,49.155031,1630.9795001600228 -120.697351,49.155059,1632.0100686400456 -120.697507,49.155057,1632.0571710399772 -120.697875,49.155,1630.3500000000301 -120.698468,49.154936,1630.6726041600048 -120.698919,49.154898,1629.6952000000929 -120.699505,49.154931,1628.2491023999119 -120.699981,49.155024,1626.0067097599506 -120.700302,49.155121,1626.6776470400757 -120.700978,49.155212,1626.1873254399275 -120.701601,49.155295,1624.7617136000872 -120.702878,49.155573,1618.0993417599263 -120.703391,49.155742,1613.9891999999904 -120.70421,49.156085,1607.7826719998898 -120.705025,49.156516,1602.2903999999148 -120.705545,49.156987,1595.8729783999938 -120.7058,49.157268,1593.044352000103 -120.705996,49.157617,1588.7465452800518 -120.706062,49.157786,1587.9055334399236 -120.706098,49.157858,1587.888737280048 -120.706164,49.157908,1587.0111999999544 -120.706255,49.15792,1584.9339999999806 -120.706433,49.157858,1579.8087999998943 -120.706626,49.157715,1574.7477728000426 -120.706746,49.157553,1570.38969248008 -120.706968,49.157275,1564.1255999999528 -120.707011,49.157153,1562.4738916800784 -120.707081,49.157025,1560.0461639999355 -120.707204,49.156947,1557.298596479971 -120.707343,49.156901,1554.703202720047 -120.707466,49.156892,1552.900090880037 -120.707968,49.156923,1547.6611334400543 -120.70838,49.156977,1543.1327999999285 -120.708718,49.156996,1540.9013811199568 -120.709035,49.157056,1539.1527999999691 -120.709395,49.157227,1538.4075999999832 -120.710159,49.157591,1538.607763519966 -120.71033,49.157746,1538.719107200012 -120.710461,49.157925,1538.870400000013 -120.710639,49.158096,1539.101861760001 -120.710799,49.158239,1539.081625440014 -120.710876,49.15836,1539.0 -120.710941,49.158572,1539.0 -120.710992,49.158796,1538.6198092799843 -120.711058,49.158919,1538.1035260800309 -120.711185,49.158983,1537.485320800001 -120.711454,49.158995,1537.1448592000097 -120.711709,49.158917,1538.1881771199835 -120.711862,49.15876,1540.196684799963 -120.711967,49.158541,1543.0820000000238 -120.711978,49.158379,1544.9900000000666 -120.711702,49.157746,1546.4814483200164 -120.711567,49.157606,1545.626963840015 -120.711505,49.157479,1546.2579991999742 -120.711491,49.157332,1549.141675520043 -120.711483,49.157144,1552.4651999999699 -120.71144,49.157027,1553.995199999995 -120.711323,49.15682,1555.552534399943 -120.711309,49.15669,1556.280958400015 -120.711331,49.156068,1557.0373126400193 -120.711316,49.155909,1557.8109555199628 -120.711363,49.155797,1559.1006094400675 -120.711432,49.155661,1561.3268000000003 -120.71144,49.155531,1562.8009343999242 -120.711407,49.155402,1563.8179574399244 -120.711385,49.155326,1564.4347696000555 -120.711407,49.155288,1565.02239935997 -120.711509,49.155257,1566.08920000006 -120.711709,49.155231,1567.6832641599894 -120.711847,49.155205,1568.605290399997 -120.711916,49.155159,1569.564213760045 -120.711916,49.155105,1570.321907200028 -120.711858,49.15504,1570.8464128000487 -120.711756,49.154971,1571.373999999874 -120.711542,49.154848,1572.6872166399196 -120.711432,49.154695,1574.6768096000487 -120.711414,49.154336,1576.7520000000677 -120.711447,49.154234,1577.5944000000895 -120.711491,49.154172,1578.4511999999215 -120.711578,49.154143,1579.9207958400605 -120.71184,49.154162,1582.271116799968 -120.71279,49.154264,1585.1605823999776 -120.713037,49.154307,1588.2613473599895 -120.713288,49.15441,1591.5749632000939 -120.713459,49.154476,1593.9320147199787 -120.714009,49.154579,1601.5305454401048 -120.714722,49.154641,1610.7517660799804 -120.715624,49.154902,1622.570659840093 -120.716501,49.155064,1632.7730105598362 -120.717735,49.155452,1643.1905888001293 -120.718386,49.155728,1648.6543999998707 -120.718935,49.155914,1653.5829935999536 -120.719452,49.15598,1656.718476800015 -120.719983,49.155971,1657.052799999958 -120.720169,49.155949,1658.4599867200122 -120.720267,49.155921,1660.225135840063 -120.720304,49.15589,1661.712684800043 -120.720278,49.155849,1664.1523097598642 -120.720187,49.155795,1666.0953016000724 -120.719856,49.155631,1670.6079923199584 -120.719703,49.155533,1672.4307619199953 -120.719518,49.155347,1674.6968236799205 -120.719303,49.155078,1675.9070166398703 -120.71923,49.154905,1679.2959759999437 -120.719212,49.154843,1680.9950406399896 -120.719223,49.154805,1682.474365599884 -120.719267,49.154793,1683.781966240112 -120.719372,49.15481,1685.357612800225 -120.719533,49.154849,1687.1755003200149 + + + 0 + clampToGround + -120.719533,49.154849,1687.1755003200149 -120.719798,49.154912,1690.0423539198644 -120.720158,49.155021,1692.7785987200205 -120.720398,49.155064,1694.58588287995 -120.721159,49.155083,1699.5991475201174 -120.721224,49.155078,1700.5187315199014 -120.721246,49.155062,1701.607256319879 -120.721235,49.155033,1703.0276607997466 -120.72117,49.154998,1703.9143264000413 -120.721068,49.154945,1704.2856704000958 -120.720926,49.154859,1705.2309353600167 -120.720773,49.154712,1707.7084070398118 -120.720249,49.154015,1717.805034399902 -120.719929,49.153451,1726.0123999999828 -120.719911,49.153398,1727.0887999998745 -120.719921,49.15337,1727.938399999929 -120.719958,49.15336,1728.7232000000963 -120.720056,49.153389,1729.6052000000655 -120.722116,49.154238,1742.3662016000224 -120.722301,49.154317,1740.4041156800756 -120.722341,49.154326,1740.4347286401303 -120.72237,49.154312,1741.5248576001136 -120.72237,49.154276,1743.4082047998183 -120.722319,49.154215,1745.8969783999498 -120.722218,49.154107,1748.6575999998759 -120.721657,49.153291,1762.7403999998162 -120.721581,49.153025,1767.1940760000202 -120.721519,49.152891,1769.1738801600889 -120.721257,49.152506,1773.9598556798599 -120.721235,49.152458,1775.1959055997909 -120.721242,49.152423,1776.4942499201031 -120.72129,49.152403,1778.0442543999447 -120.721362,49.152418,1778.7742739200903 -120.722054,49.152782,1785.3856102399225 -120.722643,49.153136,1791.8312000000806 -120.723313,49.153624,1791.9715929600268 -120.72336,49.15366,1790.4719999998727 -120.723396,49.15367,1790.2848000000383 -120.723429,49.153658,1791.3323999999513 -120.723436,49.153624,1793.3664000000342 -120.723411,49.15356,1795.7816928000295 -120.72332,49.153382,1801.016409599998 -120.722953,49.152703,1814.4692000000337 -120.722585,49.152039,1825.065199999809 -120.722443,49.151804,1827.506949120001 -120.722425,49.151749,1828.6587719999616 -120.722461,49.151716,1830.5714649598738 -120.72253,49.15172,1832.2407360002317 -120.722676,49.151799,1833.92584704027 -120.723633,49.152468,1839.191322240143 -120.723855,49.152596,1838.9339999999122 -120.723986,49.152644,1838.5151999998945 -120.724084,49.152656,1838.7888000000223 -120.724149,49.152653,1839.3647999999494 -120.724193,49.152625,1840.4999999999836 -120.724193,49.152565,1842.6599999999019 -120.724157,49.152503,1844.822399999932 -120.724044,49.152401,1846.0561862400853 -120.72388,49.152206,1849.2826687999257 -120.723626,49.151839,1855.8383465598865 -120.723444,49.151604,1857.0525209600366 -120.723335,49.151223,1862.0148336000711 -120.723331,49.151016,1864.9880761600452 -120.723382,49.150702,1868.1736000000033 -120.723418,49.150638,1868.9223999999288 -120.723473,49.150607,1869.825999999975 -120.723582,49.150635,1871.2947999998992 -120.724095,49.150871,1877.3381848000743 -120.724419,49.151016,1880.3497638399783 -120.724699,49.151099,1883.7528000000007 -120.725048,49.15114,1885.7036288000502 -120.725299,49.151159,1886.7668572799962 -120.725456,49.151106,1888.0920000000247 -120.725529,49.151016,1889.7120000000273 -120.725554,49.150859,1892.5380000000546 -120.725587,49.150688,1894.2188262400061 -120.725674,49.150509,1895.852799999985 -120.725863,49.15029,1897.2042016000282 -120.726155,49.150076,1899.4307712000564 -120.726406,49.149883,1901.2451459200042 -120.726486,49.149828,1901.3276723199626 -120.726616,49.149776,1901.8641126400576 -120.72686,49.149757,1903.9517824000154 -120.727326,49.149705,1905.757889600006 -120.727406,49.14969,1905.684636799999 -120.727443,49.149652,1906.060776319966 -120.727439,49.149543,1907.3753422399975 -120.727421,49.149443,1908.43196336005 -120.727454,49.149317,1908.8996681599267 -120.727493,49.149233,1909.6532532799226 -120.727563,49.149181,1910.3668913600313 -120.728298,49.148803,1913.4849395199763 -120.728318,49.148791,1913.5534950400208 + + + 0 + clampToGround + -120.728318,49.148791,1913.5534950400208 -120.729306,49.148196,1921.7407731200183 -120.729742,49.147946,1923.5194438400335 -120.730284,49.147677,1926.3635801600117 -120.730536,49.147508,1926.9452825599462 -120.730798,49.147182,1927.4711443200633 -120.730947,49.14701,1926.9685151999813 -120.7311,49.146884,1927.2314880000026 -120.73154,49.146623,1926.4560000000165 -120.731874,49.146344,1926.7054182400193 -120.732475,49.146039,1928.863271999967 -120.73301,49.145792,1928.8023935999931 -120.733439,49.145589,1928.4407983999665 -120.73377,49.145428,1927.947436799903 -120.734171,49.145301,1927.2765750398557 -120.734629,49.145144,1925.1798252799567 -120.735098,49.144918,1930.9959328000746 -120.735441,49.144821,1933.7603483199894 -120.735779,49.144694,1937.5423811200008 -120.735986,49.144578,1939.1401139199263 -120.736259,49.144525,1941.9252199999507 -120.736387,49.144495,1944.2557879999965 -120.736456,49.144452,1946.5919999999387 -120.736674,49.144214,1949.567999999952 -120.73671,49.144183,1950.2916000000434 -120.736754,49.144168,1950.928800000046 -120.736816,49.144175,1951.5228000000811 -120.736881,49.144202,1951.9331999999658 -120.737245,49.144309,1955.8307832000262 -120.737318,49.14433,1956.7572576000723 -120.737369,49.144333,1957.5291940799766 -120.737413,49.144302,1958.637791040016 -120.737438,49.14423,1960.0228895999508 -120.737464,49.144178,1961.1940876800973 -120.737595,49.144121,1963.930624799987 -120.737831,49.144028,1968.9564000000075 -120.738028,49.143937,1973.1575999999905 -120.73822,49.143823,1975.0643999999559 -120.73841,49.143666,1975.3662624000492 -120.738526,49.143538,1974.7647999999167 -120.738592,49.14344,1973.7135999999362 -120.738719,49.143376,1973.8735417600913 -120.738901,49.14335,1975.3681839999567 -120.739123,49.14334,1977.6041728000916 -120.739316,49.143354,1979.7960025600437 -120.739381,49.143362,1980.5447708799695 -120.739429,49.143352,1980.971336320014 -120.739454,49.143311,1980.8675315199116 -120.739469,49.143207,1979.981208639963 -120.739625,49.143028,1981.051600000075 -120.739792,49.142812,1982.220252159915 -120.740069,49.142657,1983.4308043200442 -120.740393,49.142554,1985.0219999999283 -120.74106,49.142267,1989.7772608000255 -120.741337,49.142124,1990.605547520066 -120.74157,49.14201,1991.7879999999236 -120.741897,49.141874,1993.7951999999505 -120.74221,49.141748,1995.3008831999962 -120.742669,49.141567,1996.672400000113 -120.743051,49.141453,1997.1249851199354 -120.743225,49.141407,1996.9360000000004 -120.743345,49.141398,1997.2466224000777 -120.743506,49.141405,1998.4971471999577 -120.743702,49.141424,2000.1600000000021 -120.743789,49.141436,2000.9592000000307 + + + 0 + clampToGround + -120.743789,49.141436,2000.9592000000307 -120.744022,49.141467,2001.9900489599822 -120.744335,49.141498,2001.3343631999544 -120.74467,49.141534,1998.9966112000068 -120.744986,49.141591,1997.7780000000034 -120.745241,49.141641,1998.6148675199784 -120.745398,49.141684,1999.1561932800143 -120.745696,49.141788,1998.2576921600103 -120.745871,49.14186,1998.1419775999507 -120.746049,49.141948,1998.4844275200082 -120.746238,49.142053,1999.4425174401206 -120.746369,49.142102,2000.9713884800087 -120.746729,49.142129,2007.9266913599954 -120.747013,49.142117,2010.1702243199666 -120.747363,49.1421,2011.3068000000117 -120.747548,49.1421,2011.9727999999673 -120.74777,49.142117,2013.0779999999907 -120.74797,49.14215,2013.8799200000092 -120.748199,49.142183,2014.4352000000408 -120.748443,49.142234,2015.2287395200374 -120.748571,49.142234,2015.2482774400412 -120.749804,49.142167,2018.5947999999926 -120.750163,49.14217,2021.2336000000064 -120.750265,49.142151,2021.6259999999622 -120.750612,49.142079,2023.2392300800584 -120.751369,49.142141,2025.5380000000414 -120.752129,49.142186,2029.01240000003 -120.75226,49.142174,2029.155609599954 -120.752591,49.142093,2028.6420004800161 -120.752781,49.142044,2028.8110425599662 -120.75293,49.142034,2029.5313248000773 -120.753051,49.142051,2030.5078910400266 -120.753578,49.141976,2033.3296000000264 -120.754585,49.141878,2036.6949648000052 -120.755682,49.141725,2037.654807999968 -120.756473,49.141592,2039.9114073600624 -120.757312,49.141541,2043.1563999999098 -120.758146,49.141478,2045.2503999999726 -120.758805,49.141447,2046.4012215999828 -120.759673,49.141552,2049.5231999999405 -120.760024,49.141597,2050.7459999999514 -120.760325,49.141611,2050.8620679999717 -120.761601,49.141651,2046.9302670400134 -120.76212,49.141665,2047.3733760000039 -120.761849,49.141904,2046.8283155200227 -120.761648,49.142289,2049.442000000059 -120.761491,49.14259,2051.897199999929 -120.761467,49.142646,2052.1564000000417 -120.761449,49.142723,2052.3508000000115 -120.761464,49.142899,2052.0707974399543 -120.761486,49.14317,2051.601599999928 -120.761467,49.143445,2052.5661576000607 -120.761465,49.143511,2053.0079495999594 -120.761483,49.143566,2053.0362211200277 -120.761553,49.143694,2052.5206947199126 -120.76159,49.143775,2052.397160000016 -120.761608,49.143838,2052.5170918400845 -120.761606,49.14391,2052.8141983999985 -120.761603,49.144027,2052.5335582400967 -120.76158,49.144194,2052.301798400097 -120.761544,49.144326,2051.554196479996 -120.76151,49.144385,2051.361391999987 -120.761468,49.14444,2051.2004864000323 -120.761282,49.144608,2052.8820595200914 -120.761255,49.14464,2053.02734399991 -120.76124,49.144683,2052.900566399986 -120.761231,49.144736,2052.5845926400384 -120.761223,49.144806,2052.066915519964 -120.761211,49.144894,2051.462023359955 -120.76101,49.145572,2052.7984512000185 -120.761006,49.145681,2052.7211145600813 -120.760999,49.145725,2052.77180400003 -120.760983,49.145763,2052.961575840058 -120.760839,49.145921,2054.9119617599335 -120.760801,49.145969,2055.5819999999867 -120.760783,49.146024,2055.9059999999363 -120.760783,49.146086,2055.9059999999363 -120.760797,49.146145,2055.8820424000396 -120.760834,49.146227,2055.8234012799794 -120.760971,49.146419,2055.2890769600467 -120.761164,49.146683,2054.297200000101 -120.76126,49.146815,2053.3900000000895 -120.761347,49.146922,2052.5224000000035 -120.761375,49.146975,2052.628999999953 -120.761389,49.147021,2053.1010897601027 -120.761394,49.14729,2056.6673792000342 -120.761398,49.147322,2056.9699635200313 -120.761406,49.147344,2057.1228108800337 -120.761424,49.14737,2057.224889599971 -120.761442,49.147389,2057.265992959938 -120.762139,49.148118,2058.180249919883 -120.762172,49.148165,2058.835164799911 -120.762193,49.1482,2059.3808960000915 -120.76221,49.148219,2059.636510399979 -120.762268,49.148265,2060.070140799985 + + + 0 + clampToGround + -120.762268,49.148265,2060.070140799985 -120.762289,49.148274,2060.048549440059 -120.762324,49.148284,2059.900272639866 -120.762384,49.148307,2059.7392115200796 -120.762434,49.148337,2059.775663679976 -120.762464,49.148367,2059.979092480066 -120.762495,49.148416,2060.536643199942 -120.762517,49.148442,2060.894800000042 -120.762543,49.148458,2061.0891999999862 -120.762578,49.148469,2061.1611999999577 -120.762654,49.14849,2061.2656000000525 -120.762733,49.148522,2061.5572000000066 -120.762879,49.148604,2062.4982713599848 -120.763047,49.148728,2064.5685606399643 -120.763487,49.149099,2068.500440480016 -120.763517,49.149123,2068.8120593601607 -120.763535,49.149145,2069.219371999872 -120.763553,49.149181,2069.9887999999482 -120.76369,49.149468,2075.572000000013 -120.763717,49.1495,2075.8564000000615 -120.763736,49.149517,2075.9572000001144 -120.763765,49.149524,2075.769999999922 -120.763804,49.149531,2075.47480000013 -120.76395,49.149534,2073.952000000112 -120.76398,49.149546,2073.8439999999755 -120.764,49.149566,2073.988000000047 -120.764042,49.149608,2074.2903999999744 -120.764067,49.149627,2074.362400000074 -120.764112,49.149645,2074.2004000000225 -120.764406,49.149787,2074.902549760073 -120.764506,49.149837,2074.8879651199927 -120.764545,49.149863,2074.9798616000508 -120.764577,49.149902,2075.296043839953 -120.764635,49.150001,2076.335600000074 -120.76477,49.150297,2082.472497600047 -120.764802,49.150349,2083.721161919961 -120.764849,49.1504,2084.8791839999476 -120.76513,49.150636,2088.554867199987 -120.765502,49.15098,2091.3047999999644 -120.765875,49.151363,2093.799620000029 -120.766238,49.151634,2096.2216000000144 -120.766275,49.151658,2096.5275999999585 -120.766316,49.151675,2096.6397280000438 -120.766379,49.151687,2096.742194079974 -120.766467,49.151699,2096.6835316799957 -120.7665,49.151709,2096.603760000027 -120.766529,49.151728,2096.4489715200175 -120.766553,49.151752,2096.2598937599855 -120.766567,49.151781,2096.029117919995 -120.766583,49.151832,2095.6303257600034 -120.766594,49.151864,2095.3933593599672 -120.766622,49.151897,2095.2082646400086 -120.766691,49.15195,2094.9252560000073 -120.766791,49.152029,2094.1911483200033 -120.7671,49.152235,2093.913520000017 -120.767126,49.152266,2093.7332147200214 -120.767142,49.152298,2093.5696307200033 -120.767158,49.15234,2093.379942399972 -120.767187,49.152422,2093.09841088003 -120.767234,49.152614,2090.530001920003 -120.767254,49.152692,2088.896546559954 -120.767266,49.152728,2088.1763161599474 -120.76728,49.152757,2087.6256831999754 -120.767314,49.152807,2086.417432319907 -120.767637,49.153091,2084.1901292797725 -120.767903,49.153334,2085.7824457600277 -120.767921,49.153357,2085.342187360026 -120.767936,49.15339,2084.586995200048 -120.76794,49.153414,2083.9407807999883 -120.767956,49.153534,2080.732875519943 -120.767965,49.153558,2080.2433936001808 -120.767982,49.153581,2079.99131296001 -120.768011,49.153607,2079.9879217600082 -120.768094,49.153672,2078.8253491200753 -120.768256,49.153803,2076.899925119724 -120.768297,49.153827,2077.0753689601124 -120.768349,49.153853,2077.422939360073 -120.768391,49.153865,2077.7584391998007 -120.76843,49.153873,2078.1950831999925 -120.768477,49.153876,2078.9316057601613 -120.768694,49.153873,2082.1359891200077 -120.76875,49.153881,2082.585199999988 -120.768809,49.153901,2083.1055638400594 -120.768858,49.153921,2083.704871679923 -120.768885,49.153942,2084.133139199963 -120.768912,49.153971,2084.369381759994 -120.768933,49.154001,2084.5958750400227 -120.768976,49.154052,2085.1396377599685 -120.769055,49.154139,2086.5157175999534 -120.769428,49.15444,2090.850547200042 -120.770173,49.154871,2096.31757168 -120.770423,49.155061,2099.6208000000397 -120.770452,49.155087,2100.193200000053 -120.770475,49.155115,2100.7800000000548 -120.770502,49.15517,2101.8671999999447 -120.770524,49.155226,2102.9543999999623 -120.770543,49.155259,2103.6168000000293 -120.770578,49.155286,2104.2607916799293 -120.770667,49.155337,2105.7659358400815 -120.771194,49.155618,2113.533719679985 -120.771231,49.155644,2113.704578560029 -120.771249,49.155667,2113.909244320027 -120.771261,49.155698,2114.2348931200368 -120.771324,49.155843,2115.5982707199137 -120.771329,49.155875,2115.9988600000615 -120.77134,49.155901,2116.2835264000323 -120.771357,49.155927,2116.5396894400087 -120.771685,49.156322,2121.7458928000387 -120.771709,49.156351,2122.18638736001 -120.771741,49.156373,2122.4617067200156 -120.771894,49.156447,2123.2275999999883 -120.771955,49.156489,2123.7396951999776 -120.772039,49.156554,2124.493533759964 -120.772445,49.156918,2128.8171999999463 -120.772671,49.157144,2130.4368000000172 -120.772721,49.157189,2130.7608000000178 -120.772779,49.15722,2130.979635200017 -120.773043,49.15731,2130.0594831999942 -120.773078,49.157331,2129.824276160015 -120.773095,49.157359,2129.6482376000195 -120.773091,49.157394,2129.635676480069 -120.773072,49.157504,2129.7343999999916 -120.773081,49.157529,2129.4247999999548 -120.773099,49.157544,2129.057599999961 -120.773129,49.157555,2128.546400000024 -120.773242,49.157592,2126.652800000047 -120.773421,49.157643,2123.6986582400123 -120.773655,49.157737,2118.955199999965 -120.77369,49.157752,2118.235199999941 -120.773718,49.157756,2117.774399999979 -120.773751,49.157747,2117.4287999999306 -120.773801,49.157728,2116.982399999932 -120.773824,49.157726,2116.6799999999794 -120.773856,49.157736,2116.075200000074 -120.773902,49.157766,2115.1223999999424 -120.773971,49.1578,2114.4171039999646 -120.774112,49.157847,2113.048885120041 -120.774309,49.157908,2110.663173760004 -120.77446,49.157921,2109.1956735998956 -120.774522,49.157925,2108.3211759999726 -120.774592,49.157939,2107.1207084800444 -120.77471,49.157995,2104.44959200002 -120.774756,49.158034,2103.091363840025 -120.774783,49.158065,2102.139599999995 -120.774792,49.158099,2101.3079999999454 -120.774786,49.158135,2100.595199999904 -120.77475,49.158167,2100.292800000053 -120.774709,49.158188,2100.236608639931 -120.774628,49.158227,2099.8956915200383 -120.774552,49.158285,2098.8700544000176 -120.77445,49.158404,2096.2042880000627 -120.774441,49.158438,2095.3561107200753 -120.774446,49.158458,2094.855313279927 -120.774458,49.158474,2094.4311123200523 -120.774483,49.15849,2093.991483199983 -120.774513,49.158504,2093.604033919986 -120.77453,49.158526,2093.0504287999547 -120.774533,49.158553,2092.3977070400506 -120.774519,49.158593,2091.4389003199203 -120.774526,49.158637,2090.736654080005 -120.774555,49.158671,2090.4808352000355 -120.774624,49.158708,2090.506657280029 -120.774662,49.15872,2090.6600576000287 -120.774699,49.158723,2090.8653036799874 -120.77486,49.158725,2091.1140799999866 -120.775079,49.158718,2090.520074239963 -120.775149,49.158737,2090.2365929600387 -120.775194,49.158762,2090.2931417600535 -120.775228,49.158802,2090.727307520033 -120.775236,49.158841,2091.340257920009 -120.775234,49.158886,2092.1088780800515 -120.775217,49.15893,2093.0744175999653 -120.775188,49.158957,2093.7151513600184 -120.77515,49.158982,2094.317008000043 -120.775119,49.158996,2094.6647910400293 -120.775035,49.159005,2095.016668000004 -120.774997,49.159023,2095.376757279947 -120.774994,49.15904,2095.6415488000403 -120.77501,49.159052,2095.9211392000557 -120.775054,49.15906,2096.005350399941 -120.775159,49.159069,2096.040744160044 -120.775204,49.159085,2096.3140863999456 -120.77524,49.1591,2096.5926400000503 -120.775363,49.159172,2096.9071999999705 -120.775465,49.15926,2097.6560000001264 -120.775653,49.159488,2100.0850054399657 -120.775682,49.159567,2101.130834239998 -120.775692,49.159642,2102.4493414398617 -120.775716,49.159686,2102.9622809599728 -120.775816,49.159777,2103.6299999998696 -120.775836,49.159844,2105.025008640021 -120.775841,49.159897,2106.2837659200245 -120.77585,49.159939,2107.209623999952 -120.775865,49.159974,2107.8781296000147 -120.776065,49.160145,2109.231347999966 -120.776246,49.160263,2111.116166080011 -120.776286,49.160281,2111.460193279939 -120.776331,49.16029,2111.534739200094 -120.776586,49.160352,2111.4512000001178 -120.776667,49.160388,2111.7747238399093 -120.776739,49.160439,2112.3908638398607 -120.776857,49.160572,2114.2995999999684 -120.777113,49.160925,2119.413043999966 -120.77715,49.160965,2120.1827599998405 -120.77719,49.161003,2120.890347199974 -120.777235,49.161025,2121.04026000001 -120.777308,49.161045,2120.6962943999774 -120.778092,49.161264,2117.965975040083 -120.778244,49.161333,2117.868988159957 -120.778374,49.161412,2117.992439039965 -120.778831,49.161763,2118.299326880084 -120.778947,49.161844,2118.908800000035 -120.779047,49.1619,2119.2832000000108 -120.779514,49.162089,2116.5108918399433 -120.779546,49.162112,2116.539470080073 -120.779565,49.162133,2116.721000799847 -120.77959,49.162179,2117.3723344000628 -120.7796,49.162193,2117.5225120000105 -120.779615,49.162203,2117.488088799835 -120.779635,49.162211,2117.314914400001 -120.779656,49.162216,2117.0522598399684 -120.779747,49.162228,2115.718544639962 -120.780124,49.162403,2113.1889657599345 -120.780676,49.162696,2112.038051839918 -120.781077,49.162836,2111.0452000000014 -120.781158,49.162874,2110.9271999999746 -120.781222,49.162922,2111.1576000000578 -120.781269,49.162955,2111.261999999999 -120.781324,49.162993,2111.3664000000426 -120.781383,49.163025,2111.225999999834 -120.781844,49.163223,2108.027599999934 -120.781882,49.163245,2107.8980000001584 -120.781898,49.163268,2108.189600000087 -120.781921,49.163303,2108.6576000000314 -120.782031,49.163477,2110.18915951995 -120.782069,49.163526,2110.53701023984 + + + 0 + clampToGround + -120.782069,49.163526,2110.53701023984 -120.78207,49.163527,2110.5411343999695 -120.78209,49.16354,2110.489856000004 -120.782109,49.163549,2110.3799393599825 -120.782168,49.163559,2109.691819520007 -120.78224,49.163561,2108.609145600002 -120.782342,49.163568,2106.979722239961 -120.782501,49.163598,2104.74943008006 -120.782782,49.163665,2101.9121488000665 -120.783107,49.163771,2099.8699977599267 -120.783373,49.163868,2098.26006144004 -120.78364,49.163992,2097.0034047999147 -120.783879,49.164122,2095.3441244799988 -120.784178,49.16437,2094.3008000000273 -120.784389,49.164575,2091.892211999928 -120.784573,49.164768,2089.1163916801274 -120.78503,49.165426,2082.675999999934 -120.785383,49.165936,2080.4756000000807 -120.785456,49.166067,2079.6332000000584 -120.785505,49.166181,2079.7563824000094 -120.785638,49.166884,2074.1399999998416 -120.785956,49.167495,2068.324451200039 -120.785979,49.167568,2067.8655462400493 -120.785988,49.167634,2067.7140006399413 -120.785937,49.167948,2067.6657030399188 -120.785852,49.168333,2067.598080639892 -120.785848,49.168371,2067.4119999999657 -120.785854,49.168395,2067.1096000000125 -120.785869,49.16842,2066.605599999904 -120.785894,49.168438,2065.936000000009 -120.786093,49.168516,2061.076000000154 -120.78613,49.168537,2060.107457599996 -120.786156,49.168563,2059.348810879887 -120.786167,49.168593,2058.910081759859 -120.786168,49.168619,2058.7260326399264 -120.786237,49.169082,2054.79679999998 -120.786239,49.169132,2054.408000000092 -120.786223,49.169179,2054.406684319903 -120.786186,49.169261,2055.013156159989 -120.78619,49.169276,2054.9757823999407 -120.786199,49.169289,2054.873742560046 -120.786215,49.169299,2054.6821736000215 -120.786237,49.169304,2054.4112620799965 -120.786281,49.169311,2053.8713873599704 -120.786358,49.169325,2052.9514159999158 -120.786417,49.169341,2052.222800000008 -120.786607,49.169407,2049.724399999947 -120.78682,49.169507,2046.8147103999268 -120.786869,49.169538,2046.2769251200982 -120.786902,49.169567,2046.0201846400437 -120.78692,49.169593,2045.9973376000705 -120.786934,49.169627,2046.134889279948 -120.786939,49.169717,2047.0223684799291 -120.786943,49.169733,2047.2148000001048 -120.786954,49.169749,2047.3510323201324 -120.786972,49.169754,2047.1978969598752 -120.786997,49.169749,2046.7616777600656 -120.787048,49.169725,2045.566256000013 -120.787211,49.169621,2042.4180782400056 -120.787357,49.169551,2040.4977227199854 -120.787509,49.169492,2038.4543468799802 -120.787901,49.169377,2034.3697878399241 -120.787944,49.169374,2034.0836915199786 -120.787982,49.16938,2033.9956671999387 -120.788006,49.169391,2034.0926483199685 -120.788021,49.169407,2034.3491382399902 -120.788061,49.169488,2035.770546560024 -120.788083,49.169508,2036.0904108800075 -120.788101,49.169514,2036.170330879995 -120.788121,49.169515,2036.1540847999336 -120.788167,49.16951,2035.9681664000493 -120.788208,49.169499,2035.6487686400462 -120.788291,49.169475,2034.8888719999609 -120.788405,49.169432,2033.719641600011 -120.789575,49.169402,2031.7149519999423 -120.78992,49.169404,2032.1523327999892 -120.790016,49.169398,2032.1704307200378 -120.790102,49.169384,2031.6863027200368 -120.790225,49.169364,2031.0489760000664 -120.790319,49.169359,2030.878352160005 -120.79041,49.169364,2030.9661504000671 -120.79077,49.169438,2032.9728 -120.790843,49.169452,2033.2296000000224 -120.790925,49.169457,2032.8372000000413 -120.791095,49.16945,2031.3359999999177 -120.791655,49.169488,2026.0430143999715 -120.791852,49.169512,2024.9167999998272 -120.791963,49.169539,2025.0165838400303 -120.792064,49.169568,2025.763274239947 -120.792551,49.169715,2027.3987999998644 -120.79272,49.169769,2028.0016544000541 -120.792846,49.169832,2028.5744000000243 -120.793318,49.170079,2028.8199011200506 -120.793844,49.170295,2032.6056192000494 -120.79394,49.170318,2032.618956799994 -120.794027,49.17033,2032.2281263999985 -120.794443,49.170345,2030.9705167999623 -120.794534,49.170346,2031.0672556800218 -120.794634,49.170364,2031.2885491199957 -120.794731,49.17039,2031.4544335999844 -120.794856,49.170453,2032.290206719983 -120.794928,49.170502,2032.6908262399936 -120.795014,49.170568,2032.6706841599853 -120.795081,49.170637,2030.7798057600507 -120.795126,49.170693,2029.169917440041 -120.795173,49.170767,2027.000249279978 -120.795189,49.170804,2025.9501004801612 -120.795203,49.170821,2025.3636950400692 -120.795219,49.170831,2024.9016451199632 -120.795243,49.170836,2024.4540019199876 -120.795268,49.170836,2024.0931379200777 -120.795305,49.170829,2023.7187288000732 -120.795507,49.170762,2022.7087113598955 -120.795552,49.170758,2022.2173286400107 -120.795598,49.170758,2021.7526393600378 -120.795631,49.170764,2021.2497913600469 -120.795666,49.170777,2020.510233279997 -120.795697,49.1708,2019.4935039999625 -120.795751,49.170846,2017.6291158401013 -120.79591,49.170957,2013.347675199891 -120.796276,49.171146,2004.7807999998906 -120.796333,49.171178,2003.524399999942 -120.796369,49.171206,2002.6748000000407 -120.796562,49.171416,1998.140848640067 -120.79659,49.171427,1997.6166656000528 -120.79663,49.171428,1997.068428800104 -120.796688,49.171424,1996.4671999999578 -120.796732,49.171436,1996.0207999999593 -120.796763,49.171459,1995.5492000000245 -120.796814,49.171511,1994.6203999999907 -120.796895,49.171597,1993.108399999971 -120.796992,49.171727,1990.5716000000307 -120.797015,49.171777,1989.5060000000099 -120.79703,49.171831,1988.4259999999995 -120.797049,49.171998,1985.355769919957 -120.797089,49.172077,1983.8215348799758 -120.797099,49.172103,1983.3507931199867 -120.797094,49.172128,1982.9749747199821 -120.797078,49.172147,1982.7429993599637 -120.797043,49.172166,1982.585388480038 -120.796868,49.172261,1981.8523999999952 -120.796857,49.172279,1981.6075999999225 -120.796855,49.172293,1981.3699999999853 -120.796857,49.172305,1981.1395999999531 -120.796866,49.172329,1980.642800000082 -120.796904,49.172378,1979.487199999981 -120.79692,49.172422,1978.580000000046 -120.796921,49.172459,1977.9067999999552 -120.796928,49.172671,1972.882087040041 -120.796939,49.172724,1971.4260108800104 -120.796958,49.172765,1970.133534399994 -120.797027,49.172919,1965.0461929599937 -120.79707,49.17301,1962.2597440000116 -120.797076,49.173092,1960.7213363200003 -120.797065,49.173159,1960.1764215999956 -120.797022,49.173264,1959.3999116799762 -120.796958,49.173375,1958.3438799999842 -120.796788,49.173603,1956.82284543993 -120.796738,49.17368,1955.862227199991 -120.796705,49.173748,1954.9859871998951 -120.796695,49.173825,1953.68691999999 -120.796711,49.174088,1951.5996012799774 -120.796727,49.174151,1951.1753499199335 -120.796756,49.174199,1950.2854342399512 -120.796789,49.17425,1949.1705199999385 -120.796797,49.174332,1948.0216678400284 -120.796801,49.174485,1945.9699888000164 + + + 0 + clampToGround + -120.796801,49.174485,1945.9699888000164 -120.796802,49.174508,1945.6290572799626 -120.796781,49.17456,1945.1747488000642 -120.796751,49.174609,1944.9522947200367 -120.796735,49.17466,1944.5522079999928 -120.796748,49.174704,1943.7712153601292 -120.796782,49.174751,1942.8809852801237 -120.797143,49.175486,1934.6745859198638 -120.797159,49.175522,1934.4449059201645 -120.797162,49.175558,1934.5264000000047 -120.797132,49.175672,1935.6927999998734 -120.797145,49.175709,1935.4983999999806 -120.797191,49.17574,1934.450800000042 -120.797226,49.175752,1933.6488179200892 -120.797294,49.175768,1932.670424320086 -120.797368,49.175803,1931.734291839924 -120.797756,49.176024,1927.7402137598717 -120.797792,49.176063,1927.4576678399783 -120.797819,49.17611,1927.3989935999505 -120.797872,49.176219,1927.9333094400706 -120.797894,49.176256,1927.9763251200352 -120.797934,49.176296,1927.664133120047 -120.798105,49.176424,1925.8975007998524 -120.798157,49.176466,1925.8258204799117 -120.798189,49.176501,1925.9025105601306 -120.798226,49.176548,1926.0581299199555 -120.798267,49.176613,1926.3750638401252 -120.798509,49.176893,1925.6990409600849 -120.798588,49.176952,1924.4969369601033 -120.798672,49.176989,1922.9743193600611 -120.798763,49.177017,1921.5516483200956 -120.798886,49.177041,1919.7399699200812 -120.79902,49.177066,1918.665747200043 -120.799099,49.177093,1918.2049227200087 -120.799162,49.177124,1917.9787404800293 -120.799219,49.177166,1918.0160000000424 -120.799565,49.177529,1915.7433095999872 -120.799996,49.178076,1912.9685401600834 -120.80004,49.178121,1912.2562528000622 -120.800096,49.178158,1911.503714560039 -120.800256,49.178238,1909.8674137599573 -120.8003,49.178277,1909.540976000087 -120.800329,49.17833,1909.4325871999572 -120.80051,49.178868,1910.2527999999852 -120.800569,49.17899,1910.5651824000422 -120.80071,49.179204,1909.9851264000597 -120.800765,49.179419,1910.5773336000361 -120.800781,49.179465,1910.7271415999965 -120.8008,49.179492,1910.722944000079 -120.800829,49.179508,1910.5079692800687 -120.800896,49.179526,1909.684800000099 -120.801057,49.179559,1907.6039999999807 -120.801139,49.179586,1906.7179999999357 -120.801204,49.179624,1906.2895999999942 -120.801413,49.179772,1905.3640000000225 -120.801755,49.180048,1906.2553503999918 -120.802198,49.180285,1908.2149327999466 -120.802267,49.180319,1908.8792779199534 -120.80234,49.18034,1909.44902400002 -120.80242,49.180353,1909.8731903999428 -120.802503,49.180358,1910.1705619199827 -120.803352,49.18037,1913.664000000025 -120.803399,49.180377,1913.7144 -120.803442,49.180395,1913.8439999999798 -120.803514,49.18044,1914.1679999999803 -120.803547,49.180458,1914.2976000000112 -120.803581,49.180472,1914.3984000000125 -120.80395,49.180577,1913.2485839999947 -120.804005,49.180593,1913.4607464000237 -120.804066,49.180606,1913.6605881599696 -120.804275,49.180632,1914.1008000000402 -120.804346,49.180652,1914.3888000000293 -120.804409,49.180677,1914.7488000000412 -120.804838,49.180889,1917.3847999999941 -120.804874,49.180907,1917.5143999999739 -120.804917,49.180919,1917.5324000000307 -120.804955,49.180921,1917.4243999999453 -120.805004,49.180921,1917.2623999999696 -120.805543,49.180906,1916.341375680004 -120.80628,49.180907,1917.3259999999586 -120.806328,49.180912,1917.4159999999874 -120.806365,49.180916,1917.4880000000612 -120.806437,49.180951,1917.7715999999873 -120.80658,49.181028,1918.1279999999829 -120.806703,49.181089,1918.5707649600022 -120.807032,49.181211,1921.1134918399027 -120.80726,49.181287,1922.6126847999226 -120.807421,49.18133,1923.5281072000648 -120.807699,49.181395,1925.9206392000642 -120.807798,49.181412,1926.8155430400718 -120.80787,49.18142,1927.3028160000686 -120.808044,49.181427,1928.0617075199664 -120.808104,49.181443,1928.732800000046 -120.80815,49.181464,1929.5031999999426 -120.80831,49.181559,1932.8152000000218 -120.808338,49.18157,1933.2328000000418 -120.808362,49.181579,1933.5783999999878 -120.808393,49.181585,1933.8627999999349 -120.808428,49.181589,1934.1040000000933 -120.808656,49.181601,1935.1470022400154 -120.80871,49.181605,1935.3030319999373 -120.808769,49.181619,1935.7247374399387 -120.808851,49.181646,1936.4690518400175 -120.808972,49.181697,1937.3517273600146 -120.809099,49.181742,1937.7228643199683 -120.80918,49.181768,1937.804889599966 -120.809274,49.181789,1937.5802294400362 -120.809496,49.181828,1936.556610560111 -120.809594,49.181868,1936.3645030399812 -120.80967,49.181909,1936.1696335999968 -120.809697,49.181935,1936.2179384000453 -120.809714,49.181966,1936.5862969600653 -120.80972,49.182074,1938.834131200023 -120.809737,49.1821,1939.170607999986 -120.809761,49.18212,1939.2849728000201 -120.809804,49.182143,1939.1951628800753 -120.80987,49.182165,1938.7455919999047 -120.809946,49.182195,1938.2685488000511 -120.810009,49.182232,1938.046400000073 -120.810074,49.182292,1938.1903999999395 -120.810137,49.182378,1938.8312000000356 -120.810201,49.182487,1939.8715999999413 -120.810219,49.182515,1940.158973599995 -120.81024,49.182534,1940.261753600029 -120.810273,49.182547,1940.081089760058 -120.810319,49.182556,1939.6758348798692 -120.810569,49.182584,1937.2255999999434 -120.810629,49.182599,1936.7936000000109 -120.81068,49.182622,1936.721599999988 -120.810754,49.182667,1936.9520000000202 -120.811786,49.183307,1935.2291260800187 -120.811865,49.183341,1934.705999999998 -120.811946,49.183362,1934.0208000000453 -120.812123,49.183406,1934.3303999999287 -120.812211,49.18343,1934.560800000012 -120.812289,49.18346,1934.9484223998122 -120.813068,49.183863,1944.6701260800785 -120.814095,49.184347,1950.3969064001499 -120.814556,49.184601,1954.5288582401074 -120.814859,49.184823,1955.812885440084 -120.815439,49.185291,1959.4987729600225 -120.81572,49.185534,1962.5113215999127 + + + 0 + clampToGround + -120.81572,49.185534,1962.5113215999127 -120.815857,49.185652,1963.7424000000312 -120.816117,49.185979,1965.9915999999469 -120.816165,49.186034,1965.9196000000263 -120.816211,49.186102,1966.0708000000539 -120.81626,49.186248,1966.3055999999976 -120.816535,49.186579,1966.001200000007 -120.816581,49.186637,1966.296399999978 -120.816629,49.186681,1966.632399999959 -120.816827,49.18681,1969.3141648000224 -120.816892,49.186879,1970.7307187199576 -120.816932,49.186923,1971.578141440023 -120.81697,49.186979,1972.7673552000313 -120.817013,49.187073,1974.8785809599374 -120.817045,49.187143,1976.3828424000174 -120.817077,49.187184,1977.1480627200633 -120.817114,49.187222,1977.7740883199378 -120.817241,49.187319,1978.9316481600843 -120.817292,49.18734,1978.9350912000236 -120.817361,49.187366,1978.8478070398974 -120.817439,49.187387,1978.5354667200843 -120.817608,49.187415,1978.3510272000203 -120.818366,49.187549,1982.7639999998948 -120.819234,49.187754,1990.4927494399217 -120.819282,49.18776,1990.8657727999198 -120.819322,49.187762,1991.079762559978 -120.819365,49.18776,1991.1836959999366 -120.819413,49.187755,1991.2215176000707 -120.819823,49.187685,1991.6908000000524 -120.819874,49.187675,1991.5863999999578 -120.819917,49.187667,1991.510799999944 -120.81996,49.187665,1991.6080000000568 -120.820007,49.187663,1991.7048126400225 -120.820971,49.187675,1992.712171999983 -120.821017,49.187683,1992.5384014400117 -120.821049,49.187696,1992.498172160016 -120.82108,49.187712,1992.4870784000059 -120.821101,49.187731,1992.5614702400019 -120.821191,49.187853,1991.866400000023 -120.821315,49.188048,1989.8252000000093 -120.821387,49.188204,1988.4859999999885 -120.821417,49.188295,1987.862331200047 -120.82143,49.188371,1987.4203999999418 -120.821441,49.188496,1986.8516000000734 -120.82146,49.188602,1986.264799999969 -120.821495,49.188719,1985.5572624000197 -120.821748,49.189391,1977.4796652800103 -120.821857,49.18978,1975.3246784000853 -120.821862,49.189862,1975.3365497600648 -120.821851,49.189927,1975.5935940800782 -120.821838,49.189983,1975.892548160004 -120.821828,49.190012,1976.243709440101 -120.821841,49.190029,1976.2140785599174 -120.821867,49.190046,1975.9369692800753 -120.821937,49.190083,1975.0384078399102 -120.822086,49.190217,1973.6205014400077 -120.822212,49.190337,1971.6393542401368 -120.822272,49.190411,1971.2424000000087 -120.82231,49.190476,1971.163199999953 -120.822356,49.190612,1971.4800000000228 -120.822361,49.190672,1971.8399999999835 -120.822378,49.190697,1971.7751999999937 -120.822412,49.190713,1971.400800000018 -120.82246,49.190723,1970.7815999998932 -120.822546,49.19073,1969.4896031999692 -120.822616,49.19076,1968.4942464000453 -120.822667,49.1908,1967.8261440000922 -120.822707,49.190839,1967.2740000001065 -120.822779,49.190925,1965.9897480000247 -120.822828,49.190963,1965.5540025599869 -120.822884,49.19099,1965.0195263999829 -120.822972,49.191016,1964.1418060800736 -120.823053,49.191065,1963.1920727999773 -120.823171,49.191184,1962.1246534399784 -120.823543,49.191486,1961.3121561599544 -120.824231,49.191938,1964.2231462400316 -120.824354,49.192029,1963.58559999999 -120.824421,49.192096,1963.3443999999852 -120.824493,49.192176,1963.2009734399828 -120.824512,49.192236,1963.4808627200332 -120.824525,49.192251,1963.5744440000428 -120.824547,49.192262,1963.5180694400385 -120.824592,49.192273,1963.2347033600474 -120.824694,49.192294,1962.5705465600056 -120.824799,49.192323,1962.7137220800323 -120.825254,49.192502,1964.1287999999424 -120.825349,49.192523,1964.2191700800167 -120.825432,49.192525,1963.9348320000413 -120.825496,49.192518,1963.552293119991 -120.825593,49.192499,1962.999514719999 -120.826081,49.192362,1962.0602531200154 -120.826223,49.192327,1961.7543999999914 -120.826296,49.192314,1961.660800000023 -120.826371,49.19231,1961.6319999999935 -120.826561,49.192313,1961.8560852799849 -120.826652,49.192313,1961.9631449599804 -120.826725,49.192306,1962.2614639999974 -120.826886,49.192262,1962.8957292800078 -120.827602,49.192117,1965.5980000000263 -120.827656,49.192114,1965.954399999996 -120.827689,49.192115,1966.2028000000596 -120.827723,49.192119,1966.4908000000232 -120.82776,49.192128,1966.8543999999492 -120.82787,49.192174,1968.4752000000617 -120.827895,49.192186,1968.8747999999737 -120.827926,49.192188,1969.2312000000716 -120.827953,49.19218,1969.4363999999373 -120.82798,49.192162,1969.5335999999988 -120.828034,49.19212,1969.6632000000552 -120.828623,49.191623,1973.61399999995 -120.828699,49.191577,1973.6644000000274 -120.828803,49.191533,1973.9379999999505 -120.829536,49.191333,1977.0556000000345 -120.830684,49.190947,1976.99280000001 -120.83078,49.190926,1976.1935999999303 -120.830882,49.190919,1975.6752000000113 -120.831767,49.190855,1979.5515999999727 -120.831883,49.190851,1980.7180000000974 -120.832001,49.190826,1981.2698249600617 -120.832531,49.190738,1981.0140988800772 -120.833178,49.190674,1981.3607142400497 -120.833807,49.190624,1981.676399999966 -120.834699,49.190639,1982.8275134399412 -120.835045,49.190674,1983.6513232000154 -120.835657,49.190788,1987.712399999959 -120.836158,49.190895,1990.2413263999513 -120.836794,49.191131,1992.3308214399995 -120.836941,49.191212,1992.4730963200261 -120.837067,49.1913,1992.5375839999326 -120.83747,49.191754,1991.094755200083 -120.837564,49.191854,1990.6575999999905 -120.83765,49.19193,1990.276000000033 -120.838019,49.192169,1987.1431999999593 -120.83848,49.192446,1980.0629567999054 -120.838834,49.192739,1974.736384959916 -120.838864,49.192782,1974.4770380799907 -120.838889,49.19282,1974.3016608000946 -120.838925,49.19294,1974.4639199999374 -120.838948,49.193009,1974.5651347199905 -120.838974,49.193052,1974.4384780801352 -120.839004,49.193088,1973.9764019199563 -120.839255,49.193334,1971.054836800063 -120.839294,49.19337,1971.092291200073 -120.83932,49.193421,1971.4321888000004 -120.839342,49.193642,1973.8789894399322 -120.839371,49.193703,1974.2933364799921 -120.83942,49.193765,1974.5192479999366 -120.839916,49.194162,1973.7319590399256 -120.840039,49.194222,1973.8573292800816 -120.840155,49.194257,1973.5025536000778 -120.840358,49.194289,1971.402424960011 -120.840463,49.194302,1970.2474700800137 -120.840544,49.194322,1969.5265254400788 -120.840624,49.194357,1969.3719999998677 -120.840718,49.194436,1970.0632000001178 -120.840804,49.194532,1972.1868851200102 -120.840871,49.194574,1972.9264000000437 -120.840975,49.194599,1972.9875999998892 -120.841134,49.194623,1972.649833280019 -120.841979,49.1947,1968.385647999963 -120.84218,49.194727,1968.2697856000111 -120.842338,49.194762,1968.2881715199337 -120.843422,49.195128,1970.8071999998901 -120.843931,49.195265,1968.710052799981 -120.844049,49.195289,1969.3807134400847 -120.844161,49.195298,1969.6566851200353 -120.844342,49.195319,1970.7794598399323 -120.844535,49.195361,1972.2278607999526 -120.844757,49.195437,1973.6507060799636 -120.844933,49.195516,1973.3357593600017 -120.844989,49.195556,1973.120336640031 -120.845034,49.195605,1973.0338128000087 -120.845164,49.195783,1972.7449804800183 -120.8452,49.195828,1972.5546239999596 -120.845231,49.195852,1972.40400000002 -120.845271,49.195859,1972.1412000000184 -120.845381,49.19586,1971.3527999999883 -120.845425,49.195871,1971.0755999999462 -120.845461,49.195893,1970.8955999999914 -120.845731,49.196074,1970.2348000000084 -120.845777,49.19611,1970.198799999997 -120.845822,49.196136,1969.9511999999963 -120.845873,49.19615,1969.5943920000082 -120.845935,49.19616,1969.1564160000205 -120.845996,49.196175,1968.733488000008 -120.846152,49.196231,1967.5986739199705 -120.846278,49.196285,1965.8947967999968 -120.846333,49.196306,1964.9594476799832 -120.846388,49.196316,1964.0622412798912 -120.846646,49.196336,1961.7341657599204 -120.846707,49.196353,1960.8443196800438 -120.846724,49.196372,1960.3818982401149 -120.846723,49.196393,1960.1000000000295 -120.846723,49.196393,1960.1000000000295 + + + 0 + clampToGround + -120.846723,49.196393,1960.1000000000295 -120.846702,49.196419,1959.8408000001211 -120.846672,49.196439,1959.84080000007 -120.846359,49.196624,1959.3059999998802 -120.846339,49.196646,1959.269999999971 -120.846328,49.196673,1959.0454022400286 -120.846326,49.196699,1958.8728470398964 -120.846336,49.196746,1958.3264217601263 -120.846422,49.196915,1955.7478351999662 -120.846485,49.197017,1953.661575199951 -120.846521,49.197108,1952.0224332800528 -120.846541,49.197207,1950.546391519993 -120.846542,49.197277,1949.0545033600133 -120.84652,49.197385,1946.7914080000928 -120.846491,49.197427,1946.2030052800512 -120.846459,49.197456,1945.9807801600007 -120.846341,49.197518,1946.2788000000091 -120.846322,49.197528,1946.2968000000405 -120.84632,49.197541,1945.911599999862 -120.846328,49.197549,1945.5083999999333 -120.846347,49.197558,1944.874800000075 -120.846485,49.19757,1942.348000000017 -120.84651,49.197577,1941.7611999999895 -120.84652,49.197592,1941.1312000000196 -120.84652,49.197607,1940.6452000000954 -120.846507,49.197621,1940.3788000000263 -120.846479,49.197635,1940.3284000000258 -120.846298,49.197695,1941.317999999821 -120.846181,49.197741,1941.9335999999548 -120.846134,49.197763,1942.0667999998232 -120.846089,49.197792,1941.984324479895 -120.846058,49.197819,1941.7404259199345 -120.846038,49.197846,1941.2865580801677 -120.846009,49.197906,1939.9755158399819 -120.845964,49.198045,1936.4425248000575 -120.84595,49.19811,1935.6496799999727 -120.845935,49.198151,1935.3794424000089 -120.845915,49.198188,1935.2534207998967 -120.845866,49.198245,1935.4033967999849 -120.845858,49.198277,1935.1824086399104 -120.845854,49.198308,1934.9083852799813 -120.845861,49.198335,1934.4840000000108 -120.845874,49.198365,1933.926000000064 -120.845905,49.198432,1932.6444 -120.84601,49.198646,1928.1919999999395 -120.84605,49.198721,1926.121999999922 -120.846077,49.19879,1924.3940000000641 -120.846097,49.199009,1919.6596000000663 -120.846077,49.199084,1918.3996000001268 -120.846031,49.199142,1917.9748000000227 -120.845943,49.199238,1917.32978463988 -120.845755,49.199459,1915.2875768001395 -120.84535,49.199995,1906.6380000000308 -120.845321,49.200041,1905.6133665600753 -120.845299,49.200097,1904.0762788800753 -120.845283,49.200164,1901.9942995200672 -120.845237,49.20022,1900.5509343999715 -120.845185,49.200272,1899.2169472000578 -120.84486,49.20052,1893.8565120001326 -120.844844,49.200554,1892.7439449601782 -120.844843,49.200588,1892.0455846399611 -120.844866,49.200627,1891.037126720093 -120.844902,49.200658,1890.084287360011 -120.845334,49.200863,1884.723616320033 -120.845482,49.200959,1884.3146044799614 -120.845591,49.20107,1884.5764000000142 -120.845653,49.201192,1885.0425529599981 -120.845765,49.201429,1886.5775999999628 -120.845777,49.20169,1890.6549647999786 -120.845776,49.201708,1891.1596876801 -120.845787,49.20172,1891.5039744000483 -120.845806,49.201725,1891.6593360000898 -120.845865,49.20173,1892.1140159999682 -120.845929,49.201734,1892.8054348800322 -120.845986,49.20175,1893.7190399998935 -120.846058,49.201769,1894.7776761599657 -120.846323,49.201772,1898.0371462399437 -120.846375,49.201781,1898.8785799998757 -120.846418,49.201802,1899.8068243200105 -120.846525,49.20189,1902.2881199999824 -120.846623,49.201999,1903.7473481599654 -120.846781,49.202117,1904.4115999999744 -120.847107,49.202331,1905.5496356799604 -120.847158,49.202361,1905.6539075199419 -120.847211,49.202378,1905.8202563199861 -120.847305,49.202412,1906.4184128000313 -120.84737,49.202426,1906.880249599952 -120.84744,49.202459,1907.0946368000814 -120.847628,49.202568,1907.7799961600078 -120.848196,49.202814,1910.5342604799616 -120.848271,49.20285,1911.7345119999386 -120.848341,49.202862,1912.5795683200986 -120.84866,49.202885,1915.4920000000259 -120.848719,49.202887,1915.7475999999433 -120.848802,49.20291,1916.5432000000833 -120.849003,49.202999,1918.6111222399657 -120.849026,49.203026,1919.2565619200357 -120.849027,49.20306,1920.1359552000213 -120.84899,49.203085,1920.8865840000458 -120.848907,49.203129,1922.1324388799103 -120.848834,49.203164,1922.9256000000166 -120.848775,49.203201,1923.7788000000112 -120.84856,49.2034,1928.596160000099 -120.848538,49.203456,1930.1226291201092 -120.84853,49.203504,1931.5106048000537 -120.848543,49.203568,1933.4751449599405 -120.848564,49.203608,1934.7389004800646 -120.848907,49.20403,1940.3604784000595 -120.848955,49.204095,1940.573403999987 -120.848984,49.204154,1940.8516134400088 -120.849011,49.204275,1941.0240879999733 -120.849033,49.204352,1941.1358867199663 -120.849083,49.204412,1941.1379603200194 -120.849175,49.204473,1941.2117680000028 -120.84933,49.204557,1942.2869151999987 -120.849448,49.20461,1943.4096000000009 -120.849615,49.204674,1945.5335999999588 -120.849674,49.204722,1946.6495999999038 -120.849762,49.204832,1948.78059264004 -120.84982,49.204942,1950.7953024000026 -120.849858,49.205006,1952.0318419199587 -120.849904,49.205064,1952.84922623997 -120.850009,49.205156,1954.188000000017 -120.850447,49.205484,1957.6092000000094 -120.850537,49.205573,1958.0001950400135 -120.850591,49.205661,1958.476473919981 -120.850684,49.205848,1960.1643852800592 -120.850918,49.206144,1965.509688320026 -120.850956,49.206185,1966.1814655999622 -120.850975,49.20622,1966.7679200000375 -120.85097,49.206264,1967.4723967999644 -120.850938,49.206303,1968.0234934399723 -120.850814,49.206386,1968.9576761600288 -120.850791,49.206422,1969.6970339200727 -120.850784,49.206453,1970.3438099200735 -120.850787,49.206491,1971.1442843199907 -120.850802,49.206522,1971.8211462399954 -120.850967,49.206786,1977.1641529600274 -120.85109,49.206963,1979.4860000000376 -120.851122,49.206995,1979.9028655999825 -120.851168,49.207021,1980.3215571199548 -120.85132,49.207079,1981.0577312000016 -120.851364,49.20711,1981.4374015999938 -120.851407,49.207158,1982.140399999977 -120.851688,49.207461,1985.5103654400555 -120.852045,49.207857,1989.081242399891 -120.852163,49.20795,1989.4406159998782 -120.852222,49.208006,1989.932142719973 -120.852332,49.208155,1991.6043999999174 -120.852404,49.208241,1992.374799999942 -120.85249,49.208318,1992.831999999939 -120.852859,49.208563,1990.3108000000489 -120.853053,49.208724,1990.068799999899 -120.853206,49.208885,1990.7787648000292 -120.853479,49.209233,1992.1432000000493 -120.853718,49.209527,1992.1427999999978 + + + 0 + clampToGround + -120.853718,49.209527,1992.1427999999978 -120.853863,49.209705,1991.217599999955 -120.853898,49.209758,1990.8055999999906 -120.853921,49.209827,1990.3916000000127 -120.853895,49.210494,1995.2912000000094 -120.853901,49.210531,1995.6476000000816 -120.853914,49.210567,1995.8603999999682 -120.853973,49.210687,1995.8676000000269 -120.853988,49.210735,1995.932399999991 -120.853984,49.210787,1996.1484000000341 -120.853971,49.210838,1996.4255999999991 -120.853941,49.210877,1996.7819999999692 -120.853896,49.210929,1997.2931999999573 -120.853871,49.210955,1997.5668000000085 -120.853857,49.210989,1997.7899999999565 -120.853842,49.211036,1998.0671999999986 -120.853835,49.21121,1998.7439999999779 -120.853786,49.211356,1999.622400000011 -120.853751,49.211425,2000.0592039999822 -120.853704,49.211482,2000.3322726400238 -120.853688,49.21151,2000.3724543999886 -120.853674,49.211544,2000.3680652799983 -120.853647,49.211904,2000.2395884799662 -120.853657,49.212329,2001.9975028799674 -120.853649,49.212383,2002.372948320045 -120.853622,49.212435,2002.6012272000037 -120.853593,49.21249,2002.86514720003 -120.853587,49.212534,2003.158000000002 -120.853598,49.212585,2003.564799999998 -120.853807,49.212987,2007.1636000000176 -120.853837,49.213059,2007.6139999999857 -120.853855,49.213131,2007.4843999999803 -120.853858,49.213396,2006.5269132800074 -120.85385,49.213458,2006.2083679999375 -120.853818,49.213495,2005.7590736000288 -120.853719,49.213562,2004.4501308799627 -120.853678,49.213605,2003.7611024000157 -120.853665,49.213646,2003.3513984001077 -120.8537,49.213732,2003.4899840000687 -120.854226,49.214541,2013.205142079994 -120.854547,49.215084,2014.139199999959 -120.85477,49.215493,2011.8139999999903 -120.85479,49.215548,2011.1119999999466 -120.854794,49.215623,2009.7022790399567 -120.854792,49.216003,2005.5072000000719 -120.854797,49.216069,2005.6152000001061 -120.854838,49.216135,2006.3789552000121 -120.854915,49.216291,2007.0669656001153 -120.854972,49.216408,2007.5029350400646 -120.85508,49.216541,2008.5760000000068 -120.855089,49.216577,2008.6408000000479 -120.855062,49.216614,2008.446400000027 -120.854974,49.21667,2007.6724767999876 -120.854893,49.216728,2007.2573478401064 -120.85485,49.216788,2007.3513279999977 -120.854852,49.216851,2008.169633919878 -120.854875,49.216903,2009.2203400001474 -120.854934,49.216951,2010.7996147199983 -120.855024,49.216999,2011.7681068799964 -120.855314,49.217085,2012.0779823999922 -120.855416,49.217124,2012.6144486399971 -120.855473,49.217156,2013.0584524800367 -120.855516,49.217191,2013.5364057599788 -120.855535,49.217227,2013.9935272000284 -120.855537,49.217322,2015.3460054400323 -120.855518,49.217425,2016.6861040000033 -120.855486,49.217524,2017.780034559963 -120.855403,49.217676,2019.1503731199434 -120.855379,49.217751,2020.0139281599943 -120.855376,49.217837,2020.642187520044 -120.855384,49.217977,2021.3738572799932 -120.855363,49.218085,2022.0785584000134 -120.855325,49.21818,2023.4276799999188 -120.855209,49.218337,2025.3132683199956 -120.855159,49.218385,2025.54073359997 -120.85512,49.218414,2025.6225472000401 -120.855058,49.218443,2025.5179657600072 -120.854989,49.218462,2025.0754854401162 -120.854903,49.218484,2023.6464121597824 -120.854828,49.218519,2022.835346559966 -120.854731,49.218572,2022.1812985600634 -120.854644,49.218635,2021.78883519984 -120.854517,49.218713,2020.943183680049 -120.853873,49.219021,2019.3555999997484 -120.853448,49.219255,2014.2075999999938 -120.853401,49.219277,2013.102400000124 -120.853358,49.21929,2012.0656000000047 -120.853301,49.219299,2010.661600000045 -120.853213,49.219306,2008.4691999999272 -120.852459,49.219396,1999.2145414400263 -120.852081,49.219459,1995.6421598399754 -120.8518,49.219478,1992.38847999993 -120.851759,49.219482,1991.8344976000244 -120.851718,49.21949,1991.2596640000565 -120.851561,49.219556,1989.2862745600376 -120.851523,49.219564,1988.9608115200017 -120.851482,49.219567,1988.6316390400293 -120.850606,49.21958,1993.4508416000108 -120.850523,49.219577,1994.284983520041 -120.850465,49.219567,1994.757493599994 -120.850426,49.219557,1995.0333558400187 -120.850399,49.219555,1995.2365384000307 + + + 0 + clampToGround + -120.850399,49.219555,1995.2365384000307 -120.850397,49.219555,1995.2523351999907 -120.850367,49.219557,1995.503949279968 -120.850323,49.219571,1995.9814849599918 -120.850292,49.219588,1996.4198195200534 -120.850069,49.219763,2000.362930239905 -120.849998,49.21982,1999.504537600059 -120.849988,49.219832,1999.202690560166 -120.849985,49.219843,1998.8539168000427 -120.849993,49.219854,1998.3542195201026 -120.850013,49.219856,1997.995077759938 -120.850126,49.219825,1997.367663999948 -120.850147,49.219822,1997.142177279874 -120.850166,49.219825,1996.7542239999714 -120.850172,49.219834,1996.3783321600304 -120.850171,49.219843,1996.1093257600373 -120.850075,49.219909,1995.2890959999063 -120.850069,49.219917,1995.0943561598565 -120.850069,49.219924,1994.8548755199758 -120.85008,49.219929,1994.544774399914 -120.850095,49.219928,1994.388707199949 -120.85022,49.219887,1994.0476288000966 -120.850252,49.219881,1993.785111040008 -120.850287,49.219881,1993.2944000000543 -120.850341,49.21989,1992.2576000000117 -120.850885,49.220007,1978.936235999765 -120.85102,49.220008,1977.4759679999452 -120.85105,49.22001,1977.044399999867 -120.851066,49.22002,1976.2767360000862 -120.851058,49.220034,1975.4981856001496 -120.851035,49.220045,1975.008060000098 -120.850953,49.220082,1973.1442608001207 -120.850732,49.220181,1969.4560147204352 -120.850693,49.220196,1969.639476479689 -120.850664,49.220201,1970.226426239969 -120.850558,49.220197,1973.86583615986 -120.850519,49.220199,1974.8189911997936 -120.850485,49.220207,1975.2940039997134 -120.850359,49.220264,1976.0361152000696 -120.850253,49.22032,1975.969750400081 -120.850136,49.220399,1975.05403135976 -120.850081,49.220479,1972.9985897600059 -120.850083,49.220491,1972.321046719955 -120.850091,49.2205,1971.534319999676 -120.850106,49.220506,1970.5884646401257 -120.850127,49.220506,1969.686586879961 -120.850432,49.220454,1961.9362188802784 -120.850454,49.220457,1961.0861148798233 -120.850468,49.220464,1960.2286899198211 -120.850474,49.220478,1959.177173119839 -120.850469,49.220492,1958.4536940798757 -120.850428,49.22052,1957.908377600152 -120.850368,49.220558,1957.2665164800803 -120.850228,49.220644,1956.8357990399452 -120.850005,49.220818,1951.1546447999879 -120.849816,49.221011,1950.3273868799627 -120.849567,49.221256,1947.8835795205698 -120.849525,49.221316,1945.388623999882 -120.849509,49.221363,1942.4572419203596 -120.849498,49.221404,1940.2683532801288 -120.849495,49.221542,1934.7848735998778 -120.849498,49.222112,1919.9149030400213 -120.84949,49.222156,1919.157497599956 -120.849475,49.222184,1918.9607360000816 -120.849451,49.222206,1919.1629782399461 -120.849418,49.222249,1919.5388681600318 -120.849404,49.222312,1919.0220262397381 -120.849408,49.222395,1917.579180800112 -120.84945,49.222488,1915.012799999893 -120.849479,49.222517,1913.8211999998966 -120.849516,49.222545,1912.453200000204 -120.849593,49.222582,1909.8360000000077 -120.849651,49.222603,1907.9388000001677 -120.849683,49.222626,1906.7688000000521 -120.849699,49.222658,1905.9623999999394 -120.849731,49.222723,1904.3641684798345 -120.849812,49.222866,1901.6349516799958 -120.849984,49.223129,1896.4455705598248 -120.850001,49.223157,1895.9747999998488 -120.850024,49.223196,1895.3951999998797 -120.850064,49.223384,1894.4292249599223 -120.850073,49.223423,1894.245231840123 -120.850099,49.223454,1893.660020159992 -120.850134,49.223486,1892.8883270400704 -120.850316,49.223573,1888.8995999998424 -120.850354,49.223586,1887.9888000000956 -120.850396,49.223588,1886.9375999999102 -120.850434,49.223577,1885.940399999819 -120.85056,49.223508,1882.5327999999759 -120.850672,49.223428,1879.8255999999299 -120.850719,49.223385,1878.6556000000446 -120.850762,49.223332,1877.5407999999325 -120.850802,49.22325,1876.6767999999652 -120.850832,49.223152,1876.0288000000664 -120.850863,49.222967,1875.677999999926 -120.850911,49.222827,1875.145200000069 -120.850926,49.222811,1874.878799999974 -120.850947,49.222802,1874.4575999998865 -120.850969,49.222807,1873.9643999998527 -120.850989,49.222815,1873.5036000000441 -120.851205,49.222936,1867.7264000002087 -120.85123,49.222942,1866.984799999958 -120.851252,49.22294,1866.3583999999282 -120.851268,49.222933,1865.9227999998768 -120.851278,49.222922,1865.674400000197 -120.851429,49.22276,1862.598000000055 -120.85146,49.222745,1862.3099999998867 -120.851484,49.22274,1861.9679999999828 -120.8516,49.222736,1859.9519999999568 -120.851688,49.222729,1858.353886080084 -120.851771,49.222703,1856.8179115199614 -120.851862,49.222676,1855.167252480074 -120.851959,49.222665,1853.257525600094 -120.852046,49.222674,1852.0082118400271 -120.852153,49.222709,1850.0473019199508 -120.852485,49.222928,1842.8924031999195 -120.852521,49.222959,1841.9343999999646 -120.852545,49.222995,1840.9840000000108 -120.852563,49.22308,1839.1744415999271 -120.852587,49.223115,1838.4505752000027 -120.852613,49.22313,1838.0053775999957 -120.852847,49.223272,1833.6606233599914 -120.853063,49.223433,1830.6315841599924 -120.853112,49.223464,1830.4340147200076 -120.853177,49.223493,1830.173897440007 -120.853583,49.223627,1827.3682113600019 -120.8538,49.223712,1826.9156479999522 -120.853843,49.223721,1826.6915062400035 -120.853892,49.223725,1826.374383999985 -120.85393,49.223723,1825.9300287999363 -120.853972,49.223718,1825.4182643200024 -120.854075,49.223692,1824.0014080000647 -120.854145,49.223681,1823.0275503999585 -120.854226,49.223673,1821.9800099200143 -120.854901,49.223662,1815.553507519994 -120.854978,49.223663,1814.7832054399933 -120.855025,49.223669,1814.4452000000274 -120.855061,49.223683,1814.3371999999677 -120.855158,49.22377,1814.578399999998 -120.855186,49.223778,1814.4632000000074 -120.855218,49.223777,1814.2220000000282 -120.855239,49.22377,1813.995200000038 -120.855312,49.223753,1813.3462694400332 -120.855391,49.223735,1812.7486103999931 -120.855433,49.22373,1812.4860336000036 -120.855483,49.223733,1812.2534145599886 -120.855528,49.223749,1812.1412428799485 -120.855671,49.223796,1811.2233766399913 -120.855718,49.223805,1810.8182096000323 -120.855761,49.223804,1810.396021759995 -120.855899,49.22378,1808.7550688000754 -120.856253,49.223771,1803.9568000000681 -120.856421,49.22378,1801.698515200025 -120.856616,49.22381,1799.7793983999748 -120.856808,49.223885,1797.9963999999836 -120.856908,49.22393,1797.429817599979 -120.857017,49.22401,1796.4246336000133 -120.857059,49.224027,1795.8499094399037 -120.857093,49.224033,1795.3594915199117 -120.857212,49.224041,1793.6031033599409 -120.857259,49.224045,1793.115208800018 -120.857286,49.224057,1792.8449539199773 -120.857306,49.224089,1792.59207264005 -120.857332,49.224309,1793.2343999999896 -120.857331,49.224319,1793.3135999999943 + + + 0 + clampToGround + -120.857331,49.224319,1793.3135999999943 -120.85733,49.224344,1793.500799999982 -120.857317,49.224387,1793.9040000000384 -120.857287,49.224452,1794.6151999999977 -120.85725,49.224506,1795.464800000027 -120.857224,49.224558,1796.2136000000037 -120.857215,49.224602,1796.7908528000144 -120.857247,49.224719,1797.7867999999967 -120.857275,49.224818,1798.7199119999814 -120.857318,49.224881,1799.222287680008 -120.857459,49.225034,1800.7776662399924 -120.857565,49.225132,1802.1536064000552 -120.857667,49.225204,1802.7889574399383 -120.857786,49.225283,1802.9374435199095 -120.858807,49.225936,1793.512716160002 -120.859053,49.226064,1792.7996006400003 -120.85918,49.226123,1793.0376000000267 -120.859326,49.226171,1792.8576000000207 -120.859641,49.226277,1792.9093787200363 -120.859968,49.22642,1793.3391999999994 -120.860247,49.226582,1793.5011999999997 -120.860443,49.226692,1792.8473542399727 -120.860466,49.226718,1792.704379519979 -120.860479,49.226747,1792.6309035200197 -120.860499,49.226803,1792.5071268799586 -120.860521,49.226827,1792.3206036799477 -120.860547,49.226845,1792.0813736000298 -120.860692,49.226943,1791.0150457599957 -120.860786,49.226994,1790.1927993599716 -120.860885,49.227034,1789.3655664000137 -120.861588,49.227199,1787.023675520002 -120.862209,49.227427,1787.2502907199978 -120.862546,49.227561,1787.7263999999705 -120.862748,49.227634,1787.927999999973 -120.862845,49.227665,1787.8119999999872 -120.862856,49.227667,1787.7976000000235 -120.86287,49.227667,1787.7976000000235 -120.862935,49.22765,1787.9200000000214 -120.862949,49.227648,1787.934399999985 -120.862965,49.22765,1787.9200000000214 -120.863621,49.227906,1787.1536000000392 -120.864311,49.228175,1784.1399999999983 -120.864646,49.228329,1782.3169193600017 -120.864796,49.228385,1781.1269983999725 -120.864989,49.228434,1779.604351040028 -120.865402,49.228521,1779.0691999999997 -120.86574,49.228619,1777.8295999999589 -120.866129,49.228757,1773.2253771199535 -120.866231,49.228794,1771.6754345599893 -120.866336,49.228845,1769.6147167999807 -120.86644,49.228919,1767.2183456000512 -120.866556,49.229008,1765.3395660799804 -120.866741,49.229099,1763.8611747200343 -120.866861,49.229148,1763.2956262399664 -120.867128,49.229208,1762.9194188799904 -120.867588,49.229322,1759.8709363199093 -120.86773,49.229341,1758.8758416000849 -120.867861,49.229353,1758.1933716798808 -120.868583,49.229363,1756.446800000016 -120.868822,49.229392,1755.653736960086 -120.86902,49.229425,1755.6070400000763 -120.86923,49.229511,1754.700971199949 -120.869395,49.229606,1753.575524800077 -120.869764,49.229874,1748.3586214399368 -120.869935,49.22996,1746.226304000054 -120.870016,49.229989,1745.6479999999294 -120.870093,49.230007,1745.6935630399462 -120.870174,49.230017,1746.1064643202162 -120.870236,49.230031,1746.1147846402075 -120.870292,49.230066,1745.0014348800037 -120.870571,49.230252,1737.4659999999744 -120.870659,49.230287,1736.6732363201797 -120.870728,49.230301,1736.7237011201344 -120.870855,49.230303,1738.033717600121 -120.87096,49.230311,1739.0950623999333 -120.871013,49.230329,1738.886330080196 -120.871067,49.230357,1738.1320897600638 -120.871188,49.230439,1735.9548000002428 -120.871258,49.230463,1736.1707999999278 -120.871327,49.230472,1737.1751999999242 -120.871832,49.230555,1740.22001919985 -120.871933,49.230562,1740.5686441600408 -120.872013,49.230563,1740.8315715201786 -120.872623,49.230564,1739.7765222399255 -120.872719,49.230557,1739.3689993600697 -120.872794,49.230547,1738.9851999999737 -120.873146,49.230478,1734.5591999999465 -120.873371,49.230429,1731.360620639856 -120.87347,49.230394,1730.4882528001453 -120.873535,49.230358,1730.2544687998443 -120.874195,49.229965,1729.3959999999947 -120.874713,49.229729,1725.9472100800363 -120.874849,49.229691,1724.16079999998 -120.87497,49.229669,1722.1807999998632 -120.875112,49.229667,1719.010887679988 -120.8752,49.229663,1717.0785919997952 -120.875305,49.229651,1714.7317816001482 -120.875798,49.229532,1705.0535999999533 -120.875944,49.229512,1702.722309119933 -120.876023,49.229497,1701.7540142398618 -120.876071,49.229472,1701.7443244800288 -120.876096,49.229446,1702.2163046398234 -120.876099,49.229406,1703.0544342401245 -120.876057,49.229306,1706.0614883199162 -120.876053,49.229285,1706.6100808000833 -120.876057,49.22926,1707.0873472000183 -120.876081,49.229245,1706.9278312000422 -120.876116,49.229241,1706.258309760069 -120.876165,49.229251,1704.6852984000361 -120.876216,49.229271,1702.8674265600737 -120.876469,49.229416,1692.7165964799835 -120.876495,49.229432,1691.4553407997412 -120.876522,49.229435,1690.4268784001522 -120.876556,49.229431,1689.3133523202694 -120.87658,49.229418,1688.7924927997901 -120.876598,49.229402,1688.581083519946 -120.876607,49.229385,1688.7220983997472 -120.876609,49.229364,1689.2124371200205 -120.8766,49.229255,1692.3649600000547 -120.876609,49.229216,1693.1430092799208 -120.876618,49.229196,1693.4371033601258 -120.876633,49.229178,1693.536834880116 -120.876693,49.229151,1692.500253279905 -120.876796,49.229089,1690.4494182400144 -120.876849,49.229057,1689.3304532798331 -120.876893,49.229011,1688.5209860801242 -120.87714,49.228683,1681.6640000000925 -120.877176,49.228622,1680.4975999998144 -120.877192,49.228578,1679.9792000002024 -120.877201,49.228528,1679.6876000000177 -120.877203,49.228476,1679.6228000001813 -120.877184,49.22823,1680.1872128000098 -120.877191,49.228185,1679.9515784001148 -120.877208,49.228141,1679.42534912012 -120.877236,49.228103,1678.5620716801154 -120.877397,49.227962,1673.3371999999358 -120.877482,49.22789,1670.5831999999825 -120.877511,49.22786,1669.6157216000568 -120.877546,49.227818,1668.3679788801671 -120.877562,49.227771,1667.6704000000473 -120.877565,49.227699,1666.525599999868 -120.877543,49.227535,1664.9559999999428 -120.877456,49.227147,1659.642400000188 -120.877246,49.226754,1658.4554252800112 -120.877212,49.226428,1655.79558143998 -120.877069,49.225953,1653.4768292797298 -120.876981,49.225657,1650.9587396799395 -120.876971,49.225623,1650.5731883200576 -120.876957,49.225586,1650.2458460798514 -120.876926,49.225549,1650.5648988801054 -120.876886,49.225513,1651.3945001600264 -120.876721,49.225383,1654.060120159918 -120.876547,49.225254,1655.6878470399263 -120.876519,49.225227,1655.5960470398434 -120.876505,49.225197,1654.991108800193 -120.876496,49.22512,1652.6732415998208 -120.876485,49.225048,1650.5324223999794 -120.876466,49.225011,1649.7084140802122 -120.876423,49.224971,1649.6496203198699 -120.876195,49.224856,1652.4137568001652 -120.876155,49.224824,1652.55890880002 -120.876134,49.224798,1652.392721280147 -120.87612,49.224774,1652.1068351999065 -120.87612,49.22474,1651.2539519999211 -120.876136,49.224711,1650.0656396799197 -120.876165,49.224692,1648.7902143999054 -120.876209,49.224681,1647.5061923198334 -120.876339,49.22468,1644.8070015999483 + + + 0 + clampToGround + -120.876339,49.22468,1644.8070015999483 -120.876499,49.224679,1641.8348798401296 -120.876586,49.22469,1640.6063136000093 -120.87665,49.224709,1639.9639439999648 -120.876836,49.224781,1637.8810086400529 -120.87715,49.224984,1634.3722239999836 -120.877223,49.22508,1634.4235807999553 -120.877338,49.225227,1633.9801731201003 -120.877506,49.225403,1631.2130627200747 -120.877668,49.225503,1628.3132281600315 -120.877705,49.225516,1627.5118911998445 -120.877739,49.225516,1626.6725209598592 -120.877764,49.225498,1625.9225228800349 -120.877777,49.225476,1625.4459980801314 -120.877804,49.225317,1623.6893414400342 -120.877814,49.225287,1623.2753414399933 -120.877836,49.225258,1622.7573235199586 -120.877979,49.225179,1619.9111966400828 -120.878552,49.224973,1612.2518841599972 -120.878726,49.2249,1610.4527999999839 -120.878801,49.224855,1609.5888000000166 -120.878867,49.224802,1608.73199999998 -120.879068,49.224624,1606.0663987199634 -120.879094,49.224597,1605.5763292800643 -120.879133,49.224576,1605.0793996800082 -120.879175,49.22456,1604.5689599999116 -120.87923,49.224555,1603.8614879999577 -120.879476,49.224552,1600.7991859201134 -120.87954,49.224555,1599.8049119999566 -120.879592,49.224562,1599.1080038399332 -120.879629,49.224576,1598.8342598400702 -120.87966,49.224597,1598.8413792000492 -120.879796,49.224683,1598.9116972799864 -120.879838,49.224697,1598.6593545598778 -120.879876,49.224701,1598.208504960146 -120.879916,49.224701,1597.62150336017 -120.879955,49.224692,1596.7952255999965 -120.879992,49.224666,1595.5018291199538 -120.880004,49.224634,1594.4107468800562 -120.879985,49.22448,1590.287087999908 -120.879953,49.224361,1588.5935723200198 -120.879924,49.224307,1588.2534227200063 -120.879881,49.224253,1588.0899467199974 -120.879802,49.224193,1588.2979734400237 -120.879429,49.223989,1588.547352960078 -120.879355,49.223953,1587.5908704000494 -120.879285,49.223921,1586.5006175998967 -120.879209,49.223898,1585.4932051199487 -120.879161,49.223893,1585.1631019198728 -120.879002,49.223893,1585.171573439866 -120.878923,49.223896,1585.3040563201464 -120.878878,49.223888,1585.0558745600565 -120.878841,49.223871,1584.8828974401074 -120.878823,49.223839,1584.1745988799705 -120.878821,49.223788,1582.8576339199615 -120.878832,49.223719,1581.0031443200555 -120.878876,49.223655,1579.1597311999988 -120.878946,49.22359,1577.2467744000426 -120.879047,49.223523,1575.0298497600043 -120.879098,49.223497,1574.062349760056 -120.879175,49.223476,1573.0496159999168 -120.879243,49.223473,1572.4743388799357 -120.879558,49.223484,1570.1975999999368 -120.879694,49.223497,1569.196800000059 -120.879868,49.22353,1568.6588415998683 -120.881281,49.223808,1560.8612083198993 -120.881371,49.223826,1560.7911206401186 -120.881446,49.223834,1559.9242796798821 -120.881823,49.223846,1553.4200963201226 -120.881918,49.223857,1552.2022710401163 -120.881986,49.223872,1551.891495680027 -120.882303,49.223986,1550.5132636800706 -120.882335,49.223988,1549.8004608000167 -120.882363,49.223979,1548.8876059200459 -120.882371,49.223956,1548.0714009598796 -120.882359,49.223931,1547.6890478401592 -120.882148,49.223807,1547.7748294400199 -120.882098,49.223788,1547.5471769599699 -120.882027,49.223769,1547.4890715199513 -120.881823,49.223722,1548.3145302401358 -120.88171,49.223694,1548.9102496001058 -120.881587,49.223663,1549.2627227201397 -120.881415,49.223601,1548.350578400034 -120.881255,49.223521,1548.3352784000033 -120.880433,49.223087,1545.946743840037 -120.880404,49.223067,1545.6168787200331 -120.880396,49.22304,1545.1057663999013 -120.880414,49.223018,1544.5664979200142 -120.880456,49.223008,1543.8101580800726 -120.88052,49.223011,1542.890931199932 -120.881364,49.223215,1535.4862304000437 -120.881506,49.223245,1534.8538063999422 -120.881648,49.223265,1533.74320639997 -120.881804,49.223273,1532.2135999999505 -120.882485,49.223319,1528.6459999998453 -120.882676,49.223322,1525.5015526400286 -120.88286,49.223312,1522.301132800087 -120.883043,49.223275,1518.9113080000125 -120.883333,49.223191,1513.4669553600831 -120.883464,49.223159,1512.3515769600108 -120.883818,49.22311,1511.9816384000148 -120.884108,49.223079,1513.0907494399705 -120.884168,49.223083,1513.4144000000733 -120.884213,49.223103,1514.3504000000635 -120.884246,49.223131,1515.2288000000967 -120.88425,49.22317,1515.8479999999659 -120.884159,49.223291,1516.316012480015 -120.884158,49.223321,1516.7315705599724 -120.88417,49.223337,1517.0877583999718 -120.88419,49.223348,1517.4988352000714 -120.884213,49.223352,1517.88000896002 -120.884246,49.223352,1518.3631923200478 -120.884462,49.223282,1520.60015871995 -120.884496,49.223275,1521.1659519999862 -120.884519,49.223275,1521.6147279999616 -120.884543,49.223283,1522.1777795199023 -120.884578,49.223307,1523.1159596800708 -120.884585,49.223327,1523.4618735999977 -120.884578,49.223341,1523.501129920057 -120.884559,49.223359,1523.393494239954 -120.88437,49.223446,1521.4416992001013 -120.884359,49.223455,1521.3868712000722 -120.884351,49.223464,1521.3777574399217 -120.884353,49.223474,1521.5420931201115 -120.884358,49.223481,1521.716166080092 -120.884371,49.223486,1521.9954857600417 -120.884385,49.223489,1522.2656743999219 -120.884423,49.22349,1522.9036591999445 -120.884797,49.223509,1529.3421579201165 -120.884848,49.223516,1530.3918387200677 -120.884906,49.223533,1531.6504419200442 -120.884973,49.223563,1533.1507649600312 -120.886149,49.224283,1526.316875680074 -120.887294,49.224785,1524.5800000000727 -120.888864,49.225213,1527.5087052801257 -120.889609,49.225374,1530.3539926398166 -120.889743,49.225423,1530.0255999999704 -120.889853,49.22547,1529.579199999972 -120.890164,49.225605,1528.0641087999304 -120.890288,49.225645,1527.9035007999746 -120.8904,49.22567,1528.2774399999596 -120.890543,49.225673,1529.7838211199107 -120.890945,49.225661,1531.9683983998957 -120.891079,49.22567,1531.4883455999595 -120.891254,49.225685,1530.8740000000562 -120.892026,49.225873,1522.3872739199996 -120.89242,49.225955,1519.055856000042 -120.892766,49.226001,1517.5415779201678 -120.893565,49.226093,1511.7032464000445 -120.894094,49.226176,1507.6308902399162 -120.894362,49.226208,1506.6537523200113 -120.894644,49.22622,1506.472000000069 -120.896039,49.226226,1496.6445705600875 -120.896215,49.226221,1495.3175999999426 -120.896409,49.226202,1493.8867107199312 -120.896604,49.226174,1493.2382758400176 -120.896789,49.226132,1492.4864000000327 -120.89735,49.225938,1492.391999999982 -120.89753,49.225875,1492.0841999998818 -120.897699,49.225832,1491.1789612800608 -120.8979,49.225803,1490.0547519999748 -120.898377,49.225741,1486.5722532800573 -120.89847,49.225735,1485.8841679999612 -120.898552,49.225747,1484.6802617601047 -120.898668,49.225786,1482.054702080014 -120.898715,49.225793,1481.275695199944 -120.89876,49.225791,1480.834313600015 -120.898807,49.22578,1480.6690015999318 -120.898851,49.225759,1480.854100640193 -120.898982,49.22567,1482.34539519993 -120.899024,49.225646,1482.7800563201142 -120.899072,49.225631,1482.8791225599678 -120.899452,49.225578,1482.1362022400394 -120.899531,49.225572,1482.6419532800005 -120.899575,49.225575,1482.667100000029 -120.89963,49.225599,1481.8431247998988 -120.899661,49.225626,1480.7922934400567 -120.89986,49.225873,1472.4120576000603 -120.899944,49.225964,1470.2999347201778 -120.9,49.226004,1469.855999999995 -120.900092,49.22604,1470.3007872001049 -120.901159,49.226425,1465.2248240000563 -120.901381,49.226493,1462.4811113600958 -120.901521,49.226523,1462.6168393600162 -120.901671,49.226545,1463.4043671999061 -120.901856,49.226609,1462.2976998398547 -120.90199,49.226665,1461.383968000085 -120.902213,49.226773,1459.8584910399861 -120.902959,49.227124,1451.4798886398098 -120.905715,49.22817,1427.2775120000776 -120.90605,49.228316,1421.9222720000241 -120.906153,49.228339,1419.8503052800604 -120.906221,49.228342,1418.4473708801063 -120.906278,49.228329,1418.2393724798685 -120.906356,49.228295,1418.783660799798 -120.906995,49.22789,1414.5140000000729 -120.907057,49.227852,1414.1468000002326 -120.907097,49.227815,1414.4492000002365 -120.907124,49.227763,1415.273223040111 -120.90714,49.227688,1415.1785344000566 -120.90717,49.226626,1410.6563232002886 -120.907272,49.225761,1403.3774636797923 -120.907311,49.225565,1405.6231335996843 -120.907341,49.225505,1405.5034695999448 -120.907379,49.225466,1404.869087679853 -120.907451,49.225419,1403.4534452801188 -120.907516,49.225388,1402.1931443199192 -120.90761,49.225372,1400.0584767998555 -120.907964,49.225381,1390.4568793598528 -120.908079,49.225372,1387.749886079979 -120.908175,49.225349,1386.3577919999536 -120.908333,49.22529,1385.478211199972 -120.908965,49.225001,1383.6727456002627 -120.909181,49.224867,1378.2133878397442 -120.909345,49.224722,1374.663313599919 -120.909508,49.224517,1376.637362559968 -120.909686,49.224301,1379.4334614403492 -120.909804,49.224167,1380.7475532801789 -120.909914,49.224076,1379.0400396800785 -120.909977,49.224038,1377.96454112007 -120.910052,49.224011,1376.6509068802716 -120.910131,49.22399,1375.2511375997972 -120.910219,49.223973,1373.517272480025 -120.910243,49.223958,1373.4195497600388 -120.910253,49.223937,1373.9078494400912 -120.91026,49.223911,1374.657094400118 -120.910276,49.223834,1376.2291353601706 -120.9103,49.223782,1376.7487999998366 -120.910317,49.223752,1376.968399999896 -120.910343,49.22373,1376.7883999999924 -120.910573,49.223589,1374.0647977600954 -120.910591,49.223574,1373.9484947201138 -120.910604,49.223552,1374.1686246398788 -120.910658,49.22347,1374.9803008000804 -120.910705,49.223436,1374.7871104001838 -120.910752,49.22342,1374.131507200029 -120.910816,49.223414,1372.7970419201288 -120.910887,49.223421,1370.674973919873 -120.911007,49.223448,1366.5412825601281 -120.911237,49.223504,1359.0160921600452 -120.911288,49.223507,1357.7002547200755 -120.911344,49.223504,1356.4078259200442 -120.911391,49.223488,1355.6060000000627 -120.911438,49.223455,1355.1343999998721 -120.911786,49.223193,1352.0284678397895 -120.912533,49.22258,1351.2712000000638 -120.91262,49.222525,1350.7779999999789 -120.912743,49.222483,1349.2025867199477 -120.913036,49.222382,1345.1409177598323 -120.913183,49.222305,1342.9184775999395 -120.913306,49.222223,1341.576675519883 -120.913449,49.222093,1339.8483999999937 -120.913561,49.221951,1338.8547999999955 -120.913639,49.221849,1338.935210239961 -120.913726,49.221779,1338.1562393600768 -120.913808,49.221733,1336.7602457599316 -120.913864,49.221711,1335.5699993599144 -120.913931,49.221705,1334.566120799947 -120.913982,49.221728,1334.0684121599556 -120.914036,49.221732,1333.535761920047 -120.914082,49.221709,1333.0203484800602 -120.914106,49.221676,1332.681461759965 -120.914125,49.221618,1332.5288400000197 -120.914157,49.221591,1332.1328385599602 -120.914211,49.221575,1332.1069319999876 -120.914463,49.221604,1333.078259839961 -120.914586,49.221653,1335.4486553600232 -120.914835,49.22178,1341.4156479999199 -120.91491,49.221894,1344.1036384001654 -120.914937,49.222031,1344.8739999998454 -120.914885,49.222568,1343.346799999963 -120.914779,49.222925,1342.4907999998686 -120.914677,49.223098,1341.3287999999482 -120.914623,49.223194,1340.8536000001247 -120.914605,49.223305,1341.2639999999071 -120.914641,49.223537,1341.7267876800588 -120.914524,49.225134,1335.7911219200032 -120.914535,49.225291,1336.1492824000393 -120.914556,49.22541,1335.872838400187 -120.914615,49.225578,1336.3147887999394 -120.914615,49.22582,1334.0374719999456 -120.914556,49.226164,1332.1155353600961 -120.91439,49.227022,1325.2593535998908 -120.913999,49.228843,1287.6830294402093 -120.913956,49.229166,1282.1479999999551 -120.91399,49.229451,1281.8302096000018 -120.914079,49.229667,1279.3716587200586 -120.914246,49.229862,1278.6104582402547 -120.914905,49.230581,1262.3259816000686 -120.915356,49.230938,1255.5557068799937 -120.915786,49.231209,1254.641297919946 -120.916343,49.231488,1249.810543360038 + + + 0 + clampToGround + -120.916343,49.231488,1249.810543360038 -120.917482,49.232059,1236.4262294401162 -120.91881,49.232809,1224.5318351997635 -120.919822,49.233393,1214.5672438399736 -120.920135,49.233531,1212.4020872000744 -120.92043,49.233618,1210.200799999992 -120.920945,49.233678,1209.308400000158 -120.921058,49.233706,1208.001600000132 -120.921149,49.233737,1206.4938164801285 -120.921232,49.233789,1203.827102079926 -120.9213,49.233856,1200.2922879998418 -120.921412,49.233958,1196.2233996800148 -120.921589,49.234049,1194.9987228800137 -120.921788,49.234151,1193.491435520302 -120.922056,49.234252,1192.572800000095 -120.922319,49.234354,1191.1633609598136 -120.922615,49.234461,1190.1830511996116 -120.924926,49.235124,1166.0810790400772 -120.925293,49.23525,1159.408239999901 -120.925484,49.23529,1159.2769344000121 -120.925568,49.235322,1158.5424000001913 -120.925615,49.235349,1157.3255999999892 -120.925635,49.235373,1156.0871999998417 -120.925654,49.235416,1153.7471999999175 -120.925665,49.235442,1152.3288000000198 -120.925692,49.23546,1151.4863999997933 -120.925729,49.235468,1151.2920000001818 -120.925771,49.235469,1151.5367999998707 -120.925822,49.235458,1152.5375999999019 -120.925948,49.235405,1156.5322752000347 -120.926027,49.235393,1157.8764028799148 -120.926154,49.235405,1157.9940847999767 -120.926224,49.235422,1157.2561971200391 -120.926278,49.235443,1156.137844160215 -120.926338,49.235473,1154.409152960078 -120.926377,49.235509,1152.1344267199813 -120.926405,49.235562,1148.4690911998787 -120.926402,49.235606,1145.653943040211 -120.926381,49.235645,1143.1938896001304 -120.926331,49.235685,1140.5978288002168 -120.926276,49.235702,1139.532508159789 -120.926211,49.235711,1139.0035356798817 -120.926018,49.235711,1138.8987779198858 -120.92598,49.235725,1138.0559199998718 -120.925964,49.235757,1136.2512659200186 -120.925977,49.235785,1134.6999928000068 -120.926015,49.235804,1133.6530623999079 -120.926237,49.23589,1128.6526112001166 -120.926291,49.2359,1128.1340959999563 -120.926358,49.235888,1128.6620633598814 -120.926586,49.235765,1135.0788767999595 -120.926651,49.235758,1135.3697913601593 -120.926715,49.235758,1134.9115776001618 -120.926795,49.235767,1133.655451199848 -120.9272,49.235928,1120.679935999933 -120.927254,49.235941,1120.1416000002346 -120.927308,49.235947,1120.3791999997882 -120.927372,49.23596,1120.3216000001382 -120.92741,49.23597,1120.15599999984 -120.927742,49.236164,1114.4999999997872 -120.927815,49.236212,1112.9053311998546 -120.927908,49.23624,1112.228876799896 -120.928,49.236244,1112.841280000019 -120.928091,49.236235,1113.7088383999867 -120.928378,49.236245,1111.6618832002096 -120.928558,49.236265,1108.390174399903 -120.928724,49.236303,1104.9508588800131 -120.928901,49.23637,1100.0850351999518 -120.929099,49.236464,1096.7709305600529 -120.929252,49.236585,1093.2616832000485 -120.929344,49.236668,1090.9537356799426 -120.929362,49.23669,1090.1849311998976 -120.929358,49.2367,1089.8373440000369 -120.929343,49.236704,1089.70508287989 -120.929315,49.236697,1089.9588872000165 -120.929242,49.236683,1090.4548534400226 -120.929156,49.236664,1091.0387686400927 -120.92913,49.236665,1090.9167919999402 -120.929115,49.236671,1090.6677967999449 -120.929091,49.236692,1089.8571142400706 -120.929082,49.236717,1088.9539404800141 -120.929083,49.236741,1088.129197759959 -120.929097,49.236797,1086.2910492798949 -120.929091,49.236813,1085.6993993600684 -120.929073,49.236825,1085.1483919999087 -120.929046,49.236832,1084.6916902400071 -120.929015,49.236837,1084.2655856000074 -120.928956,49.236845,1083.489814399904 -120.928756,49.236852,1080.5864576001602 -120.928563,49.236871,1076.2469703998934 -120.928445,49.236898,1071.9891280001596 -120.928334,49.236927,1067.3348464000512 -120.928254,49.236967,1064.1036092799359 -120.928214,49.236992,1062.667252479929 -120.928195,49.23703,1061.0826160001322 -120.928199,49.237081,1059.403462239987 -120.928222,49.237131,1058.0832227201558 -120.928252,49.237166,1057.4168627199426 -120.928304,49.237218,1056.6104051199945 -120.928359,49.237258,1056.3832988801034 -120.928431,49.237291,1056.7941438401017 -120.928831,49.237452,1059.47760000014 -120.928885,49.237468,1060.0679999998772 -120.92896,49.23748,1060.9815680001363 -120.929128,49.237494,1063.5014067201653 -120.929196,49.237499,1064.0327801600104 -120.929287,49.237515,1063.4833927999343 -120.930175,49.237746,1050.998399999907 -120.930395,49.237818,1047.709200000045 -120.930499,49.237872,1046.1683999999448 -120.930559,49.237914,1045.0451999999157 -120.930591,49.237927,1044.6491999999946 -120.930617,49.237931,1044.4691999999375 -120.930644,49.237928,1044.4368000000704 -120.930663,49.237917,1044.6059999999502 -120.930687,49.237881,1045.2971999999445 -120.930703,49.237795,1047.0972000000554 -120.930711,49.237783,1047.3275999999853 -120.930722,49.237772,1047.5755366399999 -120.930755,49.237761,1047.7822328000111 -120.930784,49.237757,1047.7892844799308 -120.930826,49.237758,1047.5978796800302 -120.930926,49.237774,1047.104263039953 -120.931481,49.237934,1040.6511081598703 -120.93154,49.237949,1039.6454784000039 -120.931605,49.237955,1038.8436359999723 -120.931664,49.237941,1038.7364409600432 -120.931719,49.237917,1038.92399999989 -120.93214,49.237746,1039.6887999999233 -120.932356,49.237702,1037.764360960058 -120.932412,49.23769,1037.2277823999914 -120.932466,49.237662,1037.261167359915 -120.932549,49.237601,1037.7173219201818 -120.932683,49.237506,1038.4831398399263 -120.932833,49.237421,1038.9356000000441 -120.932868,49.237419,1038.2587999999112 -120.932899,49.237427,1037.272399999997 -120.932919,49.237447,1036.0483999999155 -120.932927,49.237466,1035.1231999999493 -120.932922,49.237506,1033.6584147197689 -120.932905,49.23754,1032.733951999786 -120.932505,49.237995,1022.7361519999753 -120.93246,49.238108,1018.9840639999884 -120.932472,49.238152,1017.0586111999405 -120.932502,49.238177,1015.5982956799965 -120.932539,49.238192,1014.5239289599909 -120.932592,49.238194,1013.6901401600046 -120.932643,49.238189,1013.1166198400684 -120.932881,49.238145,1011.5485103999679 -120.93294,49.238143,1011.0972863999604 -120.932995,49.238152,1010.4194207999677 -120.933113,49.238197,1008.5311999999676 -120.933399,49.238394,1000.7036297600948 -120.933458,49.23842,998.9128256000593 -120.933522,49.23843,997.691561600009 -120.933566,49.23843,997.1130847999754 -120.933615,49.238425,996.6392399999701 -120.933656,49.238415,996.3470207998959 -120.933736,49.238384,996.0920140799506 -120.934865,49.237655,1001.9536240000535 -120.934955,49.23761,1001.2336960000418 -120.935074,49.237602,999.2932441599382 -120.935165,49.237608,997.7702944 -120.935255,49.237629,996.0118384000614 -120.935422,49.237715,992.6897391999447 -120.935471,49.237759,991.2982225599906 -120.935494,49.237802,990.3159999999098 -120.935487,49.237845,989.7724000000544 -120.935448,49.237877,989.7328000001032 -120.935275,49.237965,990.3339999999155 -120.935118,49.238063,990.5607692800412 -120.934973,49.238182,988.9724636799539 -120.934796,49.23842,981.631398400064 -120.934376,49.238925,971.7515359999331 -120.934323,49.239016,969.2302745599503 -120.934321,49.239035,968.5760911999877 + + + 0 + clampToGround + -120.934321,49.239035,968.5760911999877 -120.934315,49.239105,966.1369039999416 -120.934337,49.239253,961.1669825600665 -120.934052,49.239346,959.6024000000984 + + + 0 + clampToGround + -120.934052,49.239346,959.6024000000984 -120.933753,49.239444,957.5028172800493 -120.933691,49.239495,957.4257136000307 -120.933681,49.239521,957.3341180799418 -120.933685,49.239554,957.1297792000289 -120.933722,49.239609,956.5354278399662 -120.933864,49.239755,954.1507999999935 -120.933901,49.239831,953.5477262399618 -120.933879,49.239898,953.4739999999629 -120.933199,49.240649,947.8976000000735 -120.932626,49.241322,942.0115731200292 + + + 0 + clampToGround + -120.932626,49.241322,942.0115731200292 -120.932508,49.241461,943.1806764800181 -120.93227,49.241646,945.5815999999918 -120.93199,49.241795,947.2099999999954 -120.930686,49.242417,957.4448000000323 -120.930238,49.242783,960.5137078399277 -120.930101,49.243065,958.8659248000631 -120.929893,49.243267,959.725185759893 -120.929766,49.243384,959.6726924800108 + + + 0 + clampToGround + -120.929766,49.243384,959.6726924800108 -120.929221,49.243884,961.8968000000177 -120.929374,49.244068,959.6476787200742 -120.929208,49.244277,964.1095033599302 -120.928833,49.244356,969.8937241600444 -120.92818,49.24507,974.7688960000214 -120.928028,49.245839,982.1175763199973 -120.928132,49.246496,978.1103999999835 -120.928085,49.246764,979.9080576000085 -120.927604,49.247557,986.5892268799556 -120.927468,49.247919,985.8352646399366 -120.927498,49.248294,985.8728000000053 -120.927444,49.248777,991.9936000000187 -120.927227,49.249083,998.7315806399563 -120.927173,49.249413,1001.2412000000162 -120.927065,49.249825,1002.3414200000119 -120.927065,49.250033,1002.9871592000283 + + + 0 + clampToGround + -120.927065,49.250033,1002.9871592000283 -120.927065,49.250191,1004.3716184000173 -120.927318,49.250591,1007.3044035200041 -120.927462,49.251074,1009.909352959981 -120.927144,49.251303,1011.6778707200073 -120.926875,49.251506,1011.1960000000381 -120.926714,49.251842,1012.7511999999813 -120.926854,49.252122,1014.7417235199229 -120.927101,49.252409,1017.1492707200827 -120.927465,49.252381,1014.2872432000062 -120.927734,49.252416,1012.9651417599803 -120.927841,49.252521,1012.2655803199868 -120.927819,49.252647,1012.3192001600031 -120.927562,49.252864,1016.9865561601167 -120.927233,49.253044,1022.8431859199846 -120.927026,49.253432,1023.9066265600676 -120.926521,49.253691,1031.4504523200792 -120.925845,49.253803,1042.4937864000706 -120.925481,49.253894,1047.541738559967 -120.925169,49.254118,1051.714191680047 -120.925105,49.254412,1055.7595999999853 -120.925045,49.254758,1056.7078032000416 -120.925127,49.255119,1050.1715999998833 -120.924848,49.255505,1054.6476096001152 -120.924483,49.255764,1062.6863334402155 -120.924064,49.255932,1070.8256819199023 -120.923657,49.25596,1081.5154751997907 -120.923238,49.256009,1086.9828793600407 -120.922713,49.256254,1088.298441919986 -120.922546,49.25639,1089.4952000000094 + + + 0 + clampToGround + -120.922546,49.25639,1089.4952000000094 -120.922326,49.256569,1090.8369689600393 -120.92209,49.25687,1088.59243200006 -120.92208,49.257248,1084.2598272001117 -120.922165,49.257808,1088.704425599799 -120.922026,49.258284,1094.7766566399193 -120.921221,49.258663,1104.4806980799897 -120.920052,49.259174,1109.3266316799654 -120.918743,49.259489,1119.4967999998344 -120.917874,49.259888,1118.8036684802048 -120.917166,49.260504,1115.901600000211 -120.916393,49.261085,1118.3315999999877 -120.915556,49.261232,1117.2139852799994 -120.914891,49.2614,1115.2754079999902 -120.914713,49.261536,1113.2987827198997 + + + 0 + clampToGround + -120.914713,49.261536,1113.2987827198997 -120.914451,49.261736,1110.6178905600364 -120.914252,49.262458,1099.6103999998732 -120.913432,49.262962,1110.3920627199204 -120.912456,49.263676,1108.9215999999865 -120.911018,49.265125,1086.2603600001005 -120.910894,49.265527,1079.8138969600325 -120.910503,49.265888,1079.7803999999855 -120.909806,49.266672,1081.4168000000072 -120.909584,49.266736,1082.8709990400453 -120.909561,49.266761,1083.4208961600564 + + + 0 + clampToGround + -120.909561,49.266761,1083.4208961600564 -120.909076,49.267302,1091.01534208 -120.908401,49.267751,1092.3673169599713 -120.907916,49.267992,1092.428549119996 -120.907199,49.268175,1095.7199999999912 -120.906308,49.268794,1098.4183999999489 -120.905654,49.268794,1095.0962630399956 -120.905203,49.269158,1099.5375971199414 -120.904195,49.269746,1097.648399999984 -120.903454,49.270138,1101.4876198400402 -120.903025,49.270572,1103.003776000015 -120.902414,49.270824,1102.6296025600527 -120.901941,49.271202,1106.6691427201038 -120.900815,49.272231,1119.8294287999313 -120.9,49.272868,1116.9744000000342 -120.898562,49.27403,1128.0199424000568 -120.897543,49.274772,1136.4369401599613 -120.89676,49.275066,1136.7801664000444 -120.896159,49.275416,1140.3303999999778 -120.895172,49.276081,1137.846078719987 -120.894689,49.27655,1143.7902320000037 -120.894195,49.277173,1149.6362511999512 -120.893187,49.277852,1151.054478080036 -120.892672,49.278258,1152.7664729600708 -120.892554,49.278601,1153.5174761600567 -120.89264,49.278971,1162.524147199956 -120.892747,49.279728,1171.6629907200006 -120.892801,49.280413,1175.2722070402574 -120.89265,49.2807,1171.6232000001905 -120.89221,49.281106,1158.9024192000322 -120.891524,49.281694,1165.7274470401105 -120.891138,49.282079,1167.4372899200278 -120.890848,49.282037,1166.9464000000353 -120.890537,49.282016,1170.1695846400708 -120.890311,49.282086,1175.952296320047 -120.890022,49.282457,1185.2943999999725 -120.889518,49.282709,1193.7587644800547 -120.889099,49.282674,1196.4963999999948 -120.888842,49.282471,1195.5824000000148 -120.888294,49.282289,1192.9956406399785 -120.887887,49.282289,1194.2697252799392 -120.887479,49.282387,1198.0864459200045 -120.887071,49.282709,1204.7347868799793 -120.886245,49.283283,1210.9449231999765 -120.885087,49.283822,1228.4828000000173 -120.884078,49.284095,1233.7208463999991 -120.883563,49.284116,1238.035583360033 -120.883188,49.283962,1242.8791999999355 -120.882469,49.283255,1253.1794711999385 -120.882104,49.28287,1251.5416000001233 -120.881771,49.282548,1245.9149036799336 -120.881321,49.282471,1248.358169280032 -120.880452,49.282457,1240.546109440081 -120.879679,49.282555,1241.6024000001353 -120.87837,49.282814,1264.431200000061 -120.877287,49.282716,1266.0457452799928 -120.876686,49.282849,1270.4462454399554 -120.876074,49.28315,1289.6416799998783 -120.875398,49.283241,1308.567349119825 -120.874637,49.283122,1314.0674134398712 -120.873714,49.283003,1321.1730406399583 -120.872748,49.282891,1328.1210841599564 -120.872255,49.282744,1328.6445024000814 -120.871568,49.282443,1336.7208000000837 -120.870678,49.28217,1347.4227295999935 -120.869658,49.2821,1342.262399999988 -120.868317,49.282135,1348.9775999998474 -120.867416,49.282275,1360.6922879998951 -120.866257,49.282184,1367.8017324800535 -120.865303,49.282198,1364.5596822400144 -120.864809,49.282191,1364.7822587199894 -120.864659,49.282079,1365.1743011199715 -120.864659,49.281764,1367.9523468798886 -120.864841,49.281484,1368.3855488000404 -120.865142,49.28133,1367.3839487999467 -120.865163,49.281232,1367.8804492800248 -120.865013,49.281134,1369.8162873599574 -120.863328,49.278755,1375.1573632000236 -120.863489,49.278181,1362.6850766399243 -120.862331,49.276634,1376.5920000000983 -120.861837,49.276102,1372.6257129602516 -120.860947,49.275689,1388.8373796801204 -120.860368,49.275333,1392.6416000002264 + + + 0 + clampToGround + -120.860368,49.275333,1392.6416000002264 -120.860196,49.275227,1393.12119999999 -120.859756,49.274667,1391.3777740799044 -120.859434,49.27417,1392.2625024001268 -120.858983,49.273736,1396.6989510400983 -120.858586,49.273624,1400.6863999999157 -120.857824,49.273442,1410.131200000218 -120.857503,49.273176,1411.0970828801283 -120.857417,49.272686,1410.4842470400795 -120.857202,49.272105,1417.0513568000172 -120.856677,49.271573,1425.0042876797945 -120.856153,49.271296,1438.332082559742 -120.856166,49.271679,1447.172826559899 -120.855262,49.271257,1467.8576000000248 -120.855186,49.271791,1489.7724611198964 -120.853394,49.270726,1513.6152204803338 -120.852718,49.270341,1515.2920976001817 -120.851591,49.270152,1532.0312000002286 -120.850357,49.270068,1550.8250169601106 -120.849177,49.270145,1566.370763199982 -120.848136,49.27004,1573.4115072001937 -120.847139,49.269963,1581.341106719777 -120.845165,49.269718,1595.5079999998575 -120.844,49.269923,1612.5549599999165 -120.842836,49.269634,1604.0979379201847 -120.841742,49.269655,1618.3536304000738 -120.840347,49.269907,1644.6421678401337 -120.839092,49.270481,1658.970545919727 -120.838523,49.270789,1667.4990251200152 -120.837783,49.270831,1667.980168320056 -120.837268,49.270964,1663.1631999998558 -120.836989,49.2713,1669.3005439999542 -120.83686,49.271762,1681.7374656001634 -120.83656,49.272126,1681.7146176002443 -120.83612,49.272448,1681.7903999997143 -120.835594,49.272735,1691.2903999998841 -120.834983,49.272952,1702.5108307198461 -120.83406,49.273057,1711.8316064001076 -120.833191,49.273393,1723.0633459199742 -120.832494,49.273841,1731.0020953599617 -120.832075,49.274254,1742.7609039999704 -120.831743,49.27487,1754.501987200032 -120.831571,49.275493,1765.6155999999069 -120.831517,49.276032,1774.6730502401392 -120.830899,49.27643,1776.603387199934 + + + 0 + clampToGround + -120.830899,49.27643,1776.603387199934 -120.830756,49.276522,1779.602781440021 -120.830048,49.277173,1794.1095999999677 -120.829179,49.277397,1797.7472635200204 -120.827795,49.277782,1808.0608000000284 -120.826668,49.278335,1815.0120287999887 -120.82611,49.27879,1812.9908479999992 -120.825735,49.278951,1810.2376455999693 -120.82537,49.279056,1809.5352000000028 -120.824912,49.279137,1809.0676684800048 -120.824539,49.279173,1809.7357611199961 -120.824222,49.279187,1813.2469443199188 -120.823868,49.279135,1816.8258272000426 -120.823548,49.279081,1819.5420000000377 -120.823258,49.279,1821.216959999957 -120.823098,49.278956,1822.2013670399867 -120.822975,49.278949,1822.6408000000315 -120.822869,49.278947,1823.0439999999855 -120.822739,49.278966,1823.4076470400435 -120.822645,49.278995,1823.6274040000096 -120.822505,49.279052,1823.6257695999648 -120.822443,49.279067,1823.4352000000326 -120.822374,49.279069,1823.406400000003 -120.822043,49.279068,1823.191624959964 -120.820977,49.279054,1822.5107999999645 -120.820246,49.279269,1816.3726899199805 -120.819951,49.27931,1814.1688448000343 -120.818936,49.279237,1807.1824000000054 -120.818035,49.278967,1803.2603911999997 -120.817862,49.278979,1803.0983580800057 -120.81752,49.279097,1802.0720000000065 -120.816883,49.279039,1798.6562729600062 -120.816395,49.278706,1789.4468847999951 -120.815897,49.278114,1781.812176320041 -120.815166,49.277672,1776.128366080012 -120.814757,49.277165,1767.0951999999386 -120.814486,49.276742,1761.081143040003 -120.814371,49.276644,1760.2695500799389 -120.812719,49.276065,1749.5476000000338 -120.812045,49.275799,1747.3372567999863 -120.811825,49.275758,1746.527384000063 -120.811234,49.275734,1743.2967999999944 -120.810737,49.27562,1741.583942400053 -120.81057,49.275498,1740.829548799995 -120.810155,49.274969,1731.268127199971 -120.810018,49.274588,1723.0673113600305 -120.809916,49.274424,1719.9121433600596 -120.809775,49.274297,1717.7260519999368 -120.809453,49.274124,1714.2320000000086 -120.80775,49.273458,1706.845519999955 -120.806995,49.273112,1704.2615999999261 -120.805277,49.272681,1700.5649755199468 + + + 0 + clampToGround + -120.805277,49.272681,1700.5649755199468 -120.80527,49.272679,1700.5487567999476 -120.803999,49.272211,1700.4015999999592 -120.80334,49.272009,1696.9351775999621 -120.802416,49.271809,1694.234799999951 -120.80172,49.271709,1691.5803392000162 -120.801228,49.271692,1690.6943999999983 -120.800635,49.27175,1688.0859999999402 -120.800199,49.27172,1685.868400000065 -120.799666,49.271597,1682.7944000000052 -120.798935,49.271503,1679.2200000000494 -120.798178,49.271291,1674.3835999999524 -120.797744,49.271225,1672.6400000000167 -120.797226,49.271214,1672.1162374399987 -120.796468,49.27134,1672.058675200023 -120.795731,49.271312,1671.252826880025 -120.795421,49.271245,1669.4796000000194 -120.794843,49.27105,1666.4348000000007 -120.794351,49.270978,1665.6820028799884 -120.794027,49.271027,1666.5028000000007 -120.792719,49.271558,1662.3204220800321 -120.792272,49.271664,1661.9537203200214 -120.791766,49.271663,1661.2953593599275 -120.790238,49.271477,1654.3665769599572 -120.789907,49.271475,1654.9300000000253 -120.789668,49.271529,1655.6116588800007 -120.789576,49.27149,1655.2836096000044 -120.789344,49.271413,1654.347200000037 -120.789084,49.271368,1652.975979520069 -120.788788,49.271342,1650.73149183993 -120.788083,49.271376,1646.9191999999268 -120.787939,49.271373,1646.2371999999145 -120.787809,49.271351,1645.294000000045 -120.78719,49.271113,1642.1552224000104 -120.787126,49.271089,1642.2943734400055 + + + 0 + clampToGround + -120.787126,49.271089,1642.2943734400055 -120.787023,49.271051,1642.6560020799784 -120.786856,49.270964,1643.4870246399996 -120.786617,49.270818,1645.3575999999512 -120.786366,49.270644,1647.026236159998 -120.785912,49.270174,1644.056596479959 -120.785836,49.270116,1643.256808960022 -120.785766,49.270082,1642.8856000000233 -120.785681,49.270057,1642.6156000000142 -120.785592,49.270039,1642.421199999968 -120.785504,49.270031,1642.1492000000437 -120.785066,49.270038,1639.8855999999987 -120.784348,49.270114,1631.520709119901 -120.783526,49.270359,1625.141599999944 -120.783036,49.270464,1622.4471961599027 -120.782757,49.27049,1619.6132528000783 -120.782465,49.270491,1616.6924000000186 -120.782169,49.270479,1613.7060000000104 -120.781895,49.270455,1612.2604360000112 -120.781594,49.270411,1610.4388000000308 -120.780879,49.270226,1608.7609558400038 + + + 0 + clampToGround + -120.780879,49.270226,1608.7609558400038 -120.781594,49.270411,1610.4388000000308 -120.781895,49.270455,1612.2604360000112 -120.782169,49.270479,1613.7060000000104 -120.782465,49.270491,1616.6924000000186 -120.782757,49.27049,1619.6132528000783 -120.783036,49.270464,1622.4471961599027 -120.783526,49.270359,1625.141599999944 -120.784348,49.270114,1631.520709119901 -120.785066,49.270038,1639.8855999999987 -120.785504,49.270031,1642.1492000000437 -120.785592,49.270039,1642.421199999968 -120.785681,49.270057,1642.6156000000142 -120.785766,49.270082,1642.8856000000233 -120.785836,49.270116,1643.256808960022 -120.785912,49.270174,1644.056596479959 -120.786366,49.270644,1647.026236159998 -120.786617,49.270818,1645.3575999999512 -120.786856,49.270964,1643.4870246399996 -120.787023,49.271051,1642.6560020799784 -120.78719,49.271113,1642.1552224000104 -120.787809,49.271351,1645.294000000045 -120.787939,49.271373,1646.2371999999145 -120.788083,49.271376,1646.9191999999268 -120.788788,49.271342,1650.73149183993 -120.789084,49.271368,1652.975979520069 -120.789344,49.271413,1654.347200000037 -120.789576,49.27149,1655.2836096000044 -120.789668,49.271529,1655.6116588800007 -120.789907,49.271475,1654.9300000000253 -120.790238,49.271477,1654.3665769599572 -120.791766,49.271663,1661.2953593599275 -120.792272,49.271664,1661.9537203200214 -120.792719,49.271558,1662.3204220800321 -120.794027,49.271027,1666.5028000000007 -120.794351,49.270978,1665.6820028799884 -120.794843,49.27105,1666.4348000000007 -120.795421,49.271245,1669.4796000000194 -120.795731,49.271312,1671.252826880025 -120.796468,49.27134,1672.058675200023 -120.797226,49.271214,1672.1162374399987 -120.797744,49.271225,1672.6400000000167 -120.798178,49.271291,1674.3835999999524 -120.798935,49.271503,1679.2200000000494 -120.799666,49.271597,1682.7944000000052 -120.79994,49.27166,1683.9039999999586 + + + 0 + clampToGround + -120.79994,49.27166,1683.9039999999586 -120.800199,49.27172,1685.868400000065 -120.800635,49.27175,1688.0859999999402 -120.801228,49.271692,1690.6943999999983 -120.80172,49.271709,1691.5803392000162 -120.802416,49.271809,1694.234799999951 -120.80334,49.272009,1696.9351775999621 -120.803999,49.272211,1700.4015999999592 -120.80527,49.272679,1700.5487567999476 -120.806995,49.273112,1704.2615999999261 -120.80775,49.273458,1706.845519999955 -120.809453,49.274124,1714.2320000000086 -120.809775,49.274297,1717.7260519999368 -120.809916,49.274424,1719.9121433600596 -120.810018,49.274588,1723.0673113600305 -120.810155,49.274969,1731.268127199971 -120.81057,49.275498,1740.829548799995 -120.810737,49.27562,1741.583942400053 -120.811234,49.275734,1743.2967999999944 -120.811825,49.275758,1746.527384000063 -120.812045,49.275799,1747.3372567999863 -120.812719,49.276065,1749.5476000000338 -120.814371,49.276644,1760.2695500799389 -120.814486,49.276742,1761.081143040003 -120.814757,49.277165,1767.0951999999386 -120.815166,49.277672,1776.128366080012 -120.815897,49.278114,1781.812176320041 -120.816395,49.278706,1789.4468847999951 -120.816883,49.279039,1798.6562729600062 -120.81752,49.279097,1802.0720000000065 -120.817862,49.278979,1803.0983580800057 -120.817879,49.278978,1803.1168995200085 + + + 0 + clampToGround + -120.817879,49.278978,1803.1168995200085 -120.818035,49.278967,1803.2603911999997 -120.818936,49.279237,1807.1824000000054 -120.819951,49.27931,1814.1688448000343 -120.820246,49.279269,1816.3726899199805 -120.820977,49.279054,1822.5107999999645 -120.822043,49.279068,1823.191624959964 -120.822374,49.279069,1823.406400000003 -120.822443,49.279067,1823.4352000000326 -120.822505,49.279052,1823.6257695999648 -120.822645,49.278995,1823.6274040000096 -120.822739,49.278966,1823.4076470400435 -120.822869,49.278947,1823.0439999999855 -120.822975,49.278949,1822.6408000000315 -120.823098,49.278956,1822.2013670399867 -120.823258,49.279,1821.216959999957 -120.823548,49.279081,1819.5420000000377 -120.823868,49.279135,1816.8258272000426 -120.823906,49.279074,1817.8578502400226 -120.824262,49.278904,1817.3551999999866 -120.824285,49.278887,1817.3231999999898 -120.824287,49.278862,1817.6544000000745 -120.824274,49.278837,1818.2015999999207 -120.824241,49.278812,1819.0367999999607 -120.824177,49.278803,1820.0879999998647 -120.824146,49.278802,1820.4744000000276 -120.82412,49.278794,1820.870400000102 -120.82394,49.278735,1823.664000000109 -120.823648,49.278756,1825.195668479946 -120.823608,49.278752,1825.475038720075 -120.823552,49.278737,1825.966718079983 -120.822954,49.278343,1840.4748771200416 -120.822819,49.2783,1842.3032000000044 -120.822801,49.278285,1842.7028000000698 -120.822793,49.278264,1843.1383999999675 -120.822805,49.278131,1845.445999999948 -120.822794,49.278111,1845.8851999999392 -120.822769,49.278095,1846.3487128000334 -120.822586,49.27804,1848.4154751999986 -120.822487,49.277986,1849.0117187200044 -120.82237,49.277906,1849.2519711999848 -120.822278,49.277827,1849.6575817600026 -120.821828,49.277384,1852.3202841600137 -120.821806,49.277359,1852.5244243200145 -120.821787,49.277333,1852.7232796800154 -120.821732,49.277192,1853.9632204799914 -120.821712,49.277158,1854.406936320028 -120.821672,49.277124,1854.7928217600202 -120.821484,49.277041,1855.6044000000475 -120.821141,49.276829,1856.029918559976 -120.820891,49.276574,1855.7619446400763 -120.820848,49.27648,1856.1917183999828 -120.820811,49.276371,1856.678400000089 -120.820804,49.27629,1856.5775999999341 -120.820813,49.27621,1856.7072000000162 -120.820944,49.275684,1857.5935999999601 -120.82103,49.275435,1858.7052719998942 -120.821112,49.275025,1860.0066879999954 -120.821353,49.274672,1862.5841593600207 -120.821382,49.274611,1862.5599299199996 -120.821401,49.274542,1862.3229123200006 -120.821433,49.274359,1862.148800000015 -120.821449,49.274294,1862.3252000000043 -120.821474,49.274244,1862.4152000000072 -120.821685,49.273938,1864.4152687999815 -120.821787,49.273667,1864.9128401599946 -120.821824,49.273546,1865.4687999999771 -120.821816,49.273457,1866.1095999999964 -120.821781,49.273375,1866.6999999999894 -120.821701,49.273305,1867.6119999999876 -120.82161,49.273254,1868.5678623999854 -120.821463,49.273193,1869.668412640011 -120.821328,49.273163,1870.7804268801356 -120.821081,49.273139,1873.3722366399682 -120.821012,49.27313,1873.8491775999473 -120.82094,49.273103,1874.9735872000556 -120.820886,49.273069,1876.3355766400102 -120.82075,49.272964,1879.1356799999705 -120.820236,49.272711,1881.6484684798656 -120.819826,49.272492,1880.9832000001147 -120.819789,49.272458,1880.8067999999973 -120.81977,49.272407,1881.199200000081 -120.819778,49.272326,1882.5095999999955 -120.819878,49.271944,1889.3648000000599 -120.819885,49.27177,1889.515999999985 -120.819869,49.271738,1889.1703999999368 -120.819838,49.271714,1888.5008000000926 -120.819802,49.271692,1887.7231999999071 -120.819775,49.27166,1887.1068800001362 -120.819754,49.271573,1886.272048640046 -120.819748,49.27151,1885.8881216000693 -120.819783,49.27146,1886.2432256000045 -120.81995,49.271308,1888.093632000065 -120.820286,49.27101,1890.0376511999802 -120.820517,49.270825,1890.0299999999925 -120.820541,49.27081,1890.0840000000096 -120.820552,49.270785,1890.1740000000127 -120.820524,49.27019,1889.7975999999765 -120.820553,49.270143,1889.5239999999767 -120.82058,49.270082,1889.1812223999757 -120.820588,49.27002,1888.9467904000046 -120.820588,49.269962,1888.6095999999943 -120.820571,49.269904,1888.2531999999733 -120.820456,49.269724,1887.3712000000003 + + + 0 + clampToGround + -120.820456,49.269724,1887.3712000000003 -120.820383,49.26961,1886.158235199987 -120.820371,49.269579,1885.775035360013 -120.820365,49.26953,1885.135288000047 -120.82037,49.269307,1883.0103999999837 -120.820385,49.269258,1882.6576000000048 -120.820415,49.269219,1882.3767999999975 -120.821039,49.268427,1878.3372000000095 -120.821288,49.268181,1877.4515999999944 -120.821376,49.268093,1877.1348000000012 -120.821434,49.268001,1877.1305046399887 -120.821459,49.267917,1877.1265028800053 -120.821472,49.267721,1876.8776870400036 -120.821469,49.267684,1876.8052723200217 -120.82146,49.267645,1876.7552639999967 -120.821402,49.267462,1876.775256959965 -120.821397,49.267418,1876.6550198399693 -120.821405,49.267372,1876.4499264000135 -120.821463,49.267145,1874.7695408000004 -120.82147,49.267107,1874.2910432000322 -120.821458,49.267071,1873.8444934400197 -120.821426,49.267043,1873.4579974400165 -120.821354,49.266962,1872.3704620799945 -120.821312,49.266904,1871.5487999999189 -120.821299,49.266873,1871.037599999956 -120.821293,49.266814,1869.9971999999482 -120.821291,49.266745,1868.7623999999962 -120.82131,49.266703,1867.9380000000056 -120.821349,49.266658,1867.054480320004 -120.822156,49.266013,1859.671402880004 -120.822203,49.265993,1859.3286238400212 -120.822262,49.265977,1859.0687999999923 -120.822321,49.265975,1859.0399999999627 -120.822457,49.265969,1858.9535999999762 -120.822506,49.265962,1858.842794880019 -120.82255,49.265951,1858.6181520000057 -120.82304,49.265713,1854.323199999951 -120.823078,49.265698,1853.9704000000231 -120.823133,49.265692,1853.686000000025 -120.823553,49.26569,1852.1451999999758 -120.823604,49.265697,1852.0624000000314 -120.823646,49.265708,1852.1951999999496 -120.824001,49.265809,1854.0178446399425 -120.824112,49.265822,1854.6072294399983 -120.824186,49.265824,1854.903938560027 -120.82426,49.265821,1855.128918400025 -120.824329,49.265811,1855.2293857600125 -120.824446,49.265783,1855.2807999999663 -120.824583,49.265742,1855.1836000000583 -120.824689,49.265697,1854.9172000000658 -120.824794,49.265631,1854.721237119963 -120.824842,49.26558,1854.5697088000384 -120.824874,49.265505,1854.357755199926 -120.82489,49.265475,1854.3468399999767 -120.824919,49.265449,1854.469857759925 -120.82516,49.265227,1854.3156928000017 -120.825185,49.265196,1854.2732704000368 -120.825196,49.265155,1854.133475200028 -120.825172,49.264729,1850.311599999992 -120.825175,49.264693,1849.839200000028 -120.825192,49.264662,1849.5152000000276 -120.825291,49.264576,1848.916943359985 -120.825317,49.264546,1848.51402272004 -120.82534,49.26451,1847.9968640000181 -120.825357,49.264476,1847.4867987200284 -120.825448,49.264153,1842.4461497599825 -120.825449,49.264086,1841.6338025600153 -120.825445,49.263992,1840.4653376000333 -120.825425,49.263908,1839.2987359999784 -120.825376,49.263812,1838.1536000000515 -120.825314,49.263732,1837.1312000000237 -120.825232,49.263652,1835.9890585600017 -120.824888,49.263351,1833.0636000000004 -120.8239,49.262659,1840.2999039999995 -120.823518,49.262508,1841.3351999999745 -120.823368,49.262419,1842.8227833599817 -120.82316,49.262243,1844.198275200022 -120.822943,49.262117,1845.054730719945 -120.822891,49.262092,1845.042048639971 -120.822837,49.262065,1844.9280136000436 -120.822783,49.262031,1844.7272257600073 -120.822728,49.261979,1844.8756000000121 -120.822697,49.261936,1845.1254790400224 -120.82259,49.261715,1847.4548720000216 -120.822573,49.261683,1847.6244420800776 -120.822542,49.261645,1847.6875999999686 -120.822295,49.261385,1849.2973360000149 -120.822126,49.261093,1850.9570147199481 -120.822105,49.261053,1851.535717600023 -120.822067,49.261005,1852.137738400064 -120.821986,49.260923,1852.9597091199987 -120.821637,49.260701,1854.2155999999957 -120.821505,49.260559,1854.7988000000068 -120.820944,49.260169,1857.1532000000125 -120.820441,49.259751,1859.2767999999937 -120.820124,49.259431,1859.9360115200548 -120.820027,49.259301,1859.2947881599566 -120.820021,49.259283,1859.239077439964 -120.820016,49.25926,1859.1916927999462 -120.820029,49.259028,1859.4697164800843 -120.820038,49.259007,1859.6258326399475 -120.820058,49.258988,1859.9695001600496 -120.820225,49.25882,1863.0499999998838 -120.820247,49.2588,1863.4459999999071 -120.820272,49.25877,1863.89600000005 -120.820283,49.258738,1864.0752000000493 -120.820296,49.258606,1864.279592959985 -120.8203,49.258583,1864.4131040000389 -120.820313,49.25856,1864.6678687999738 -120.820325,49.258543,1864.8835159999485 -120.820357,49.258516,1865.385547520017 -120.821064,49.258154,1869.9398937600104 -120.821139,49.258109,1870.1004000000082 -120.821194,49.258071,1870.29840000002 -120.821236,49.258023,1870.8011999999808 -120.82127,49.257963,1871.5716000000307 -120.821338,49.257664,1875.045600000008 -120.821403,49.257566,1876.3500700800473 -120.821433,49.257479,1877.170805279998 -120.821727,49.257171,1879.0801033599948 -120.821744,49.257136,1879.1728307199842 -120.821752,49.257089,1879.294666240011 -120.821752,49.257034,1879.4163174400235 -120.821757,49.256996,1879.5296857600283 -120.821768,49.256965,1879.6756096000363 -120.821945,49.256634,1882.888635199988 -120.821957,49.25658,1882.83770239997 -120.821966,49.256506,1882.699283840048 -120.821886,49.256085,1883.5963552000746 -120.821887,49.256035,1884.0409264000587 -120.821895,49.255976,1884.6119584000107 -120.821936,49.255911,1885.3934003199831 -120.821987,49.255866,1885.8008326400093 -120.82205,49.255824,1886.0 -120.82216,49.255786,1886.0 -120.822248,49.255769,1885.9071999999933 -120.822358,49.255762,1885.511200000021 -120.822492,49.255761,1885.0288000000112 + + + 0 + clampToGround + -120.822492,49.255761,1885.0288000000112 -120.822581,49.25576,1884.9230176000028 -120.822787,49.255758,1884.7288000000005 -120.822897,49.255763,1884.7468000000063 -120.823002,49.255776,1884.7935999999904 -120.823283,49.255797,1884.1574990399897 -120.823925,49.25578,1881.9569599999766 -120.824072,49.255763,1881.101709439983 -120.824222,49.255728,1880.5591999999947 -120.824364,49.255676,1880.422399999982 -120.824574,49.255559,1880.0481833600222 -120.824643,49.255524,1879.7976000000297 -120.824719,49.255517,1879.300800000005 -120.824797,49.255522,1878.9723999999926 -120.825031,49.255546,1877.841760960015 -120.825085,49.255536,1877.5072575999936 -120.825131,49.25551,1877.3074576000026 -120.825537,49.255168,1874.1740006400269 -120.825575,49.255131,1873.8599999999956 -120.825629,49.255102,1873.471199999954 -120.825694,49.255099,1873.00320000001 -120.825766,49.255112,1872.4847999999888 -120.825839,49.255146,1871.9591999999857 -120.826223,49.255305,1869.096400000009 -120.826267,49.255315,1868.7435999999789 -120.826321,49.255322,1868.3296000000519 -120.826833,49.255322,1863.108271040075 -120.826923,49.255299,1862.1610980800676 -120.827012,49.255255,1861.1490848000606 -120.827094,49.255204,1860.3979980799763 -120.827158,49.255143,1860.1560435200602 -120.827234,49.255046,1860.3689785599504 -120.827482,49.254528,1863.0975999999941 -120.82755,49.254436,1864.332000000038 -120.827604,49.254399,1865.192399999989 -120.827649,49.254372,1865.840400000041 -120.827728,49.254344,1866.6287999999179 -120.827811,49.254325,1867.2014279999682 -120.827909,49.254307,1867.7077435200652 -120.828175,49.254299,1867.936747999924 -120.828196,49.254297,1867.947572479965 -120.828213,49.254288,1868.1316217599324 -120.828386,49.254191,1870.197200000051 -120.828413,49.254179,1870.4023999999936 -120.828448,49.254174,1870.4024000000445 -120.829277,49.254169,1867.9378635200685 -120.829361,49.254183,1867.5472635200651 -120.829426,49.254191,1867.3050166400371 -120.829492,49.254194,1867.1959539200507 -120.829563,49.254187,1867.3831582399498 -120.829986,49.254084,1870.0832000000287 -120.830032,49.25407,1870.4360000000331 -120.830084,49.254064,1870.5872000000095 -120.830858,49.254044,1875.7487718400173 -120.830908,49.254047,1875.8908019199762 -120.830968,49.254056,1875.973711360021 -120.831783,49.254183,1873.9764508800172 -120.83272,49.254387,1869.7853856000022 -120.832822,49.254422,1869.096063360044 -120.832898,49.254447,1868.5304000000142 -120.833358,49.254718,1864.062149760046 -120.833444,49.254755,1863.3390111999406 -120.83353,49.254776,1862.9754688000037 -120.833618,49.254792,1862.7625702399691 -120.833689,49.254817,1862.4787275199642 -120.834244,49.254955,1859.7648991999588 -120.834318,49.254961,1859.6255100799772 -120.834385,49.25496,1859.6068160000473 -120.83486,49.254855,1858.5739999999646 -120.834927,49.254831,1858.3508000000165 -120.835005,49.254822,1857.87993440007 -120.835603,49.254816,1854.365662079989 -120.835664,49.254827,1853.8576588800495 -120.835731,49.254854,1853.1388310400039 -120.835887,49.254946,1851.2343580800396 -120.835943,49.25496,1850.699251200035 -120.835997,49.254948,1850.4934982399752 -120.836061,49.254919,1850.474595359981 -120.836161,49.254888,1850.3260147199594 -120.836272,49.254883,1849.7703590400424 -120.836451,49.254892,1848.4968000000574 -120.836628,49.25492,1846.7183999999943 -120.836756,49.254969,1845.1646188799843 -120.83692,49.255009,1843.896451199952 -120.837073,49.255009,1842.8949947199505 -120.837178,49.254998,1842.3400000000347 -120.837368,49.254946,1841.6356211200546 -120.837428,49.254918,1841.6226841599357 -120.837507,49.254904,1841.3407091199383 -120.838444,49.254882,1834.3347596800286 -120.838534,49.254889,1833.1425289600695 -120.838603,49.254904,1832.1344915198888 -120.839124,49.255112,1825.0976000000464 -120.839185,49.255127,1824.4605751999777 -120.839241,49.255125,1823.9676199999926 -120.840111,49.254989,1817.2641758400391 + + + 0 + clampToGround + -120.840111,49.254989,1817.2641758400391 -120.840116,49.254988,1817.2331596800539 -120.840596,49.254883,1814.1299999999655 -120.84105,49.254872,1811.3213760000237 -120.841103,49.254872,1811.0426553599793 -120.841153,49.254862,1811.0 -120.841632,49.254752,1811.9690214400273 -120.842209,49.254738,1815.063296320011 -120.84226,49.25473,1815.192000000001 -120.842309,49.254712,1815.3262073600035 -120.842782,49.254512,1814.4900966399873 -120.842819,49.254496,1814.3987430400284 -120.842861,49.254489,1814.3688558399956 -120.842979,49.254475,1814.2996839999935 -120.843029,49.254463,1814.194013920013 -120.843078,49.254444,1814.0789414400176 -120.843532,49.254156,1811.257336320019 -120.843805,49.253836,1808.9211999999864 -120.843837,49.253787,1808.4532000000424 -120.843853,49.253734,1808.0140000000258 -120.843861,49.253332,1802.0807180799907 -120.843864,49.253283,1801.3485644800585 -120.84389,49.253251,1800.817214399987 -120.843938,49.253218,1800.4425926399404 -120.84401,49.253178,1799.9553887999693 -120.84413,49.253101,1798.79640480002 -120.844197,49.253066,1798.1921059199424 -120.844266,49.253017,1796.79033023999 -120.844517,49.25276,1787.4240000000677 -120.844545,49.252736,1786.6463999999592 -120.844562,49.25271,1785.8040000000142 -120.844628,49.252464,1777.9632000000965 -120.84463,49.252435,1777.1279999999542 -120.844618,49.252405,1776.2640000000893 -120.8445,49.252249,1771.7711999999729 -120.844505,49.252212,1770.623977600004 -120.844514,49.252169,1769.0360233600743 -120.84459,49.25209,1765.9905760001038 -120.844607,49.252064,1764.9706700799 -120.844598,49.252045,1764.2673136000844 -120.844513,49.251919,1760.020400000003 -120.844499,49.251867,1758.5731999999223 -120.844497,49.251829,1757.486000000007 -120.844516,49.251781,1756.0352000000375 -120.844574,49.251747,1754.8472000000697 -120.844913,49.251592,1749.6649881600654 -120.845015,49.251551,1748.2839143999813 -120.84506,49.251528,1747.6153728000206 -120.845091,49.251498,1746.6638812799279 -120.845157,49.251275,1739.7199999999912 -120.845216,49.251219,1738.1071999999704 -120.845295,49.251165,1736.490000000034 -120.845365,49.251129,1735.2011999999625 -120.845437,49.251101,1734.1928644799507 -120.846184,49.250924,1725.9952947199818 -120.846248,49.250913,1725.0986700798805 -120.846318,49.250906,1724.2480793600482 -120.846655,49.250891,1721.1600000001147 -120.846697,49.250881,1720.4788612799418 -120.846726,49.250868,1719.794142719864 -120.846764,49.250836,1718.3918361600358 -120.846964,49.250609,1712.0135999999407 -120.847003,49.250578,1711.0631999999357 -120.847052,49.250566,1710.451199999946 -120.847289,49.25056,1708.8555999999899 -120.84736,49.250545,1708.3331520000593 -120.847407,49.250523,1707.7880385600033 -120.847437,49.250501,1707.2893435200006 -120.847453,49.250472,1706.7160953600278 -120.847499,49.250394,1705.097693760005 -120.847571,49.250334,1703.5008000000262 -120.847732,49.25025,1701.1966400000479 -120.84785,49.250214,1700.3100960000374 -120.847976,49.250198,1699.4905459199806 -120.84818,49.250191,1697.9640448000075 -120.848408,49.250202,1696.116271359956 -120.848542,49.250236,1695.2342195200467 -120.848665,49.250281,1694.4550504000458 -120.848822,49.250349,1693.6862588800147 -120.848912,49.250411,1693.708702720009 -120.848971,49.250494,1694.4568950400042 -120.848988,49.250533,1694.9326278399144 -120.849033,49.250552,1694.9045593599424 -120.849092,49.25055,1694.4429759999564 -120.849148,49.250531,1693.6983404800437 -120.849202,49.250522,1693.1415999998999 -120.849296,49.250535,1692.6988000000204 -120.849701,49.250598,1692.2871260799136 -120.849749,49.250601,1692.389428960099 -120.849792,49.250594,1692.3236339199675 -120.850013,49.250509,1691.0255126400314 -120.850045,49.250499,1690.8744335999381 -120.850079,49.250493,1690.8051062399581 -120.850173,49.250483,1690.7302492800666 -120.850206,49.250479,1690.6792300799614 -120.850243,49.250474,1690.5995174399247 -120.850449,49.250417,1689.3170603199633 -120.850499,49.25041,1689.2253136000568 -120.850551,49.250407,1689.2476292800604 -120.851335,49.250395,1687.5798679999011 -120.851385,49.250398,1687.1980592000039 -120.851427,49.250405,1686.8040375999988 -120.851618,49.250438,1685.0605446400302 -120.851685,49.250445,1684.4139999999452 -120.851742,49.250439,1683.668799999985 -120.851844,49.250415,1682.0487999999311 -120.851951,49.250394,1680.4868000000013 -120.852125,49.25039,1679.7740000000615 -120.852504,49.250413,1677.8997702400582 -120.852669,49.250439,1677.4230539199643 -120.852866,49.250482,1676.6545004799448 -120.853099,49.250553,1675.8266388799757 -120.853215,49.250583,1675.8632887999593 -120.85333,49.250599,1675.6394768000175 -120.853464,49.250597,1674.655999999969 -120.853579,49.250602,1673.900000000036 -120.85382,49.250662,1673.7807999999807 -120.853909,49.250678,1673.6908000000033 -120.853989,49.250672,1673.3164000000277 -120.85418,49.250606,1671.6871167999675 -120.854226,49.250587,1671.263379520055 -120.854312,49.250572,1670.7445734399948 -120.854604,49.250567,1670.780399999921 -120.854673,49.25055,1670.7663439999878 -120.854716,49.250511,1670.4999929600212 -120.854816,49.250401,1669.4805593599579 -120.854861,49.250365,1669.098874400027 -120.85493,49.250339,1668.826059199953 -120.855051,49.250302,1668.013619839945 -120.855395,49.250059,1665.245632799991 -120.855744,49.249876,1667.1956377599838 -120.855836,49.249833,1668.3858284799549 -120.855937,49.249809,1668.1853876800556 -120.856056,49.249793,1667.5062476801406 -120.856189,49.249792,1666.5000364800148 -120.856397,49.249828,1664.2871193599724 -120.856497,49.249846,1663.275827520022 -120.856578,49.249849,1662.7543171200616 -120.856707,49.249835,1662.3915024000537 -120.856922,49.249783,1662.251041920004 -120.857053,49.249762,1661.7575251200763 -120.857205,49.249753,1660.8886608000555 -120.857347,49.249757,1659.1215195199381 -120.857452,49.249771,1657.7757689600671 -120.857562,49.249785,1656.9523568000013 -120.857672,49.249825,1656.5216960000325 -120.857774,49.249872,1656.0209331199655 -120.857844,49.249884,1655.6227558399985 -120.857912,49.249883,1655.2371238400115 -120.85797,49.24987,1654.9398560000266 -120.858023,49.249834,1654.7619612799858 -120.858056,49.249769,1654.831599999998 -120.858089,49.249676,1655.6856345599908 -120.858109,49.249573,1657.2521572800274 -120.858127,49.249534,1657.884678720036 -120.858167,49.249504,1658.4575827199574 -120.85842,49.249391,1660.9619999999495 -120.858478,49.249356,1661.5920000000215 -120.858521,49.249325,1662.1500000000196 -120.858674,49.24907,1665.8436127999641 -120.858701,49.249019,1666.214774240012 -120.858703,49.248934,1666.7289219200127 -120.858659,49.248676,1667.7663999999884 -120.85865,49.248654,1667.845599999993 -120.858626,49.248633,1667.9212000000068 -120.858473,49.248474,1669.9716358399637 -120.858462,49.24846,1670.2160383999671 -120.858461,49.248437,1670.5578454399358 -120.858478,49.248308,1672.414399999935 -120.858478,49.248261,1673.2603999999735 -120.858458,49.248207,1674.376399999995 -120.858311,49.248063,1678.1050452799595 -120.8583,49.24801,1678.9766399999917 -120.858311,49.24796,1679.5618848000088 -120.858358,49.247899,1680.076800000079 -120.858455,49.247846,1680.1415999999153 + + + 0 + clampToGround + -120.858455,49.247846,1680.1415999999153 -120.858464,49.247841,1680.1487999999995 -120.858595,49.247748,1680.538582400021 -120.858646,49.247684,1681.1416025600474 -120.858674,49.247604,1682.1912358399927 -120.858686,49.247517,1683.4691004799472 -120.858683,49.247419,1684.3243999999977 -120.858603,49.247261,1685.7499999999795 -120.85858,49.246952,1686.1119999999876 -120.858575,49.246924,1686.0659679999926 -120.858559,49.246895,1686.0429928000076 -120.858327,49.246689,1687.066566879971 -120.858297,49.246654,1687.5172710400761 -120.85827,49.246618,1688.129708799975 -120.858258,49.246587,1688.5184396799607 -120.858276,49.246561,1688.6033708799728 -120.858311,49.246537,1688.5665385600325 -120.858423,49.24652,1687.767961600014 -120.85846,49.246508,1687.5411328000703 -120.858481,49.246489,1687.5155886400507 -120.858492,49.24646,1687.664627200026 -120.858563,49.246196,1689.362259840001 -120.858606,49.24552,1695.4959551999966 -120.858624,49.245477,1696.2108780800224 -120.858661,49.245417,1697.2648155199429 -120.858689,49.245382,1697.8848060800005 -120.858732,49.24535,1698.5083519999903 -120.858801,49.245311,1699.2869585599876 -120.858975,49.245252,1700.6171679999954 -120.859009,49.24523,1701.022772800009 -120.859022,49.245201,1701.440850879962 -120.859027,49.244968,1704.2827225599826 -120.858952,49.244817,1705.2811206400343 -120.858946,49.244788,1705.543886080016 -120.858956,49.244754,1705.9260390400216 -120.858986,49.244715,1706.3496000000057 -120.859055,49.24464,1706.5979999999925 -120.85908,49.244611,1706.688000000021 -120.859091,49.244572,1706.7276000000234 -120.859088,49.244531,1706.7167999999995 -120.859072,49.244491,1706.6591999999914 -120.859023,49.244423,1706.445527839974 -120.858973,49.244355,1706.2052984000134 -120.858913,49.244079,1706.0 -120.858895,49.244033,1706.0 -120.858846,49.243983,1706.0 -120.858679,49.243796,1706.4161273599796 -120.858675,49.243759,1706.5751480000213 -120.858686,49.243718,1706.7810579200075 -120.858727,49.243657,1707.183078560011 -120.858775,49.243617,1707.5562919999775 -120.859291,49.243337,1709.4284916800127 -120.859316,49.243321,1709.4932000000308 -120.859323,49.243299,1709.4392000000134 -120.859338,49.243202,1709.143999999966 -120.859345,49.243172,1709.0612000000215 -120.859365,49.243148,1709.046799999981 -120.859526,49.243024,1709.2133529600057 -120.859552,49.242994,1709.2514035199788 -120.859567,49.242961,1709.2509844799945 -120.859526,49.242495,1705.5639152000088 -120.859531,49.242448,1705.377668480041 -120.859553,49.242384,1705.2008019200368 -120.859598,49.242322,1705.1105657600347 -120.859646,49.242264,1704.9851302399834 -120.859893,49.242017,1706.5141857600022 -120.859952,49.241975,1706.80819200003 -120.860028,49.241944,1707.2015999999985 -120.860105,49.241921,1707.7560000000312 -120.860169,49.241888,1708.2167999999933 -120.860271,49.241828,1708.9511999999809 -120.860362,49.241753,1709.3031999999819 -120.860405,49.241693,1709.4580000000003 -120.86044,49.241637,1709.4771999999791 -120.860461,49.241604,1709.4340000000116 -120.860759,49.241345,1710.3490808000174 -120.860816,49.241308,1710.6021708799872 -120.860896,49.241271,1711.026799999968 -120.860948,49.241247,1711.3147999999571 -120.861025,49.241213,1711.746799999992 -120.861272,49.241058,1712.468456960004 -120.861315,49.241024,1712.5038176000196 -120.861328,49.240987,1712.431938559992 -120.861349,49.240953,1712.368937119999 -120.861387,49.240921,1712.3134539199932 -120.861731,49.240751,1710.3424000000546 -120.861773,49.240735,1710.3208000000068 -120.861818,49.240725,1710.3747999999728 -120.861868,49.240714,1710.4359999999717 -120.861944,49.2407,1710.558399999944 -120.862012,49.240677,1710.6611270399403 -120.862169,49.2406,1710.4177439999849 -120.862188,49.240581,1710.2319148799966 -120.862196,49.240557,1709.9259091200265 -120.862208,49.240489,1709.721467519976 -120.86221,49.240465,1709.6443440000123 -120.862228,49.240438,1709.609597440001 -120.862269,49.240397,1709.6698772799948 -120.862337,49.240329,1709.673794079999 -120.862416,49.240177,1709.807310719988 -120.862471,49.240064,1709.9759462400007 -120.862542,49.239956,1710.2856499200163 -120.862615,49.239876,1710.6755904000054 -120.862736,49.239774,1710.9719654400003 -120.862886,49.239667,1711.1213475200038 -120.862903,49.239652,1711.1388377599983 -120.86291,49.239627,1711.179627200003 -120.862877,49.239395,1711.9987999999962 + + + 0 + clampToGround + -120.862877,49.239395,1711.9987999999962 -120.862851,49.239214,1713.3955999999996 -120.862846,49.239191,1713.5791999999965 -120.862836,49.239171,1713.7592000000025 -120.862822,49.239158,1714.023032959952 -120.862727,49.239089,1715.9409782399678 -120.862713,49.23907,1716.3920928000402 -120.862704,49.239052,1716.7887206400083 -120.862723,49.238896,1719.8564966400622 -120.862733,49.238875,1720.241939999998 -120.862754,49.238851,1720.670324160054 -120.863142,49.238626,1723.3505196800947 -120.863163,49.238613,1723.5697697599799 -120.863177,49.238596,1723.780400000008 -120.863183,49.238577,1724.0323999999857 -120.863184,49.238559,1724.2879999999543 -120.863169,49.238186,1729.085630720011 -120.863161,49.238159,1729.3520739199737 -120.863153,49.238138,1729.5489651200023 -120.863134,49.238117,1729.7150662399808 -120.863108,49.238094,1729.8597401600202 -120.863065,49.238072,1729.9115744000003 -120.863002,49.238041,1730.1470972799725 -120.862917,49.238017,1730.3471665600046 -120.862831,49.238002,1730.4225404799872 -120.862758,49.237998,1730.429152640019 -120.862648,49.237992,1730.5644953600197 -120.862613,49.237989,1730.6213307200137 -120.862578,49.237976,1730.7787788799685 -120.862483,49.237938,1731.2696000000026 -120.862448,49.237922,1731.4424000000267 -120.862429,49.237893,1731.7556000000034 -120.862413,49.237848,1732.2416000000042 -120.862379,49.237816,1732.5871999999758 -120.862338,49.237782,1732.9543999999696 -120.862142,49.237657,1732.836770240013 -120.861624,49.237136,1733.0847999999824 -120.861612,49.237122,1733.2792000000288 -120.861605,49.237101,1733.5311999999808 -120.861602,49.236828,1736.4903999999533 -120.861608,49.236811,1736.6523999999536 -120.861622,49.236798,1736.7424000000078 -120.861725,49.236687,1737.3757719999173 -120.861732,49.236673,1737.4665625599428 -120.861739,49.236654,1737.5703999999891 -120.861737,49.236459,1738.988799999938 -120.861725,49.236422,1739.3415999999681 -120.861693,49.236393,1739.7807999999848 -120.861663,49.236378,1740.0137174400263 -120.861616,49.236363,1740.19939968001 -120.861548,49.236349,1740.4885459200084 -120.861467,49.236323,1740.8892993599825 -120.861369,49.236275,1741.439356000009 -120.861289,49.236221,1741.8217422399994 -120.861254,49.236195,1742.0369487999824 -120.86124,49.236164,1742.2435456000017 -120.86123,49.236117,1742.5386735999775 -120.861222,49.235779,1745.2270329600005 -120.861262,49.235729,1745.551148160031 -120.861481,49.235508,1745.518799999993 -120.861504,49.235483,1745.540400000015 -120.861518,49.235455,1745.6915999999403 -120.861521,49.235424,1745.994000000021 -120.861509,49.235392,1746.469200000049 -120.861482,49.235363,1747.074000000057 -120.861438,49.235335,1747.8515999999354 -120.861396,49.235315,1748.5212000000097 -120.861336,49.235299,1749.1661465600657 -120.861269,49.235285,1749.7964216000742 -120.861022,49.235261,1751.7019999999557 -120.860903,49.235261,1752.5588000000434 -120.860877,49.235257,1752.7603999999951 -120.860849,49.23525,1752.987199999985 -120.860826,49.23524,1753.1887999999876 -120.860788,49.235221,1753.530799999994 -120.860537,49.235075,1755.8148360000225 -120.860519,49.235065,1755.9283944000194 -120.860511,49.235049,1756.01229456 -120.860511,49.235029,1756.0727457599985 -120.86053,49.235005,1756.0756560000073 -120.860549,49.234978,1756.0254691199768 -120.860556,49.23494,1755.9967999999817 -120.860553,49.234896,1756.0126444800203 -120.860548,49.234877,1756.0392441600297 -120.860538,49.234858,1756.0955078399702 -120.860512,49.234831,1756.2521971200003 -120.860506,49.234821,1756.293360959997 -120.860502,49.234808,1756.326063360019 -120.860478,49.234652,1757.3873817600315 -120.860451,49.234612,1758.0925555200388 -120.860419,49.234548,1759.1731315201068 -120.860384,49.234514,1759.946952960073 -120.860353,49.234493,1760.5357358399579 -120.86031,49.234474,1761.2387424000349 -120.860268,49.234461,1761.8319999999194 -120.860151,49.234455,1762.76080000003 -120.860089,49.234436,1763.4406598399623 -120.859964,49.234368,1765.0847999999262 -120.859933,49.234333,1765.6860000000197 -120.859914,49.234265,1766.5571999999688 -120.85992,49.233967,1768.5018143999769 -120.859949,49.233896,1768.5204998399906 -120.859976,49.233835,1768.824438399977 -120.860019,49.233771,1769.0970289600848 -120.860066,49.233723,1769.2206947199982 -120.860199,49.233614,1769.5285494400023 -120.860282,49.233531,1770.1384000000178 -120.860301,49.233511,1770.357999999975 -120.860315,49.233486,1770.6676000000118 -120.860311,49.233453,1771.1572000000547 -120.860298,49.233413,1771.780000000017 -120.860235,49.233303,1773.2800167999908 -120.860175,49.233161,1774.2199479999979 -120.860087,49.232755,1775.3172824000424 -120.860067,49.232598,1775.0265046400043 -120.860061,49.232576,1774.9939174399967 -120.860046,49.232549,1774.981588160009 -120.860028,49.232523,1774.9736537600024 -120.859924,49.23243,1775.3425471999697 -120.859897,49.232394,1775.5122972799825 -120.859877,49.232359,1775.6675652800106 -120.859865,49.232316,1775.8079264000094 -120.859861,49.232218,1776.06159999999 -120.859864,49.232171,1776.7167999999729 -120.859874,49.232135,1777.1631999999713 -120.860007,49.231926,1779.0572000000334 -120.860018,49.231907,1779.075200000039 -120.860028,49.23188,1779.1616000000256 -120.860043,49.231787,1779.6691999999719 -120.860047,49.231766,1779.7772000000573 -120.860061,49.231747,1779.7627999999913 -120.86012,49.231671,1779.672800000065 -120.860141,49.231644,1779.477200000014 -120.860151,49.23162,1779.3691999999796 -120.860155,49.231601,1779.3259999999352 -120.860159,49.231582,1779.2828000000443 -120.860157,49.231558,1779.3043999999898 -120.860151,49.231528,1779.3691999999796 -120.86014,49.231506,1779.4879999999866 -120.860108,49.231471,1779.833600000035 -120.860101,49.231458,1779.9091999999976 -120.860099,49.231448,1779.930799999943 + + + 0 + clampToGround + -120.860099,49.231448,1779.930799999943 -120.860097,49.231441,1779.952400000042 -120.860094,49.231258,1780.2965459199613 -120.860093,49.231239,1780.3545419199156 -120.860089,49.231225,1780.439763999968 -120.860069,49.231173,1780.8389435200625 -120.860062,49.231158,1780.976076160009 -120.860061,49.231138,1781.0460572799716 -120.860062,49.230996,1781.1072000000113 -120.860055,49.230978,1781.2079999999614 -120.860039,49.230955,1781.4383999999936 -120.860012,49.230935,1781.8272000000352 -120.859986,49.230923,1782.1674691199235 -120.859956,49.230914,1782.5211993600367 -120.859927,49.230909,1782.859986720009 -120.859756,49.230907,1784.8681516799545 -120.859725,49.230903,1785.2182920000228 -120.859694,49.230895,1785.6058447999267 -120.859658,49.230881,1786.056726080045 -120.859618,49.23086,1786.560780800034 -120.859432,49.230747,1789.1174476800168 -120.859418,49.230732,1789.2598579199475 -120.859408,49.230709,1789.3798502399823 -120.859411,49.230694,1789.361585280061 -120.859421,49.230676,1789.2644083200262 -120.859459,49.23065,1788.7903999999844 -120.859625,49.230544,1786.358400000092 -120.859648,49.230516,1785.9263999999034 -120.85966,49.230479,1785.6203999999343 -120.859657,49.230445,1785.541200000032 -120.85965,49.230412,1785.523199999975 -120.859624,49.230382,1785.7896000000442 -120.859264,49.230119,1790.5053772800643 -120.859247,49.230104,1790.6646470400353 -120.859235,49.230083,1790.712590400026 -120.859221,49.230052,1790.7143673599412 -120.859212,49.229991,1790.483287680001 -120.859195,49.229971,1790.6002488 -120.859172,49.229949,1790.7623251199825 -120.859114,49.22991,1791.1833695999353 -120.859098,49.229887,1791.2342390399626 -120.859081,49.229855,1791.242215199973 -120.859078,49.229842,1791.207239040019 -120.859067,49.229756,1790.8828300800394 -120.85906,49.229732,1790.8167167999961 -120.859045,49.229711,1790.8987048000336 -120.858922,49.229572,1791.826063359965 -120.85891,49.229533,1791.899771200025 -120.85891,49.229501,1791.9085264000241 -120.858919,49.22947,1791.8816271999995 -120.858948,49.229442,1791.811727360019 -120.85899,49.229424,1791.8300095999625 -120.859179,49.229347,1791.6416244800328 -120.85941,49.229213,1791.0605168000147 -120.859434,49.229201,1790.946952640034 -120.859467,49.229191,1790.8240868800187 -120.85959,49.22914,1790.5673920000086 -120.859619,49.229118,1790.501808640025 -120.859633,49.229104,1790.466125439989 -120.859646,49.229084,1790.4377228800115 -120.859656,49.229049,1790.440372479987 -120.859678,49.228926,1790.435061759975 -120.859676,49.228901,1790.4846899200704 -120.859666,49.228881,1790.6355999999496 -120.859599,49.228747,1791.8415999999743 -120.859589,49.228732,1792.0036 -120.859585,49.228714,1792.11160000006 -120.859618,49.228427,1791.462800000039 -120.859629,49.22841,1791.2828000000075 -120.859656,49.228395,1790.9371999999848 -120.859743,49.228377,1789.932800000065 -120.859767,49.228368,1789.641199999957 -120.859791,49.228352,1789.3243999999895 -120.860042,49.228168,1787.2927999999613 -120.860071,49.228156,1787.1271999999701 -120.860104,49.22815,1786.9111999999527 -120.860196,49.228148,1786.2559999999953 -120.86022,49.228149,1786.079600000006 -120.860247,49.228141,1785.9139999999893 -120.86047,49.227983,1784.5268495999085 -120.860484,49.227962,1784.5331596800213 -120.86049,49.227947,1784.5818288000137 -120.860488,49.227916,1784.8565836799498 -120.860473,49.227889,1785.2690011200339 -120.860415,49.227824,1786.6070015999708 -120.860381,49.227801,1787.293865759951 -120.860381,49.227777,1787.5240809599013 -120.860395,49.227745,1787.765592000059 -120.860486,49.227603,1788.8324966400216 -120.860509,49.227454,1790.6664000000367 -120.860526,49.227429,1790.7240000000447 -120.860544,49.227413,1790.7095999999788 -120.86057,49.227399,1790.5712000000592 -120.860612,49.227386,1790.211199999945 -120.860672,49.227374,1789.6496000000839 -120.860733,49.227346,1789.1924000000615 -120.860792,49.2273,1788.8863999999644 -120.860832,49.227243,1788.8647999999678 -120.860859,49.227164,1789.0978329599723 -120.860864,49.227125,1789.2009599999653 -120.860861,49.227062,1789.419750720012 -120.860885,49.227034,1789.3655664000137 + + + 0 + clampToGround + -120.860885,49.227034,1789.3655664000137 -120.860786,49.226994,1790.1927993599716 -120.860692,49.226943,1791.0150457599957 -120.860547,49.226845,1792.0813736000298 -120.860521,49.226827,1792.3206036799477 -120.860499,49.226803,1792.5071268799586 -120.860479,49.226747,1792.6309035200197 -120.860466,49.226718,1792.704379519979 -120.860443,49.226692,1792.8473542399727 -120.860247,49.226582,1793.5011999999997 -120.859968,49.22642,1793.3391999999994 -120.859641,49.226277,1792.9093787200363 -120.859326,49.226171,1792.8576000000207 -120.85918,49.226123,1793.0376000000267 -120.859053,49.226064,1792.7996006400003 -120.858807,49.225936,1793.512716160002 -120.857786,49.225283,1802.9374435199095 -120.857667,49.225204,1802.7889574399383 -120.857565,49.225132,1802.1536064000552 -120.857459,49.225034,1800.7776662399924 -120.857318,49.224881,1799.222287680008 -120.857275,49.224818,1798.7199119999814 -120.857247,49.224719,1797.7867999999967 -120.857215,49.224602,1796.7908528000144 -120.857224,49.224558,1796.2136000000037 -120.85725,49.224506,1795.464800000027 -120.857287,49.224452,1794.6151999999977 -120.857317,49.224387,1793.9040000000384 -120.85733,49.224344,1793.500799999982 -120.857332,49.224309,1793.2343999999896 -120.857306,49.224089,1792.59207264005 -120.857286,49.224057,1792.8449539199773 -120.857259,49.224045,1793.115208800018 -120.857212,49.224041,1793.6031033599409 -120.857093,49.224033,1795.3594915199117 -120.857059,49.224027,1795.8499094399037 -120.857017,49.22401,1796.4246336000133 -120.856908,49.22393,1797.429817599979 -120.856808,49.223885,1797.9963999999836 -120.856616,49.22381,1799.7793983999748 -120.856421,49.22378,1801.698515200025 -120.856253,49.223771,1803.9568000000681 -120.855899,49.22378,1808.7550688000754 -120.855761,49.223804,1810.396021759995 -120.855718,49.223805,1810.8182096000323 -120.855671,49.223796,1811.2233766399913 -120.855528,49.223749,1812.1412428799485 -120.855483,49.223733,1812.2534145599886 -120.855433,49.22373,1812.4860336000036 -120.855391,49.223735,1812.7486103999931 -120.855312,49.223753,1813.3462694400332 -120.855239,49.22377,1813.995200000038 -120.855218,49.223777,1814.2220000000282 -120.855186,49.223778,1814.4632000000074 -120.855158,49.22377,1814.578399999998 -120.855061,49.223683,1814.3371999999677 -120.855025,49.223669,1814.4452000000274 -120.854978,49.223663,1814.7832054399933 -120.854901,49.223662,1815.553507519994 -120.854226,49.223673,1821.9800099200143 -120.854145,49.223681,1823.0275503999585 -120.854075,49.223692,1824.0014080000647 -120.853972,49.223718,1825.4182643200024 -120.85393,49.223723,1825.9300287999363 -120.853892,49.223725,1826.374383999985 -120.853843,49.223721,1826.6915062400035 -120.8538,49.223712,1826.9156479999522 -120.853583,49.223627,1827.3682113600019 -120.853177,49.223493,1830.173897440007 -120.853112,49.223464,1830.4340147200076 -120.853063,49.223433,1830.6315841599924 -120.852847,49.223272,1833.6606233599914 -120.852613,49.22313,1838.0053775999957 -120.852587,49.223115,1838.4505752000027 -120.852563,49.22308,1839.1744415999271 -120.852545,49.222995,1840.9840000000108 -120.852521,49.222959,1841.9343999999646 -120.852485,49.222928,1842.8924031999195 -120.852153,49.222709,1850.0473019199508 -120.852046,49.222674,1852.0082118400271 -120.851959,49.222665,1853.257525600094 -120.851862,49.222676,1855.167252480074 -120.851771,49.222703,1856.8179115199614 -120.851688,49.222729,1858.353886080084 -120.8516,49.222736,1859.9519999999568 -120.851484,49.22274,1861.9679999999828 -120.85146,49.222745,1862.3099999998867 -120.851429,49.22276,1862.598000000055 -120.851278,49.222922,1865.674400000197 -120.851268,49.222933,1865.9227999998768 -120.851252,49.22294,1866.3583999999282 -120.85123,49.222942,1866.984799999958 -120.851205,49.222936,1867.7264000002087 -120.850989,49.222815,1873.5036000000441 -120.850969,49.222807,1873.9643999998527 -120.850947,49.222802,1874.4575999998865 -120.850926,49.222811,1874.878799999974 -120.850911,49.222827,1875.145200000069 -120.850863,49.222967,1875.677999999926 -120.850832,49.223152,1876.0288000000664 -120.850802,49.22325,1876.6767999999652 -120.850762,49.223332,1877.5407999999325 -120.850719,49.223385,1878.6556000000446 -120.850672,49.223428,1879.8255999999299 -120.85056,49.223508,1882.5327999999759 -120.850434,49.223577,1885.940399999819 -120.850396,49.223588,1886.9375999999102 -120.850354,49.223586,1887.9888000000956 -120.850316,49.223573,1888.8995999998424 -120.850134,49.223486,1892.8883270400704 -120.850099,49.223454,1893.660020159992 -120.850073,49.223423,1894.245231840123 -120.850064,49.223384,1894.4292249599223 -120.850024,49.223196,1895.3951999998797 -120.850001,49.223157,1895.9747999998488 -120.849984,49.223129,1896.4455705598248 -120.849812,49.222866,1901.6349516799958 -120.849756,49.222766,1903.2708441600178 + + + 0 + clampToGround + -120.849756,49.222766,1903.2708441600178 -120.849731,49.222723,1904.3641684798345 -120.849699,49.222658,1905.9623999999394 -120.849683,49.222626,1906.7688000000521 -120.849651,49.222603,1907.9388000001677 -120.849593,49.222582,1909.8360000000077 -120.849516,49.222545,1912.453200000204 -120.849479,49.222517,1913.8211999998966 -120.84945,49.222488,1915.012799999893 -120.849408,49.222395,1917.579180800112 -120.849404,49.222312,1919.0220262397381 -120.849418,49.222249,1919.5388681600318 -120.849451,49.222206,1919.1629782399461 -120.849475,49.222184,1918.9607360000816 -120.84949,49.222156,1919.157497599956 -120.849498,49.222112,1919.9149030400213 -120.849495,49.221542,1934.7848735998778 -120.849498,49.221404,1940.2683532801288 -120.849509,49.221363,1942.4572419203596 -120.849525,49.221316,1945.388623999882 -120.849567,49.221256,1947.8835795205698 -120.849816,49.221011,1950.3273868799627 -120.850005,49.220818,1951.1546447999879 -120.850228,49.220644,1956.8357990399452 -120.850368,49.220558,1957.2665164800803 -120.850428,49.22052,1957.908377600152 -120.850469,49.220492,1958.4536940798757 -120.850474,49.220478,1959.177173119839 -120.850468,49.220464,1960.2286899198211 -120.850454,49.220457,1961.0861148798233 -120.850432,49.220454,1961.9362188802784 -120.850127,49.220506,1969.686586879961 -120.850106,49.220506,1970.5884646401257 -120.850091,49.2205,1971.534319999676 -120.850083,49.220491,1972.321046719955 -120.850081,49.220479,1972.9985897600059 -120.850136,49.220399,1975.05403135976 -120.850253,49.22032,1975.969750400081 -120.850359,49.220264,1976.0361152000696 -120.850485,49.220207,1975.2940039997134 -120.850519,49.220199,1974.8189911997936 -120.850558,49.220197,1973.86583615986 -120.850664,49.220201,1970.226426239969 -120.850693,49.220196,1969.639476479689 -120.850732,49.220181,1969.4560147204352 -120.850953,49.220082,1973.1442608001207 -120.851035,49.220045,1975.008060000098 -120.851058,49.220034,1975.4981856001496 -120.851066,49.22002,1976.2767360000862 -120.85105,49.22001,1977.044399999867 -120.85102,49.220008,1977.4759679999452 -120.850885,49.220007,1978.936235999765 -120.850341,49.21989,1992.2576000000117 -120.850287,49.219881,1993.2944000000543 -120.850252,49.219881,1993.785111040008 -120.85022,49.219887,1994.0476288000966 -120.850095,49.219928,1994.388707199949 -120.85008,49.219929,1994.544774399914 -120.850069,49.219924,1994.8548755199758 -120.850069,49.219917,1995.0943561598565 -120.850075,49.219909,1995.2890959999063 -120.850171,49.219843,1996.1093257600373 -120.850172,49.219834,1996.3783321600304 -120.850166,49.219825,1996.7542239999714 -120.850147,49.219822,1997.142177279874 -120.850126,49.219825,1997.367663999948 -120.850013,49.219856,1997.995077759938 -120.849993,49.219854,1998.3542195201026 -120.849985,49.219843,1998.8539168000427 -120.849988,49.219832,1999.202690560166 -120.849998,49.21982,1999.504537600059 -120.850069,49.219763,2000.362930239905 -120.850292,49.219588,1996.4198195200534 -120.850323,49.219571,1995.9814849599918 -120.850367,49.219557,1995.503949279968 -120.850397,49.219555,1995.2523351999907 -120.850426,49.219557,1995.0333558400187 -120.850465,49.219567,1994.757493599994 -120.850523,49.219577,1994.284983520041 -120.850606,49.21958,1993.4508416000108 -120.851482,49.219567,1988.6316390400293 -120.851523,49.219564,1988.9608115200017 -120.851561,49.219556,1989.2862745600376 -120.851718,49.21949,1991.2596640000565 -120.851759,49.219482,1991.8344976000244 -120.8518,49.219478,1992.38847999993 -120.852081,49.219459,1995.6421598399754 -120.852459,49.219396,1999.2145414400263 -120.853213,49.219306,2008.4691999999272 -120.853301,49.219299,2010.661600000045 -120.853358,49.21929,2012.0656000000047 -120.853401,49.219277,2013.102400000124 -120.853448,49.219255,2014.2075999999938 -120.853873,49.219021,2019.3555999997484 -120.854517,49.218713,2020.943183680049 -120.854644,49.218635,2021.78883519984 -120.854731,49.218572,2022.1812985600634 -120.854828,49.218519,2022.835346559966 -120.854903,49.218484,2023.6464121597824 -120.854989,49.218462,2025.0754854401162 -120.855058,49.218443,2025.5179657600072 -120.85512,49.218414,2025.6225472000401 -120.855159,49.218385,2025.54073359997 -120.855209,49.218337,2025.3132683199956 -120.855325,49.21818,2023.4276799999188 -120.855363,49.218085,2022.0785584000134 -120.855384,49.217977,2021.3738572799932 -120.855376,49.217837,2020.642187520044 -120.855379,49.217751,2020.0139281599943 -120.855403,49.217676,2019.1503731199434 -120.855486,49.217524,2017.780034559963 -120.855518,49.217425,2016.6861040000033 -120.855537,49.217322,2015.3460054400323 -120.855535,49.217227,2013.9935272000284 -120.855516,49.217191,2013.5364057599788 -120.855473,49.217156,2013.0584524800367 -120.855416,49.217124,2012.6144486399971 -120.855314,49.217085,2012.0779823999922 -120.855024,49.216999,2011.7681068799964 -120.854934,49.216951,2010.7996147199983 -120.854875,49.216903,2009.2203400001474 -120.854852,49.216851,2008.169633919878 -120.85485,49.216788,2007.3513279999977 -120.854893,49.216728,2007.2573478401064 -120.854974,49.21667,2007.6724767999876 -120.855062,49.216614,2008.446400000027 -120.855089,49.216577,2008.6408000000479 -120.85508,49.216541,2008.5760000000068 -120.854972,49.216408,2007.5029350400646 -120.854915,49.216291,2007.0669656001153 -120.854838,49.216135,2006.3789552000121 -120.854797,49.216069,2005.6152000001061 -120.854792,49.216003,2005.5072000000719 -120.854794,49.215623,2009.7022790399567 -120.85479,49.215548,2011.1119999999466 -120.85477,49.215493,2011.8139999999903 -120.854547,49.215084,2014.139199999959 -120.854226,49.214541,2013.205142079994 -120.853925,49.214077,2011.1385080001103 + + + 0 + clampToGround + -120.853925,49.214077,2011.1385080001103 -120.8537,49.213732,2003.4899840000687 -120.853665,49.213646,2003.3513984001077 -120.853678,49.213605,2003.7611024000157 -120.853719,49.213562,2004.4501308799627 -120.853818,49.213495,2005.7590736000288 -120.85385,49.213458,2006.2083679999375 -120.853858,49.213396,2006.5269132800074 -120.853855,49.213131,2007.4843999999803 -120.853837,49.213059,2007.6139999999857 -120.853807,49.212987,2007.1636000000176 -120.853598,49.212585,2003.564799999998 -120.853587,49.212534,2003.158000000002 -120.853593,49.21249,2002.86514720003 -120.853622,49.212435,2002.6012272000037 -120.853649,49.212383,2002.372948320045 -120.853657,49.212329,2001.9975028799674 -120.853647,49.211904,2000.2395884799662 -120.853674,49.211544,2000.3680652799983 -120.853688,49.21151,2000.3724543999886 -120.853704,49.211482,2000.3322726400238 -120.853751,49.211425,2000.0592039999822 -120.853786,49.211356,1999.622400000011 -120.853835,49.21121,1998.7439999999779 -120.853842,49.211036,1998.0671999999986 -120.853857,49.210989,1997.7899999999565 -120.853871,49.210955,1997.5668000000085 -120.853896,49.210929,1997.2931999999573 -120.853941,49.210877,1996.7819999999692 -120.853971,49.210838,1996.4255999999991 -120.853984,49.210787,1996.1484000000341 -120.853988,49.210735,1995.932399999991 -120.853973,49.210687,1995.8676000000269 -120.853914,49.210567,1995.8603999999682 -120.853901,49.210531,1995.6476000000816 -120.853895,49.210494,1995.2912000000094 -120.853921,49.209827,1990.3916000000127 -120.853898,49.209758,1990.8055999999906 -120.853863,49.209705,1991.217599999955 -120.853479,49.209233,1992.1432000000493 -120.853206,49.208885,1990.7787648000292 -120.853053,49.208724,1990.068799999899 -120.852859,49.208563,1990.3108000000489 -120.85249,49.208318,1992.831999999939 -120.852404,49.208241,1992.374799999942 -120.852332,49.208155,1991.6043999999174 -120.852222,49.208006,1989.932142719973 -120.852163,49.20795,1989.4406159998782 -120.852045,49.207857,1989.081242399891 -120.851688,49.207461,1985.5103654400555 -120.851407,49.207158,1982.140399999977 -120.851364,49.20711,1981.4374015999938 -120.85132,49.207079,1981.0577312000016 -120.851168,49.207021,1980.3215571199548 -120.851122,49.206995,1979.9028655999825 -120.85109,49.206963,1979.4860000000376 -120.850967,49.206786,1977.1641529600274 -120.850802,49.206522,1971.8211462399954 -120.850787,49.206491,1971.1442843199907 -120.850784,49.206453,1970.3438099200735 -120.850791,49.206422,1969.6970339200727 -120.850814,49.206386,1968.9576761600288 -120.850938,49.206303,1968.0234934399723 -120.85097,49.206264,1967.4723967999644 -120.850975,49.20622,1966.7679200000375 -120.850956,49.206185,1966.1814655999622 -120.850918,49.206144,1965.509688320026 -120.850684,49.205848,1960.1643852800592 -120.850591,49.205661,1958.476473919981 -120.850537,49.205573,1958.0001950400135 -120.850447,49.205484,1957.6092000000094 -120.850009,49.205156,1954.188000000017 -120.849904,49.205064,1952.84922623997 -120.849858,49.205006,1952.0318419199587 -120.84982,49.204942,1950.7953024000026 -120.849762,49.204832,1948.78059264004 -120.849674,49.204722,1946.6495999999038 -120.849615,49.204674,1945.5335999999588 -120.849448,49.20461,1943.4096000000009 -120.84933,49.204557,1942.2869151999987 -120.849175,49.204473,1941.2117680000028 -120.849083,49.204412,1941.1379603200194 -120.849033,49.204352,1941.1358867199663 -120.849011,49.204275,1941.0240879999733 -120.848984,49.204154,1940.8516134400088 -120.848955,49.204095,1940.573403999987 -120.848907,49.20403,1940.3604784000595 -120.848564,49.203608,1934.7389004800646 -120.848543,49.203568,1933.4751449599405 -120.84853,49.203504,1931.5106048000537 -120.848538,49.203456,1930.1226291201092 -120.84856,49.2034,1928.596160000099 -120.848775,49.203201,1923.7788000000112 -120.848834,49.203164,1922.9256000000166 -120.848907,49.203129,1922.1324388799103 -120.84899,49.203085,1920.8865840000458 -120.849027,49.20306,1920.1359552000213 -120.849026,49.203026,1919.2565619200357 -120.849003,49.202999,1918.6111222399657 -120.848802,49.20291,1916.5432000000833 -120.848719,49.202887,1915.7475999999433 -120.84866,49.202885,1915.4920000000259 -120.848341,49.202862,1912.5795683200986 -120.848271,49.20285,1911.7345119999386 -120.848196,49.202814,1910.5342604799616 -120.847628,49.202568,1907.7799961600078 -120.84744,49.202459,1907.0946368000814 -120.84737,49.202426,1906.880249599952 -120.847305,49.202412,1906.4184128000313 -120.847211,49.202378,1905.8202563199861 -120.847158,49.202361,1905.6539075199419 -120.847107,49.202331,1905.5496356799604 -120.846781,49.202117,1904.4115999999744 -120.846623,49.201999,1903.7473481599654 -120.846621,49.201996,1903.7322252799952 + + + 0 + clampToGround + -120.846621,49.201996,1903.7322252799952 -120.846525,49.20189,1902.2881199999824 -120.846418,49.201802,1899.8068243200105 -120.846375,49.201781,1898.8785799998757 -120.846323,49.201772,1898.0371462399437 -120.846058,49.201769,1894.7776761599657 -120.845986,49.20175,1893.7190399998935 -120.845929,49.201734,1892.8054348800322 -120.845865,49.20173,1892.1140159999682 -120.845806,49.201725,1891.6593360000898 -120.845787,49.20172,1891.5039744000483 -120.845776,49.201708,1891.1596876801 -120.845777,49.20169,1890.6549647999786 -120.845765,49.201429,1886.5775999999628 -120.845653,49.201192,1885.0425529599981 -120.845591,49.20107,1884.5764000000142 -120.845482,49.200959,1884.3146044799614 -120.845334,49.200863,1884.723616320033 -120.844902,49.200658,1890.084287360011 -120.844866,49.200627,1891.037126720093 -120.844843,49.200588,1892.0455846399611 -120.844844,49.200554,1892.7439449601782 -120.84486,49.20052,1893.8565120001326 -120.845185,49.200272,1899.2169472000578 -120.845237,49.20022,1900.5509343999715 -120.845283,49.200164,1901.9942995200672 -120.845299,49.200097,1904.0762788800753 -120.845321,49.200041,1905.6133665600753 -120.84535,49.199995,1906.6380000000308 -120.845755,49.199459,1915.2875768001395 -120.845943,49.199238,1917.32978463988 -120.846031,49.199142,1917.9748000000227 -120.846077,49.199084,1918.3996000001268 -120.846097,49.199009,1919.6596000000663 -120.846077,49.19879,1924.3940000000641 -120.84605,49.198721,1926.121999999922 -120.84601,49.198646,1928.1919999999395 -120.845905,49.198432,1932.6444 -120.845874,49.198365,1933.926000000064 -120.845861,49.198335,1934.4840000000108 -120.845854,49.198308,1934.9083852799813 -120.845858,49.198277,1935.1824086399104 -120.845866,49.198245,1935.4033967999849 -120.845915,49.198188,1935.2534207998967 -120.845935,49.198151,1935.3794424000089 -120.84595,49.19811,1935.6496799999727 -120.845964,49.198045,1936.4425248000575 -120.846009,49.197906,1939.9755158399819 -120.846038,49.197846,1941.2865580801677 -120.846058,49.197819,1941.7404259199345 -120.846089,49.197792,1941.984324479895 -120.846134,49.197763,1942.0667999998232 -120.846181,49.197741,1941.9335999999548 -120.846298,49.197695,1941.317999999821 -120.846479,49.197635,1940.3284000000258 -120.846507,49.197621,1940.3788000000263 -120.84652,49.197607,1940.6452000000954 -120.84652,49.197592,1941.1312000000196 -120.84651,49.197577,1941.7611999999895 -120.846485,49.19757,1942.348000000017 -120.846347,49.197558,1944.874800000075 -120.846328,49.197549,1945.5083999999333 -120.84632,49.197541,1945.911599999862 -120.846322,49.197528,1946.2968000000405 -120.846341,49.197518,1946.2788000000091 -120.846459,49.197456,1945.9807801600007 -120.846491,49.197427,1946.2030052800512 -120.84652,49.197385,1946.7914080000928 -120.846542,49.197277,1949.0545033600133 -120.846541,49.197207,1950.546391519993 -120.846521,49.197108,1952.0224332800528 -120.846485,49.197017,1953.661575199951 -120.846422,49.196915,1955.7478351999662 -120.846336,49.196746,1958.3264217601263 -120.846326,49.196699,1958.8728470398964 -120.846328,49.196673,1959.0454022400286 -120.846339,49.196646,1959.269999999971 -120.846359,49.196624,1959.3059999998802 -120.846672,49.196439,1959.84080000007 -120.846702,49.196419,1959.8408000001211 -120.846723,49.196393,1960.1000000000295 -120.846724,49.196372,1960.3818982401149 -120.846707,49.196353,1960.8443196800438 -120.846646,49.196336,1961.7341657599204 -120.846388,49.196316,1964.0622412798912 -120.846333,49.196306,1964.9594476799832 -120.846278,49.196285,1965.8947967999968 -120.846152,49.196231,1967.5986739199705 -120.845996,49.196175,1968.733488000008 -120.845935,49.19616,1969.1564160000205 -120.845873,49.19615,1969.5943920000082 -120.845822,49.196136,1969.9511999999963 -120.845777,49.19611,1970.198799999997 -120.845731,49.196074,1970.2348000000084 -120.845461,49.195893,1970.8955999999914 -120.845425,49.195871,1971.0755999999462 -120.845381,49.19586,1971.3527999999883 -120.845271,49.195859,1972.1412000000184 -120.845231,49.195852,1972.40400000002 -120.8452,49.195828,1972.5546239999596 -120.845164,49.195783,1972.7449804800183 -120.845034,49.195605,1973.0338128000087 -120.844989,49.195556,1973.120336640031 -120.844933,49.195516,1973.3357593600017 -120.844757,49.195437,1973.6507060799636 -120.844535,49.195361,1972.2278607999526 -120.844342,49.195319,1970.7794598399323 -120.844161,49.195298,1969.6566851200353 -120.844049,49.195289,1969.3807134400847 -120.843931,49.195265,1968.710052799981 -120.843422,49.195128,1970.8071999998901 -120.842338,49.194762,1968.2881715199337 -120.84218,49.194727,1968.2697856000111 -120.841979,49.1947,1968.385647999963 -120.841134,49.194623,1972.649833280019 -120.840975,49.194599,1972.9875999998892 -120.840871,49.194574,1972.9264000000437 -120.840804,49.194532,1972.1868851200102 -120.840718,49.194436,1970.0632000001178 -120.840624,49.194357,1969.3719999998677 -120.840544,49.194322,1969.5265254400788 -120.840463,49.194302,1970.2474700800137 -120.840358,49.194289,1971.402424960011 -120.840155,49.194257,1973.5025536000778 -120.840039,49.194222,1973.8573292800816 -120.839916,49.194162,1973.7319590399256 -120.83942,49.193765,1974.5192479999366 -120.839371,49.193703,1974.2933364799921 -120.839342,49.193642,1973.8789894399322 -120.83932,49.193421,1971.4321888000004 -120.839294,49.19337,1971.092291200073 -120.839255,49.193334,1971.054836800063 -120.839004,49.193088,1973.9764019199563 -120.838974,49.193052,1974.4384780801352 -120.838948,49.193009,1974.5651347199905 -120.838925,49.19294,1974.4639199999374 -120.838889,49.19282,1974.3016608000946 -120.838864,49.192782,1974.4770380799907 -120.838834,49.192739,1974.736384959916 -120.83848,49.192446,1980.0629567999054 -120.838019,49.192169,1987.1431999999593 -120.83765,49.19193,1990.276000000033 -120.837564,49.191854,1990.6575999999905 -120.83747,49.191754,1991.094755200083 -120.837067,49.1913,1992.5375839999326 -120.836941,49.191212,1992.4730963200261 -120.836794,49.191131,1992.3308214399995 -120.836158,49.190895,1990.2413263999513 -120.835657,49.190788,1987.712399999959 -120.835045,49.190674,1983.6513232000154 -120.834699,49.190639,1982.8275134399412 -120.833807,49.190624,1981.676399999966 -120.833178,49.190674,1981.3607142400497 -120.832531,49.190738,1981.0140988800772 -120.832001,49.190826,1981.2698249600617 -120.831883,49.190851,1980.7180000000974 -120.831767,49.190855,1979.5515999999727 -120.830882,49.190919,1975.6752000000113 -120.83078,49.190926,1976.1935999999303 -120.830684,49.190947,1976.99280000001 -120.829536,49.191333,1977.0556000000345 -120.828803,49.191533,1973.9379999999505 -120.828699,49.191577,1973.6644000000274 -120.828623,49.191623,1973.61399999995 -120.828034,49.19212,1969.6632000000552 -120.82798,49.192162,1969.5335999999988 -120.827953,49.19218,1969.4363999999373 -120.827926,49.192188,1969.2312000000716 -120.827895,49.192186,1968.8747999999737 -120.82787,49.192174,1968.4752000000617 -120.82776,49.192128,1966.8543999999492 -120.827723,49.192119,1966.4908000000232 -120.827689,49.192115,1966.2028000000596 -120.827656,49.192114,1965.954399999996 -120.827602,49.192117,1965.5980000000263 -120.826886,49.192262,1962.8957292800078 -120.826725,49.192306,1962.2614639999974 -120.826652,49.192313,1961.9631449599804 -120.826561,49.192313,1961.8560852799849 -120.826371,49.19231,1961.6319999999935 -120.826296,49.192314,1961.660800000023 -120.826223,49.192327,1961.7543999999914 -120.826081,49.192362,1962.0602531200154 -120.825593,49.192499,1962.999514719999 -120.825496,49.192518,1963.552293119991 -120.825432,49.192525,1963.9348320000413 -120.825349,49.192523,1964.2191700800167 -120.825254,49.192502,1964.1287999999424 -120.824886,49.192357,1963.139926080008 + + + 0 + clampToGround + -120.824886,49.192357,1963.139926080008 -120.824799,49.192323,1962.7137220800323 -120.824694,49.192294,1962.5705465600056 -120.824592,49.192273,1963.2347033600474 -120.824547,49.192262,1963.5180694400385 -120.824525,49.192251,1963.5744440000428 -120.824512,49.192236,1963.4808627200332 -120.824493,49.192176,1963.2009734399828 -120.824421,49.192096,1963.3443999999852 -120.824354,49.192029,1963.58559999999 -120.824231,49.191938,1964.2231462400316 -120.823543,49.191486,1961.3121561599544 -120.823171,49.191184,1962.1246534399784 -120.823053,49.191065,1963.1920727999773 -120.822972,49.191016,1964.1418060800736 -120.822884,49.19099,1965.0195263999829 -120.822828,49.190963,1965.5540025599869 -120.822779,49.190925,1965.9897480000247 -120.822707,49.190839,1967.2740000001065 -120.822667,49.1908,1967.8261440000922 -120.822616,49.19076,1968.4942464000453 -120.822546,49.19073,1969.4896031999692 -120.82246,49.190723,1970.7815999998932 -120.822412,49.190713,1971.400800000018 -120.822378,49.190697,1971.7751999999937 -120.822361,49.190672,1971.8399999999835 -120.822356,49.190612,1971.4800000000228 -120.82231,49.190476,1971.163199999953 -120.822272,49.190411,1971.2424000000087 -120.822212,49.190337,1971.6393542401368 -120.822086,49.190217,1973.6205014400077 -120.821937,49.190083,1975.0384078399102 -120.821867,49.190046,1975.9369692800753 -120.821841,49.190029,1976.2140785599174 -120.821828,49.190012,1976.243709440101 -120.821838,49.189983,1975.892548160004 -120.821851,49.189927,1975.5935940800782 -120.821862,49.189862,1975.3365497600648 -120.821857,49.18978,1975.3246784000853 -120.821748,49.189391,1977.4796652800103 -120.821495,49.188719,1985.5572624000197 -120.82146,49.188602,1986.264799999969 -120.821441,49.188496,1986.8516000000734 -120.82143,49.188371,1987.4203999999418 -120.821417,49.188295,1987.862331200047 -120.821387,49.188204,1988.4859999999885 -120.821315,49.188048,1989.8252000000093 -120.821191,49.187853,1991.866400000023 -120.821101,49.187731,1992.5614702400019 -120.82108,49.187712,1992.4870784000059 -120.821049,49.187696,1992.498172160016 -120.821017,49.187683,1992.5384014400117 -120.820971,49.187675,1992.712171999983 -120.820007,49.187663,1991.7048126400225 -120.81996,49.187665,1991.6080000000568 -120.819917,49.187667,1991.510799999944 -120.819874,49.187675,1991.5863999999578 -120.819823,49.187685,1991.6908000000524 -120.819413,49.187755,1991.2215176000707 -120.819365,49.18776,1991.1836959999366 -120.819322,49.187762,1991.079762559978 -120.819282,49.18776,1990.8657727999198 -120.819234,49.187754,1990.4927494399217 -120.818366,49.187549,1982.7639999998948 -120.817608,49.187415,1978.3510272000203 -120.817439,49.187387,1978.5354667200843 -120.817361,49.187366,1978.8478070398974 -120.817292,49.18734,1978.9350912000236 -120.817241,49.187319,1978.9316481600843 -120.817114,49.187222,1977.7740883199378 -120.817077,49.187184,1977.1480627200633 -120.817045,49.187143,1976.3828424000174 -120.817013,49.187073,1974.8785809599374 -120.81697,49.186979,1972.7673552000313 -120.816932,49.186923,1971.578141440023 -120.816892,49.186879,1970.7307187199576 -120.816827,49.18681,1969.3141648000224 -120.816629,49.186681,1966.632399999959 -120.816581,49.186637,1966.296399999978 -120.816535,49.186579,1966.001200000007 -120.81626,49.186248,1966.3055999999976 -120.816211,49.186102,1966.0708000000539 -120.816165,49.186034,1965.9196000000263 -120.816117,49.185979,1965.9915999999469 -120.815857,49.185652,1963.7424000000312 -120.815439,49.185291,1959.4987729600225 -120.814859,49.184823,1955.812885440084 -120.814556,49.184601,1954.5288582401074 -120.814095,49.184347,1950.3969064001499 -120.813068,49.183863,1944.6701260800785 -120.812289,49.18346,1934.9484223998122 -120.812211,49.18343,1934.560800000012 -120.812123,49.183406,1934.3303999999287 -120.811946,49.183362,1934.0208000000453 -120.811865,49.183341,1934.705999999998 -120.811786,49.183307,1935.2291260800187 -120.810754,49.182667,1936.9520000000202 -120.81068,49.182622,1936.721599999988 -120.810629,49.182599,1936.7936000000109 -120.810569,49.182584,1937.2255999999434 -120.810319,49.182556,1939.6758348798692 -120.810273,49.182547,1940.081089760058 -120.81024,49.182534,1940.261753600029 -120.810219,49.182515,1940.158973599995 -120.810201,49.182487,1939.8715999999413 -120.810137,49.182378,1938.8312000000356 -120.810074,49.182292,1938.1903999999395 -120.810009,49.182232,1938.046400000073 -120.809946,49.182195,1938.2685488000511 -120.80987,49.182165,1938.7455919999047 -120.809804,49.182143,1939.1951628800753 -120.809761,49.18212,1939.2849728000201 -120.809737,49.1821,1939.170607999986 -120.80972,49.182074,1938.834131200023 -120.809714,49.181966,1936.5862969600653 -120.809697,49.181935,1936.2179384000453 -120.80967,49.181909,1936.1696335999968 -120.809594,49.181868,1936.3645030399812 -120.809496,49.181828,1936.556610560111 -120.809274,49.181789,1937.5802294400362 -120.80918,49.181768,1937.804889599966 -120.809099,49.181742,1937.7228643199683 -120.808972,49.181697,1937.3517273600146 -120.808851,49.181646,1936.4690518400175 -120.808769,49.181619,1935.7247374399387 -120.80871,49.181605,1935.3030319999373 -120.808656,49.181601,1935.1470022400154 -120.808428,49.181589,1934.1040000000933 -120.808393,49.181585,1933.8627999999349 -120.808362,49.181579,1933.5783999999878 -120.808338,49.18157,1933.2328000000418 -120.80831,49.181559,1932.8152000000218 -120.80815,49.181464,1929.5031999999426 -120.808104,49.181443,1928.732800000046 -120.808044,49.181427,1928.0617075199664 -120.80787,49.18142,1927.3028160000686 -120.807798,49.181412,1926.8155430400718 -120.807699,49.181395,1925.9206392000642 -120.807421,49.18133,1923.5281072000648 -120.80726,49.181287,1922.6126847999226 -120.807032,49.181211,1921.1134918399027 -120.806703,49.181089,1918.5707649600022 -120.80658,49.181028,1918.1279999999829 -120.806437,49.180951,1917.7715999999873 -120.806365,49.180916,1917.4880000000612 -120.806328,49.180912,1917.4159999999874 -120.80628,49.180907,1917.3259999999586 -120.805543,49.180906,1916.341375680004 -120.805004,49.180921,1917.2623999999696 -120.804955,49.180921,1917.4243999999453 -120.804917,49.180919,1917.5324000000307 -120.804874,49.180907,1917.5143999999739 -120.804838,49.180889,1917.3847999999941 -120.804409,49.180677,1914.7488000000412 -120.804346,49.180652,1914.3888000000293 -120.804275,49.180632,1914.1008000000402 -120.804066,49.180606,1913.6605881599696 -120.804005,49.180593,1913.4607464000237 -120.80395,49.180577,1913.2485839999947 -120.803871,49.180555,1913.126671199987 + + + 0 + clampToGround + -120.803871,49.180555,1913.126671199987 -120.803581,49.180472,1914.3984000000125 -120.803547,49.180458,1914.2976000000112 -120.803514,49.18044,1914.1679999999803 -120.803442,49.180395,1913.8439999999798 -120.803399,49.180377,1913.7144 -120.803352,49.18037,1913.664000000025 -120.802503,49.180358,1910.1705619199827 -120.80242,49.180353,1909.8731903999428 -120.80234,49.18034,1909.44902400002 -120.802267,49.180319,1908.8792779199534 -120.802198,49.180285,1908.2149327999466 -120.801755,49.180048,1906.2553503999918 -120.801413,49.179772,1905.3640000000225 -120.801204,49.179624,1906.2895999999942 -120.801139,49.179586,1906.7179999999357 -120.801057,49.179559,1907.6039999999807 -120.800896,49.179526,1909.684800000099 -120.800829,49.179508,1910.5079692800687 -120.8008,49.179492,1910.722944000079 -120.800781,49.179465,1910.7271415999965 -120.800765,49.179419,1910.5773336000361 -120.80071,49.179204,1909.9851264000597 -120.800569,49.17899,1910.5651824000422 -120.80051,49.178868,1910.2527999999852 -120.800329,49.17833,1909.4325871999572 -120.8003,49.178277,1909.540976000087 -120.800256,49.178238,1909.8674137599573 -120.800096,49.178158,1911.503714560039 -120.80004,49.178121,1912.2562528000622 -120.799996,49.178076,1912.9685401600834 -120.799565,49.177529,1915.7433095999872 -120.799219,49.177166,1918.0160000000424 -120.799162,49.177124,1917.9787404800293 -120.799099,49.177093,1918.2049227200087 -120.79902,49.177066,1918.665747200043 -120.798886,49.177041,1919.7399699200812 -120.798763,49.177017,1921.5516483200956 -120.798672,49.176989,1922.9743193600611 -120.798588,49.176952,1924.4969369601033 -120.798509,49.176893,1925.6990409600849 -120.798267,49.176613,1926.3750638401252 -120.798226,49.176548,1926.0581299199555 -120.798189,49.176501,1925.9025105601306 -120.798157,49.176466,1925.8258204799117 -120.798105,49.176424,1925.8975007998524 -120.797934,49.176296,1927.664133120047 -120.797894,49.176256,1927.9763251200352 -120.797872,49.176219,1927.9333094400706 -120.797819,49.17611,1927.3989935999505 -120.797792,49.176063,1927.4576678399783 -120.797756,49.176024,1927.7402137598717 -120.797368,49.175803,1931.734291839924 -120.797294,49.175768,1932.670424320086 -120.797226,49.175752,1933.6488179200892 -120.797191,49.17574,1934.450800000042 -120.797145,49.175709,1935.4983999999806 -120.797132,49.175672,1935.6927999998734 -120.797162,49.175558,1934.5264000000047 -120.797159,49.175522,1934.4449059201645 -120.797143,49.175486,1934.6745859198638 -120.796782,49.174751,1942.8809852801237 -120.796748,49.174704,1943.7712153601292 -120.796735,49.17466,1944.5522079999928 -120.796751,49.174609,1944.9522947200367 -120.796781,49.17456,1945.1747488000642 -120.796802,49.174508,1945.6290572799626 -120.796797,49.174332,1948.0216678400284 -120.796789,49.17425,1949.1705199999385 -120.796756,49.174199,1950.2854342399512 -120.796727,49.174151,1951.1753499199335 -120.796711,49.174088,1951.5996012799774 -120.796695,49.173825,1953.68691999999 -120.796705,49.173748,1954.9859871998951 -120.796738,49.17368,1955.862227199991 -120.796788,49.173603,1956.82284543993 -120.796958,49.173375,1958.3438799999842 -120.797022,49.173264,1959.3999116799762 -120.797065,49.173159,1960.1764215999956 -120.797076,49.173092,1960.7213363200003 -120.79707,49.17301,1962.2597440000116 -120.797027,49.172919,1965.0461929599937 -120.796958,49.172765,1970.133534399994 -120.796939,49.172724,1971.4260108800104 -120.796928,49.172671,1972.882087040041 -120.796921,49.172459,1977.9067999999552 -120.79692,49.172422,1978.580000000046 -120.796904,49.172378,1979.487199999981 -120.796866,49.172329,1980.642800000082 -120.796857,49.172305,1981.1395999999531 -120.796855,49.172293,1981.3699999999853 -120.796857,49.172279,1981.6075999999225 -120.796868,49.172261,1981.8523999999952 -120.797043,49.172166,1982.585388480038 -120.797078,49.172147,1982.7429993599637 -120.797094,49.172128,1982.9749747199821 -120.797099,49.172103,1983.3507931199867 -120.797089,49.172077,1983.8215348799758 -120.797049,49.171998,1985.355769919957 -120.79703,49.171831,1988.4259999999995 -120.797015,49.171777,1989.5060000000099 -120.796992,49.171727,1990.5716000000307 -120.796895,49.171597,1993.108399999971 -120.796814,49.171511,1994.6203999999907 -120.796763,49.171459,1995.5492000000245 -120.796732,49.171436,1996.0207999999593 -120.796688,49.171424,1996.4671999999578 -120.79663,49.171428,1997.068428800104 -120.79659,49.171427,1997.6166656000528 -120.796562,49.171416,1998.140848640067 -120.796369,49.171206,2002.6748000000407 -120.796333,49.171178,2003.524399999942 -120.796276,49.171146,2004.7807999998906 -120.79591,49.170957,2013.347675199891 -120.795751,49.170846,2017.6291158401013 -120.795697,49.1708,2019.4935039999625 -120.795666,49.170777,2020.510233279997 -120.795631,49.170764,2021.2497913600469 -120.795598,49.170758,2021.7526393600378 -120.795552,49.170758,2022.2173286400107 -120.795507,49.170762,2022.7087113598955 -120.795305,49.170829,2023.7187288000732 -120.795268,49.170836,2024.0931379200777 -120.795243,49.170836,2024.4540019199876 -120.795219,49.170831,2024.9016451199632 -120.795203,49.170821,2025.3636950400692 -120.795189,49.170804,2025.9501004801612 -120.795173,49.170767,2027.000249279978 -120.795126,49.170693,2029.169917440041 -120.795081,49.170637,2030.7798057600507 -120.795014,49.170568,2032.6706841599853 -120.794928,49.170502,2032.6908262399936 -120.794856,49.170453,2032.290206719983 -120.794731,49.17039,2031.4544335999844 -120.794634,49.170364,2031.2885491199957 -120.794534,49.170346,2031.0672556800218 -120.794443,49.170345,2030.9705167999623 -120.794027,49.17033,2032.2281263999985 -120.79394,49.170318,2032.618956799994 -120.793844,49.170295,2032.6056192000494 -120.793318,49.170079,2028.8199011200506 -120.792846,49.169832,2028.5744000000243 -120.79272,49.169769,2028.0016544000541 -120.792551,49.169715,2027.3987999998644 -120.792064,49.169568,2025.763274239947 -120.791963,49.169539,2025.0165838400303 -120.791852,49.169512,2024.9167999998272 -120.791655,49.169488,2026.0430143999715 -120.791095,49.16945,2031.3359999999177 -120.790925,49.169457,2032.8372000000413 -120.790843,49.169452,2033.2296000000224 -120.79077,49.169438,2032.9728 -120.79041,49.169364,2030.9661504000671 -120.790319,49.169359,2030.878352160005 -120.790225,49.169364,2031.0489760000664 -120.790102,49.169384,2031.6863027200368 -120.790016,49.169398,2032.1704307200378 -120.78992,49.169404,2032.1523327999892 -120.789575,49.169402,2031.7149519999423 -120.788405,49.169432,2033.719641600011 -120.788291,49.169475,2034.8888719999609 -120.788208,49.169499,2035.6487686400462 -120.788167,49.16951,2035.9681664000493 -120.788121,49.169515,2036.1540847999336 -120.788101,49.169514,2036.170330879995 -120.788083,49.169508,2036.0904108800075 -120.788061,49.169488,2035.770546560024 -120.788021,49.169407,2034.3491382399902 -120.788006,49.169391,2034.0926483199685 -120.787982,49.16938,2033.9956671999387 -120.787944,49.169374,2034.0836915199786 -120.787901,49.169377,2034.3697878399241 -120.787509,49.169492,2038.4543468799802 -120.787357,49.169551,2040.4977227199854 -120.787211,49.169621,2042.4180782400056 -120.787048,49.169725,2045.566256000013 -120.786997,49.169749,2046.7616777600656 -120.786972,49.169754,2047.1978969598752 -120.786954,49.169749,2047.3510323201324 -120.786943,49.169733,2047.2148000001048 -120.786939,49.169717,2047.0223684799291 -120.786934,49.169627,2046.134889279948 -120.78692,49.169593,2045.9973376000705 -120.786902,49.169567,2046.0201846400437 -120.786869,49.169538,2046.2769251200982 -120.78682,49.169507,2046.8147103999268 -120.786706,49.169453,2048.3579612800636 + + + 0 + clampToGround + -120.786706,49.169453,2048.3579612800636 -120.786607,49.169407,2049.724399999947 -120.786417,49.169341,2052.222800000008 -120.786358,49.169325,2052.9514159999158 -120.786281,49.169311,2053.8713873599704 -120.786237,49.169304,2054.4112620799965 -120.786215,49.169299,2054.6821736000215 -120.786199,49.169289,2054.873742560046 -120.78619,49.169276,2054.9757823999407 -120.786186,49.169261,2055.013156159989 -120.786223,49.169179,2054.406684319903 -120.786239,49.169132,2054.408000000092 -120.786237,49.169082,2054.79679999998 -120.786168,49.168619,2058.7260326399264 -120.786167,49.168593,2058.910081759859 -120.786156,49.168563,2059.348810879887 -120.78613,49.168537,2060.107457599996 -120.786093,49.168516,2061.076000000154 -120.785894,49.168438,2065.936000000009 -120.785869,49.16842,2066.605599999904 -120.785854,49.168395,2067.1096000000125 -120.785848,49.168371,2067.4119999999657 -120.785852,49.168333,2067.598080639892 -120.785937,49.167948,2067.6657030399188 -120.785988,49.167634,2067.7140006399413 -120.785979,49.167568,2067.8655462400493 -120.785956,49.167495,2068.324451200039 -120.785638,49.166884,2074.1399999998416 -120.785505,49.166181,2079.7563824000094 -120.785456,49.166067,2079.6332000000584 -120.785383,49.165936,2080.4756000000807 -120.78503,49.165426,2082.675999999934 -120.784573,49.164768,2089.1163916801274 -120.784389,49.164575,2091.892211999928 -120.784178,49.16437,2094.3008000000273 -120.783879,49.164122,2095.3441244799988 -120.78364,49.163992,2097.0034047999147 -120.783373,49.163868,2098.26006144004 -120.783107,49.163771,2099.8699977599267 -120.782782,49.163665,2101.9121488000665 -120.782501,49.163598,2104.74943008006 -120.782342,49.163568,2106.979722239961 -120.78224,49.163561,2108.609145600002 -120.782168,49.163559,2109.691819520007 -120.782109,49.163549,2110.3799393599825 -120.78209,49.16354,2110.489856000004 -120.78207,49.163527,2110.5411343999695 -120.782031,49.163477,2110.18915951995 -120.781921,49.163303,2108.6576000000314 -120.781898,49.163268,2108.189600000087 -120.781882,49.163245,2107.8980000001584 -120.781844,49.163223,2108.027599999934 -120.781383,49.163025,2111.225999999834 -120.781324,49.162993,2111.3664000000426 -120.781269,49.162955,2111.261999999999 -120.781222,49.162922,2111.1576000000578 -120.781158,49.162874,2110.9271999999746 -120.781077,49.162836,2111.0452000000014 -120.780676,49.162696,2112.038051839918 -120.780124,49.162403,2113.1889657599345 -120.779747,49.162228,2115.718544639962 -120.779656,49.162216,2117.0522598399684 -120.779635,49.162211,2117.314914400001 -120.779615,49.162203,2117.488088799835 -120.7796,49.162193,2117.5225120000105 -120.77959,49.162179,2117.3723344000628 -120.779565,49.162133,2116.721000799847 -120.779546,49.162112,2116.539470080073 -120.779514,49.162089,2116.5108918399433 -120.779047,49.1619,2119.2832000000108 -120.778947,49.161844,2118.908800000035 -120.778831,49.161763,2118.299326880084 -120.778374,49.161412,2117.992439039965 -120.778244,49.161333,2117.868988159957 -120.778092,49.161264,2117.965975040083 -120.777308,49.161045,2120.6962943999774 -120.777235,49.161025,2121.04026000001 -120.77719,49.161003,2120.890347199974 -120.77715,49.160965,2120.1827599998405 -120.777113,49.160925,2119.413043999966 -120.776857,49.160572,2114.2995999999684 -120.776739,49.160439,2112.3908638398607 -120.776667,49.160388,2111.7747238399093 -120.776586,49.160352,2111.4512000001178 -120.776331,49.16029,2111.534739200094 -120.776286,49.160281,2111.460193279939 -120.776246,49.160263,2111.116166080011 -120.776065,49.160145,2109.231347999966 -120.775865,49.159974,2107.8781296000147 -120.77585,49.159939,2107.209623999952 -120.775841,49.159897,2106.2837659200245 -120.775836,49.159844,2105.025008640021 -120.775816,49.159777,2103.6299999998696 -120.775716,49.159686,2102.9622809599728 -120.775692,49.159642,2102.4493414398617 -120.775682,49.159567,2101.130834239998 -120.775653,49.159488,2100.0850054399657 -120.775465,49.15926,2097.6560000001264 -120.775363,49.159172,2096.9071999999705 -120.77524,49.1591,2096.5926400000503 -120.775204,49.159085,2096.3140863999456 -120.775159,49.159069,2096.040744160044 -120.775054,49.15906,2096.005350399941 -120.77501,49.159052,2095.9211392000557 -120.774994,49.15904,2095.6415488000403 -120.774997,49.159023,2095.376757279947 -120.775035,49.159005,2095.016668000004 -120.775119,49.158996,2094.6647910400293 -120.77515,49.158982,2094.317008000043 -120.775188,49.158957,2093.7151513600184 -120.775217,49.15893,2093.0744175999653 -120.775234,49.158886,2092.1088780800515 -120.775236,49.158841,2091.340257920009 -120.775228,49.158802,2090.727307520033 -120.775194,49.158762,2090.2931417600535 -120.775149,49.158737,2090.2365929600387 -120.775079,49.158718,2090.520074239963 -120.77486,49.158725,2091.1140799999866 -120.774699,49.158723,2090.8653036799874 -120.774662,49.15872,2090.6600576000287 -120.774624,49.158708,2090.506657280029 -120.774555,49.158671,2090.4808352000355 -120.774526,49.158637,2090.736654080005 -120.774519,49.158593,2091.4389003199203 -120.774533,49.158553,2092.3977070400506 -120.77453,49.158526,2093.0504287999547 -120.774513,49.158504,2093.604033919986 -120.774483,49.15849,2093.991483199983 -120.774458,49.158474,2094.4311123200523 -120.774446,49.158458,2094.855313279927 -120.774441,49.158438,2095.3561107200753 -120.77445,49.158404,2096.2042880000627 -120.774552,49.158285,2098.8700544000176 -120.774628,49.158227,2099.8956915200383 -120.774709,49.158188,2100.236608639931 -120.77475,49.158167,2100.292800000053 -120.774786,49.158135,2100.595199999904 -120.774792,49.158099,2101.3079999999454 -120.774783,49.158065,2102.139599999995 -120.774756,49.158034,2103.091363840025 -120.77471,49.157995,2104.44959200002 -120.774592,49.157939,2107.1207084800444 -120.774522,49.157925,2108.3211759999726 -120.77446,49.157921,2109.1956735998956 -120.774309,49.157908,2110.663173760004 -120.774112,49.157847,2113.048885120041 -120.773971,49.1578,2114.4171039999646 -120.773902,49.157766,2115.1223999999424 -120.773856,49.157736,2116.075200000074 -120.773824,49.157726,2116.6799999999794 -120.773801,49.157728,2116.982399999932 -120.773751,49.157747,2117.4287999999306 -120.773718,49.157756,2117.774399999979 -120.77369,49.157752,2118.235199999941 -120.773655,49.157737,2118.955199999965 -120.773421,49.157643,2123.6986582400123 -120.773242,49.157592,2126.652800000047 -120.773129,49.157555,2128.546400000024 -120.773099,49.157544,2129.057599999961 -120.773081,49.157529,2129.4247999999548 -120.773072,49.157504,2129.7343999999916 -120.773091,49.157394,2129.635676480069 -120.773095,49.157359,2129.6482376000195 -120.773078,49.157331,2129.824276160015 -120.773043,49.15731,2130.0594831999942 -120.772779,49.15722,2130.979635200017 -120.772721,49.157189,2130.7608000000178 -120.772671,49.157144,2130.4368000000172 -120.772445,49.156918,2128.8171999999463 -120.772039,49.156554,2124.493533759964 -120.771955,49.156489,2123.7396951999776 -120.771894,49.156447,2123.2275999999883 -120.771741,49.156373,2122.4617067200156 -120.771709,49.156351,2122.18638736001 -120.771685,49.156322,2121.7458928000387 -120.771357,49.155927,2116.5396894400087 -120.77134,49.155901,2116.2835264000323 -120.771329,49.155875,2115.9988600000615 -120.771324,49.155843,2115.5982707199137 -120.771261,49.155698,2114.2348931200368 -120.771249,49.155667,2113.909244320027 -120.771231,49.155644,2113.704578560029 -120.771194,49.155618,2113.533719679985 -120.770667,49.155337,2105.7659358400815 -120.770578,49.155286,2104.2607916799293 -120.770543,49.155259,2103.6168000000293 -120.770524,49.155226,2102.9543999999623 -120.770502,49.15517,2101.8671999999447 -120.770475,49.155115,2100.7800000000548 -120.770452,49.155087,2100.193200000053 -120.770423,49.155061,2099.6208000000397 -120.770173,49.154871,2096.31757168 -120.769786,49.154647,2093.4392972799706 + + + 0 + clampToGround + -120.769786,49.154647,2093.4392972799706 -120.769428,49.15444,2090.850547200042 -120.769055,49.154139,2086.5157175999534 -120.768976,49.154052,2085.1396377599685 -120.768933,49.154001,2084.5958750400227 -120.768912,49.153971,2084.369381759994 -120.768885,49.153942,2084.133139199963 -120.768858,49.153921,2083.704871679923 -120.768809,49.153901,2083.1055638400594 -120.76875,49.153881,2082.585199999988 -120.768694,49.153873,2082.1359891200077 -120.768477,49.153876,2078.9316057601613 -120.76843,49.153873,2078.1950831999925 -120.768391,49.153865,2077.7584391998007 -120.768349,49.153853,2077.422939360073 -120.768297,49.153827,2077.0753689601124 -120.768256,49.153803,2076.899925119724 -120.768094,49.153672,2078.8253491200753 -120.768011,49.153607,2079.9879217600082 -120.767982,49.153581,2079.99131296001 -120.767965,49.153558,2080.2433936001808 -120.767956,49.153534,2080.732875519943 -120.76794,49.153414,2083.9407807999883 -120.767936,49.15339,2084.586995200048 -120.767921,49.153357,2085.342187360026 -120.767903,49.153334,2085.7824457600277 -120.767637,49.153091,2084.1901292797725 -120.767314,49.152807,2086.417432319907 -120.76728,49.152757,2087.6256831999754 -120.767266,49.152728,2088.1763161599474 -120.767254,49.152692,2088.896546559954 -120.767234,49.152614,2090.530001920003 -120.767187,49.152422,2093.09841088003 -120.767158,49.15234,2093.379942399972 -120.767142,49.152298,2093.5696307200033 -120.767126,49.152266,2093.7332147200214 -120.7671,49.152235,2093.913520000017 -120.766791,49.152029,2094.1911483200033 -120.766691,49.15195,2094.9252560000073 -120.766622,49.151897,2095.2082646400086 -120.766594,49.151864,2095.3933593599672 -120.766583,49.151832,2095.6303257600034 -120.766567,49.151781,2096.029117919995 -120.766553,49.151752,2096.2598937599855 -120.766529,49.151728,2096.4489715200175 -120.7665,49.151709,2096.603760000027 -120.766467,49.151699,2096.6835316799957 -120.766379,49.151687,2096.742194079974 -120.766316,49.151675,2096.6397280000438 -120.766275,49.151658,2096.5275999999585 -120.766238,49.151634,2096.2216000000144 -120.765875,49.151363,2093.799620000029 -120.765502,49.15098,2091.3047999999644 -120.76513,49.150636,2088.554867199987 -120.764849,49.1504,2084.8791839999476 -120.764802,49.150349,2083.721161919961 -120.76477,49.150297,2082.472497600047 -120.764635,49.150001,2076.335600000074 -120.764577,49.149902,2075.296043839953 -120.764545,49.149863,2074.9798616000508 -120.764506,49.149837,2074.8879651199927 -120.764406,49.149787,2074.902549760073 -120.764112,49.149645,2074.2004000000225 -120.764067,49.149627,2074.362400000074 -120.764042,49.149608,2074.2903999999744 -120.764,49.149566,2073.988000000047 -120.76398,49.149546,2073.8439999999755 -120.76395,49.149534,2073.952000000112 -120.763804,49.149531,2075.47480000013 -120.763765,49.149524,2075.769999999922 -120.763736,49.149517,2075.9572000001144 -120.763717,49.1495,2075.8564000000615 -120.76369,49.149468,2075.572000000013 -120.763553,49.149181,2069.9887999999482 -120.763535,49.149145,2069.219371999872 -120.763517,49.149123,2068.8120593601607 -120.763487,49.149099,2068.500440480016 -120.763047,49.148728,2064.5685606399643 -120.762879,49.148604,2062.4982713599848 -120.762733,49.148522,2061.5572000000066 -120.762654,49.14849,2061.2656000000525 -120.762578,49.148469,2061.1611999999577 -120.762543,49.148458,2061.0891999999862 -120.762517,49.148442,2060.894800000042 -120.762495,49.148416,2060.536643199942 -120.762464,49.148367,2059.979092480066 -120.762434,49.148337,2059.775663679976 -120.762384,49.148307,2059.7392115200796 -120.762324,49.148284,2059.900272639866 -120.762289,49.148274,2060.048549440059 -120.762268,49.148265,2060.070140799985 -120.76221,49.148219,2059.636510399979 -120.762193,49.1482,2059.3808960000915 -120.762172,49.148165,2058.835164799911 -120.762139,49.148118,2058.180249919883 -120.761442,49.147389,2057.265992959938 -120.761424,49.14737,2057.224889599971 -120.761406,49.147344,2057.1228108800337 -120.761398,49.147322,2056.9699635200313 -120.761394,49.14729,2056.6673792000342 -120.761389,49.147021,2053.1010897601027 -120.761375,49.146975,2052.628999999953 -120.761347,49.146922,2052.5224000000035 -120.76126,49.146815,2053.3900000000895 -120.761164,49.146683,2054.297200000101 -120.760971,49.146419,2055.2890769600467 -120.760834,49.146227,2055.8234012799794 -120.760797,49.146145,2055.8820424000396 -120.760783,49.146086,2055.9059999999363 -120.760783,49.146024,2055.9059999999363 -120.760801,49.145969,2055.5819999999867 -120.760839,49.145921,2054.9119617599335 -120.760983,49.145763,2052.961575840058 -120.760999,49.145725,2052.77180400003 -120.761006,49.145681,2052.7211145600813 -120.76101,49.145572,2052.7984512000185 -120.761211,49.144894,2051.462023359955 -120.761223,49.144806,2052.066915519964 -120.761231,49.144736,2052.5845926400384 -120.76124,49.144683,2052.900566399986 -120.761255,49.14464,2053.02734399991 -120.761282,49.144608,2052.8820595200914 -120.761468,49.14444,2051.2004864000323 -120.76151,49.144385,2051.361391999987 -120.761544,49.144326,2051.554196479996 -120.76158,49.144194,2052.301798400097 -120.761603,49.144027,2052.5335582400967 -120.761606,49.14391,2052.8141983999985 -120.761608,49.143838,2052.5170918400845 -120.76159,49.143775,2052.397160000016 -120.761553,49.143694,2052.5206947199126 -120.761483,49.143566,2053.0362211200277 -120.761465,49.143511,2053.0079495999594 -120.761467,49.143445,2052.5661576000607 -120.761486,49.14317,2051.601599999928 -120.761464,49.142899,2052.0707974399543 -120.761449,49.142723,2052.3508000000115 -120.761467,49.142646,2052.1564000000417 -120.761491,49.14259,2051.897199999929 -120.761648,49.142289,2049.442000000059 -120.761849,49.141904,2046.8283155200227 -120.76212,49.141665,2047.3733760000039 -120.762303,49.141521,2047.772191040015 -120.762491,49.141364,2048.8208000000213 -120.763119,49.140933,2044.635650079976 -120.763692,49.140447,2042.0410009599975 -120.763708,49.140401,2041.7324723199922 -120.763697,49.14036,2041.500476800014 -120.763273,49.139613,2030.7791999999654 -120.763215,49.139444,2029.142518399986 -120.763137,49.139246,2025.9382620800036 -120.7631,49.138812,2017.9802879999795 -120.763129,49.13848,2012.5579968001014 -120.763135,49.138448,2012.089939200016 -120.763146,49.13841,2011.592534400088 -120.76328,49.138084,2006.8463999999055 -120.763307,49.138034,2006.0923129599055 -120.763355,49.138,2005.8339999999985 -120.763803,49.137736,2003.5365926398995 -120.763873,49.137696,2003.3799206400035 -120.763894,49.13768,2003.264876800024 -120.76391,49.137662,2003.0236767999572 -120.763922,49.13762,2002.3469055999349 -120.763932,49.137522,2000.6941081599273 -120.763984,49.137157,1993.2759999999962 -120.764026,49.137066,1991.6127999999492 -120.76413,49.136961,1990.0935999999479 -120.764212,49.136818,1987.5951999999631 -120.764401,49.136638,1985.1002809599977 -120.764475,49.136569,1983.9617240000432 -120.764574,49.136502,1982.633118079997 -120.76509,49.136241,1976.7617951999546 -120.765372,49.1361,1973.7071359999734 + + + 0 + clampToGround + -120.765372,49.1361,1973.7071359999734 -120.765514,49.136029,1972.1379564800056 -120.765542,49.135999,1971.53900863995 -120.76556,49.13597,1970.99212799995 -120.765561,49.13593,1970.1399792000907 -120.765544,49.1359,1969.3767680000187 -120.765502,49.135869,1968.434311039977 -120.765448,49.135843,1967.461349120075 -120.765207,49.135761,1964.6316500800458 -120.76509,49.135723,1963.7454927999745 -120.764511,49.135497,1958.996507680014 -120.76448,49.13549,1958.8502079999564 -120.764449,49.135484,1958.7162246399662 -120.764349,49.13547,1958.3541711999565 -120.764321,49.135465,1958.2251256000632 -120.76428,49.135454,1957.9555648000567 -120.763661,49.135253,1950.6455999999566 -120.763603,49.135235,1949.7096000000688 -120.763535,49.135222,1948.8455999999992 -120.763372,49.135201,1947.0672000000895 -120.763315,49.135193,1946.3805432000845 -120.763249,49.135178,1945.3246531201103 -120.762316,49.134932,1933.1237555199712 -120.761314,49.13467,1925.816284800001 -120.761241,49.13465,1925.590824000024 -120.760911,49.13458,1924.064444799981 -120.760867,49.134569,1923.7447460799865 -120.760819,49.134553,1923.3246347199288 -120.760784,49.134538,1922.9609843199835 -120.760743,49.134516,1922.461828479961 -120.760513,49.134365,1918.800984800079 -120.760484,49.134354,1918.3793254400737 -120.760439,49.134337,1917.7104987200833 -120.760343,49.134295,1916.057122400078 -120.760216,49.134253,1914.26400000003 -120.760178,49.134238,1913.6952000000847 -120.760084,49.134202,1912.320000000078 -120.760059,49.134161,1911.0083999999529 -120.760033,49.134125,1909.6187999999313 -120.759978,49.134091,1908.1967999999404 -120.75992,49.134048,1906.4399999999762 -120.759841,49.133998,1904.3555999999182 -120.759824,49.133981,1903.6823999999297 -120.75981,49.133959,1902.8399999999078 -120.759721,49.133842,1898.47640000004 -120.759695,49.13382,1897.6699999999785 -120.759647,49.133792,1896.5899999999938 -120.759527,49.133733,1894.2464000000018 -120.759478,49.133705,1893.162799999975 -120.759313,49.133607,1889.8745966399335 -120.759187,49.133524,1887.200555520039 -120.759169,49.133503,1886.5004692799187 -120.759159,49.133478,1885.672825919886 -120.759173,49.133338,1881.1736169600777 -120.759183,49.133314,1880.5675075199413 -120.7592,49.133292,1880.0605439999522 -120.759396,49.133131,1876.125432959961 -120.759415,49.133107,1875.4619688000746 -120.759428,49.133073,1874.4986822400888 -120.75943,49.133037,1873.535873600042 -120.759412,49.132849,1868.881652480012 -120.759419,49.132812,1867.94271488005 -120.759438,49.132771,1866.900633919957 -120.759461,49.132733,1866.1344000000256 -120.759566,49.1326,1863.3623999999131 -120.759588,49.132562,1862.5992000000238 -120.759593,49.132517,1861.771200000017 -120.759584,49.132472,1860.8932838399874 -120.759569,49.132447,1860.3871094399822 -120.759534,49.132418,1859.8015449600257 -120.75948,49.132378,1858.910035200069 -120.759168,49.132228,1854.1711001599454 -120.759122,49.132209,1853.5612540800828 -120.759065,49.132195,1853.0818680000818 -120.758658,49.13212,1850.6346816 -120.758613,49.132111,1850.40247728002 -120.758559,49.132097,1849.9061700800528 -120.758164,49.131962,1846.5799999999929 -120.757684,49.13185,1841.9623999999917 -120.757638,49.131841,1841.6348000000257 -120.757603,49.131831,1841.3287999999798 -120.757569,49.131813,1840.8824000000327 -120.75754,49.131786,1840.2919999999885 -120.757392,49.131676,1838.1549363199795 -120.757347,49.131644,1837.7552000000155 -120.757289,49.131619,1837.4852000000064 -120.757045,49.131543,1836.9484375999973 -120.756953,49.1315,1836.7815200000282 -120.756918,49.131469,1836.5090556800176 -120.756898,49.131443,1836.2549705599986 -120.756887,49.131422,1836.056250560024 -120.756877,49.131371,1835.4508363200348 -120.756879,49.131339,1834.908713759942 -120.756903,49.131268,1833.7397318399637 -120.756948,49.131167,1832.003424639965 -120.756969,49.13103,1830.390187200022 -120.75696,49.130992,1830.1183871999833 -120.756942,49.130964,1829.940799999991 -120.756913,49.130931,1829.703199999977 -120.756766,49.130793,1828.5124764799648 -120.756718,49.130748,1828.0216294399581 -120.756635,49.130665,1827.1820000000052 -120.756461,49.130412,1822.8991999999946 -120.756447,49.130364,1821.8624000000032 -120.756438,49.130312,1820.7392000000764 -120.756444,49.130229,1818.9812390399998 -120.756455,49.130192,1818.220694400023 -120.756469,49.130144,1817.2492934400207 -120.756507,49.130092,1816.2715737599508 -120.756513,49.130056,1815.5663251200604 -120.756503,49.130019,1814.7931012799274 -120.756475,49.129962,1813.352399999956 -120.756271,49.12968,1805.6636000000033 -120.756242,49.129653,1804.9759999999487 -120.756216,49.129627,1804.3207999999659 -120.756149,49.129573,1802.9131999999897 -120.756118,49.129535,1801.9807999999393 -120.756102,49.129492,1800.9887846399733 -120.756058,49.12932,1798.0167999999835 -120.755918,49.129086,1795.014399999982 -120.755832,49.12902,1794.4742655999926 -120.755788,49.128992,1794.431779839992 -120.755761,49.128988,1794.4497107199898 -120.755739,49.128986,1794.4683241599814 -120.755636,49.128985,1794.5917984000064 -120.755605,49.128981,1794.6041752000172 -120.755573,49.128972,1794.5796102399838 -120.755526,49.128956,1794.5638937600067 -120.755289,49.128893,1794.9749979199753 -120.755253,49.128883,1795.1977089600211 -120.755206,49.12888,1795.5405311999857 -120.754736,49.128873,1799.7968371200582 -120.753829,49.128872,1806.1679084800185 -120.753789,49.128871,1806.3183582400013 -120.753747,49.128867,1806.4722510399831 -120.753709,49.128858,1806.6084131199989 -120.753647,49.128838,1806.8471305599971 -120.753527,49.128762,1807.467280959972 -120.753382,49.128627,1807.8719785599972 -120.753301,49.128573,1808.116400000024 -120.753177,49.128514,1808.5628000000224 -120.753002,49.128447,1809.3067062400273 -120.752872,49.128404,1810.1534924800146 -120.752755,49.128372,1811.0362144000546 -120.752688,49.128354,1811.5960460799495 -120.752618,49.128334,1812.149380480009 -120.752519,49.128319,1812.9628705599678 -120.752437,49.128309,1813.6287999999854 -120.752408,49.128301,1813.8951999999779 -120.75237,49.128286,1814.2767999999865 -120.752347,49.128264,1814.600799999987 -120.75233,49.128236,1814.9247999999875 -120.75231,49.128186,1815.4288000000452 -120.752277,49.128119,1816.1487999999667 -120.752232,49.128053,1816.9299238399535 -120.752152,49.128004,1817.55251968004 -120.752055,49.127947,1818.4392616000146 -120.751833,49.127882,1820.4271999999664 -120.751674,49.127853,1821.6764000000355 -120.751423,49.127834,1822.4972547199925 -120.751164,49.127831,1822.6167999999966 -120.750932,49.127839,1823.7066860799468 -120.750381,49.127884,1824.852799999982 -120.750258,49.127891,1824.735021120024 -120.750158,49.127879,1824.632729279997 -120.750057,49.127856,1824.584215679985 -120.749953,49.127815,1824.709327200016 -120.749547,49.12756,1827.9997471999627 -120.749209,49.127302,1830.1559999999486 -120.7492,49.127282,1830.379199999999 -120.749194,49.127262,1830.6239999999948 -120.749197,49.12724,1830.962399999959 -120.749208,49.127218,1831.3431999999793 -120.749244,49.127168,1832.142400000059 -120.749288,49.127106,1833.1288000000754 -120.749302,49.126997,1834.4067999999695 -120.749286,49.126899,1835.4433174399649 -120.749267,49.126781,1836.8634699200015 -120.749241,49.126733,1837.3763028799697 -120.749204,49.126688,1837.8375219200027 -120.749085,49.126586,1838.577199999943 -120.749061,49.12655,1838.8795999999725 -120.749039,49.126502,1839.3187999999636 -120.748984,49.126365,1840.4849535999779 -120.748962,49.126258,1841.0815801600113 -120.748957,49.12619,1841.5016367999972 -120.748973,49.126156,1841.7257324799934 -120.749009,49.126134,1841.870829759998 -120.749083,49.126106,1841.99445792 -120.749204,49.126081,1841.894737919984 -120.749365,49.126078,1841.4562176000088 -120.749661,49.126044,1840.8919513599885 -120.749743,49.126024,1840.940799999989 -120.749827,49.125998,1841.2215999999707 -120.750082,49.125874,1842.560799999966 -120.750137,49.125859,1842.7228000000173 -120.750214,49.12586,1842.711999999968 -120.750321,49.12587,1842.894660800008 -120.750462,49.125883,1843.6714198399695 -120.750537,49.125881,1844.1914628799666 -120.75061,49.125869,1844.4863999999775 -120.750692,49.125838,1844.932799999976 -120.750888,49.125706,1846.2849868799847 -120.75091,49.125666,1846.6409375999654 -120.750915,49.12562,1847.0782080000038 -120.750907,49.125573,1847.563054559979 -120.750873,49.125529,1848.2259483200025 -120.750736,49.125431,1850.1440000000027 -120.750579,49.125326,1852.2211999999765 -120.75043,49.125219,1853.991156800004 -120.750404,49.125193,1854.2946828799656 -120.7504,49.125158,1854.6121280000107 -120.75041,49.125095,1855.1532079999818 -120.750429,49.125038,1855.6519260799691 -120.750438,49.124981,1856.1368000000127 -120.750444,49.124923,1856.5543999999816 -120.750438,49.124862,1856.9935999999982 -120.750413,49.124814,1857.3391999999953 -120.750395,49.124753,1857.778400000012 -120.750391,49.124694,1858.4064000000276 -120.750411,49.12436,1863.215999999958 -120.750433,49.124297,1864.123200000021 -120.750464,49.124247,1864.8432000000448 -120.750526,49.124197,1865.5631999999664 -120.750652,49.124119,1865.7083795199821 -120.75079,49.124053,1865.4757647999927 -120.75096,49.123976,1865.6031999999884 -120.751051,49.123935,1865.7183999999534 -120.751181,49.123875,1866.4390200000332 -120.751654,49.123547,1869.7485244799682 -120.751696,49.123503,1870.020900480028 -120.751719,49.123442,1870.523302080062 -120.751707,49.123373,1871.3019345600171 -120.751684,49.123065,1877.610921600095 -120.751696,49.123014,1878.1477977600716 -120.751713,49.122986,1878.2925667200775 -120.751745,49.12296,1878.2830080000142 -120.75222,49.122625,1876.1320000000896 -120.752277,49.122568,1875.967074559928 -120.752317,49.12249,1876.1326831999606 -120.752355,49.122394,1877.274804800055 -120.752395,49.122264,1879.0608511999246 -120.752407,49.122051,1880.2859999999664 -120.752408,49.121998,1880.469599999989 -120.752391,49.121952,1880.757599999978 -120.752244,49.12166,1880.7407999999455 -120.752197,49.121557,1880.7318478400325 -120.752075,49.121444,1881.1532479999626 -120.752051,49.121416,1881.2702393600428 -120.752031,49.121368,1881.3015999999716 -120.752034,49.121167,1880.556400000037 -120.752052,49.121131,1880.2971999999495 -120.752087,49.121081,1879.9179691199706 -120.752192,49.120956,1878.7199539200562 -120.752261,49.120857,1877.7941060799706 -120.752277,49.120786,1877.946399999935 -120.752276,49.120732,1878.3424000000607 -120.752256,49.12068,1878.8607999999797 -120.752208,49.12063,1879.5801216000436 -120.751893,49.12042,1883.6264224000386 -120.751761,49.120331,1885.7245326399773 -120.751632,49.120212,1887.943247359975 -120.751532,49.120089,1889.9689299200302 -120.751413,49.119913,1892.3388142400286 -120.751215,49.119674,1896.5689263999684 -120.751129,49.119529,1897.7871966399296 -120.751029,49.119354,1899.1823999999624 -120.751018,49.11933,1899.3407999999718 -120.751013,49.119301,1899.4127999999946 -120.750996,49.119137,1899.7643999999832 -120.750954,49.119017,1900.8012000001024 -120.750943,49.118937,1901.2475999998962 -120.750952,49.118869,1901.2912000000279 -120.750982,49.118771,1900.8592000000954 -120.751011,49.118705,1900.441599999922 -120.751068,49.118636,1899.6207999999478 -120.751146,49.118547,1898.4975999999187 -120.751164,49.118519,1898.2383999999593 -120.751174,49.11848,1898.0943999999135 -120.751234,49.118342,1897.2304000000486 -120.751257,49.118312,1896.9395353600592 -120.751285,49.118287,1896.6004168000534 -120.751571,49.118073,1893.3559716800191 -120.751799,49.117897,1892.215741759944 -120.751841,49.117859,1891.9794204799962 -120.751855,49.117835,1891.9493359999235 -120.751859,49.117803,1892.083739840042 -120.751849,49.117658,1893.8028793599353 -120.751855,49.117635,1894.012983999935 -120.751875,49.117611,1894.1014000000603 -120.752076,49.117467,1893.2243999999732 -120.752108,49.117442,1892.853599999912 -120.75213,49.117414,1892.6376000000992 -120.752139,49.117383,1892.6196000000168 -120.752142,49.117344,1892.7167999998992 -120.752154,49.11718,1892.905062399927 -120.752163,49.117161,1892.7263785600715 -120.752189,49.117142,1892.2586809599813 -120.752305,49.117078,1889.6927999998488 + + + 0 + clampToGround + -120.752305,49.117078,1889.6927999998488 -120.752311,49.117074,1889.54879999988 -120.752358,49.117043,1888.4219999999877 -120.752385,49.117017,1887.7451999999062 -120.752391,49.116976,1887.4679999999407 -120.752364,49.116908,1887.8706355200034 -120.752216,49.116687,1891.155160319895 -120.752077,49.116472,1894.659600000003 -120.752055,49.116417,1895.2140000000356 -120.752047,49.116354,1895.3363715198896 -120.752062,49.116273,1894.7969590401149 -120.752108,49.116141,1893.5114451198472 -120.752177,49.115952,1892.5495999999307 -120.752254,49.115782,1891.7976000000951 -120.752283,49.115702,1891.8515999999076 -120.752288,49.115662,1892.049600000124 -120.75228,49.115625,1892.4600000000087 -120.75226,49.115542,1893.5573631999146 -120.752226,49.115441,1895.9883913601864 -120.752215,49.115384,1897.191999999924 -120.752248,49.115287,1898.280280960081 -120.752273,49.115219,1898.7882804799 -120.752334,49.115082,1899.6884115198939 -120.752378,49.115025,1899.78552800008 -120.752427,49.114961,1899.5948000000417 -120.752495,49.114884,1898.925200000044 -120.75255,49.114817,1897.9961839999992 -120.752589,49.114749,1897.5335134399622 -120.752631,49.114654,1896.9444000001497 -120.752698,49.114446,1896.0048000001175 -120.75281,49.114233,1896.2605008000999 -120.752825,49.114209,1896.2263079999316 -120.752851,49.114188,1895.8678444798486 -120.752917,49.114152,1894.7557267201382 -120.752965,49.114099,1894.5847727998657 -120.753018,49.114034,1894.682856960128 -120.753043,49.113994,1894.9898073599309 -120.753067,49.113938,1895.699284160024 -120.753076,49.113867,1896.794800000162 -120.753181,49.11371,1896.2224000000724 -120.753264,49.113615,1895.4555999999366 -120.753442,49.113238,1894.6910598397892 -120.753463,49.113179,1894.6361540800744 -120.753477,49.113117,1894.8227953598798 -120.753483,49.11306,1895.1877791999545 -120.753492,49.112809,1898.5520000001643 -120.753496,49.112777,1898.9108396800245 -120.753509,49.112748,1898.9625932801048 -120.753573,49.112657,1898.639945439949 -120.753655,49.112471,1899.8900000000863 -120.753678,49.112395,1900.9268000001034 -120.75373,49.112324,1901.4559999999183 -120.753788,49.112277,1901.4668000000188 -120.753815,49.112243,1901.6899999999669 -120.75384,49.112214,1901.8763904000134 -120.75385,49.112186,1902.3241439999754 -120.753857,49.11207,1904.6842895999853 -120.753868,49.11204,1905.1874688000273 -120.753886,49.112008,1905.6607795200018 -120.753993,49.11184,1906.6264000000392 -120.754077,49.111709,1906.493200000043 -120.754104,49.111687,1906.3780000000013 -120.754132,49.111676,1906.216000000001 -120.754167,49.111669,1905.9915798400034 -120.75424,49.111665,1905.9835840000208 -120.754552,49.111686,1905.7642508800132 -120.75476,49.111674,1906.15562880002 -120.754975,49.111671,1907.7150119999835 -120.755276,49.111777,1907.4110841600743 -120.755559,49.111876,1907.252799999967 -120.756332,49.112102,1905.3654502399963 -120.756668,49.112178,1906.2505075200722 -120.757097,49.11227,1905.122187199836 -120.757164,49.112273,1904.8042553598714 -120.757223,49.112276,1904.5101420799356 -120.75729,49.112272,1904.3301248001908 -120.757363,49.112256,1904.8592268798793 -120.757505,49.112205,1906.7068839999097 -120.757982,49.111924,1913.3831999999625 -120.758022,49.111894,1913.8152000000484 -120.758064,49.111876,1913.864581119876 -120.758472,49.111734,1914.335587840083 -120.758551,49.111715,1914.4201072001629 -120.75864,49.111706,1913.8700736000872 -120.758843,49.111683,1911.906113760049 -120.758919,49.111661,1911.6234113599523 -120.758978,49.111623,1911.7040297599692 -120.759068,49.111523,1913.2470905600671 -120.759566,49.111198,1916.1968812798723 -120.759876,49.110932,1916.3647999999453 -120.759908,49.110911,1916.2064000000382 -120.759963,49.110894,1915.6591999999873 -120.760152,49.110882,1913.4658611200016 -120.760214,49.110869,1912.9773612799236 -120.760256,49.110844,1913.0108211200156 -120.760292,49.110809,1913.2888851198688 -120.760321,49.110754,1914.0056393599539 -120.76032,49.110686,1915.2766208000405 -120.760323,49.110447,1919.9555777599899 -120.760359,49.110238,1923.7969283200255 -120.76042,49.110025,1926.5480800000475 -120.760664,49.109653,1927.293999999872 -120.760713,49.109613,1927.132000000025 -120.7608,49.109577,1926.2139999999129 -120.761365,49.109498,1920.2937423999465 -120.761456,49.109474,1919.753706240124 -120.761512,49.109453,1919.4896505599986 -120.761566,49.109418,1919.3486995200055 -120.761593,49.109384,1919.5738924799455 -120.761612,49.109332,1920.3331353599667 -120.761615,49.10917,1923.6742319999346 -120.761516,49.10864,1932.3243903999733 -120.761507,49.108551,1933.7971867201593 -120.761525,49.10847,1934.3530799998762 -120.761813,49.108059,1933.7771999999713 -120.761866,49.107931,1934.0396000000424 -120.76196,49.107557,1934.4142911999522 -120.761971,49.107517,1934.27545071993 -120.761977,49.107464,1934.065189119959 -120.761979,49.107179,1932.7868000000958 -120.761974,49.107133,1932.3980000000797 -120.761957,49.107086,1932.2576000001272 -120.761858,49.106844,1932.0818700799496 -120.761857,49.106808,1931.9281702399032 -120.761861,49.106765,1931.6619416000742 -120.761939,49.106312,1932.103425280035 -120.761953,49.106248,1931.7995782398727 -120.761955,49.106187,1931.7443815999904 -120.761933,49.106103,1932.2180000000103 -120.761821,49.105796,1933.5917273600483 -120.761802,49.105724,1933.8850380799352 -120.761786,49.105627,1934.1032931199795 -120.761847,49.104476,1941.0749491199456 -120.762033,49.103848,1944.78759999991 -120.762062,49.103755,1944.3087999999934 -120.762051,49.10366,1945.4284000000316 -120.76201,49.103497,1947.9117487998737 -120.762006,49.103446,1948.2885190400261 -120.761992,49.103362,1949.167132159778 -120.762023,49.102965,1953.7098072000706 + + + 0 + clampToGround + -120.762023,49.102965,1953.7098072000706 -120.762035,49.10282,1955.7035520000825 -120.762302,49.101973,1957.6365516797955 -120.762509,49.101687,1958.1312000002672 -120.763066,49.101036,1956.8974681601792 -120.763541,49.100435,1953.6619415999853 -120.764015,49.100009,1955.5156903999855 -120.764064,49.099926,1956.3535385598013 -120.764348,49.099737,1954.0232710401126 -120.764438,49.099648,1954.2814028799799 -120.764701,49.099429,1954.6124564799452 -120.764768,49.099293,1955.0061958399106 -120.764872,49.09923,1954.7458496000586 -120.764929,49.099077,1955.4286923200307 -120.764843,49.099067,1955.9276062400015 -120.76489,49.099011,1955.570081600027 -120.764935,49.098925,1955.264420000025 -120.765077,49.098647,1954.7586142399837 -120.765359,49.098007,1951.4132000001227 -120.765591,49.097413,1945.4470929600677 -120.765958,49.096336,1938.6070515198933 -120.76608,49.095888,1935.8944000001882 -120.766198,49.095568,1927.978400000116 -120.76629,49.095366,1925.3818655998675 -120.766466,49.095196,1925.850274559844 -120.76673,49.095044,1926.9719999999647 -120.766949,49.09495,1926.6455999997622 -120.767108,49.094887,1924.6259999998988 -120.767286,49.094855,1924.6610512001612 + + + 0 + clampToGround + -120.767286,49.094855,1924.6610512001612 -120.767464,49.094823,1924.1886188799135 -120.76781,49.094818,1926.0175967997718 -120.768667,49.094859,1927.503329119969 -120.768905,49.094859,1927.9962408000022 -120.769261,49.094848,1929.8778873599636 -120.769725,49.094799,1933.6783999999968 -120.770001,49.094746,1934.5136000000143 -120.770381,49.094645,1933.2693047999992 -120.770726,49.094528,1933.2151999999637 -120.771122,49.094358,1934.0270009600133 -120.771157,49.094344,1934.105463680005 -120.771353,49.09423,1934.1985424000138 -120.771645,49.094042,1934.47320000003 -120.771815,49.093907,1934.1000168000091 -120.772035,49.093677,1932.9717456000114 -120.772533,49.093081,1934.4371179199486 -120.772694,49.092833,1936.8964000000185 -120.772786,49.092604,1938.7550822399385 -120.772803,49.092409,1939.6907999999316 -120.77277,49.092185,1939.891751999919 -120.772705,49.091917,1939.385714400008 -120.772635,49.091686,1939.8405743998994 -120.772501,49.091455,1938.7764000000661 -120.772394,49.091293,1937.8277433601309 -120.771838,49.090629,1929.4056000000721 -120.771702,49.090439,1925.6513731201555 -120.771563,49.090204,1920.7488000000126 -120.771496,49.090003,1918.0955999999837 -120.771466,49.0898,1917.94080000012 -120.771439,49.089547,1918.1358184000028 -120.771418,49.089231,1918.92174335999 -120.771396,49.089024,1918.973007360017 -120.771348,49.088785,1918.4542527999936 -120.771158,49.088094,1914.2803238399824 -120.771107,49.087808,1910.6855590400228 -120.771066,49.087493,1907.794584959811 -120.771088,49.087111,1907.815773439736 -120.771118,49.086608,1909.0635622399236 -120.771176,49.085138,1899.425495040123 -120.771167,49.084934,1898.1332102397723 -120.771149,49.084643,1895.9477012801642 -120.771087,49.084325,1894.9781240001269 -120.771026,49.084073,1891.2249180797646 -120.770941,49.083773,1887.7194134401836 -120.77081,49.083452,1885.941369600089 -120.770602,49.083139,1884.1158268801726 -120.770405,49.082881,1881.9682072001012 -120.77013,49.082598,1881.1521792000408 -120.769862,49.082391,1880.045455679989 -120.769485,49.082169,1875.2756000000122 -120.76895,49.08193,1870.8834399999546 -120.768137,49.081638,1868.308658239955 -120.766867,49.081162,1866.0793238398974 -120.765276,49.080622,1861.3983999999348 -120.765093,49.080578,1860.473200000122 -120.764845,49.080543,1860.2163783999852 -120.764667,49.080551,1860.676520640005 -120.764513,49.080586,1861.123599999883 -120.764357,49.080656,1862.551304959955 -120.76431,49.080737,1864.2771535999432 -120.764272,49.080843,1866.3031923200863 -120.764216,49.08103,1868.6162816000488 -120.764162,49.081117,1869.0172438399698 -120.764074,49.081161,1868.3240854399262 -120.763988,49.081196,1867.4530380799456 -120.763857,49.081214,1865.6094780800536 -120.763682,49.081212,1862.8398886400507 -120.763506,49.081166,1860.260000000095 -120.763408,49.081067,1858.1711999999434 -120.763369,49.080938,1855.8924000000181 -120.763398,49.080811,1854.333834239897 -120.763494,49.080637,1853.018826239951 -120.763635,49.08051,1853.8378959998759 -120.76392,49.080339,1854.930969600031 -120.764273,49.080247,1853.1819857599978 -120.764463,49.080235,1852.6909128000268 -120.764731,49.080237,1851.6787999998958 -120.765096,49.080284,1852.8111999999733 -120.765546,49.080368,1852.6560268799967 -120.766368,49.080545,1849.849257600022 -120.766847,49.08062,1848.1404000001212 -120.767476,49.080715,1843.4379871999065 -120.769128,49.081072,1840.1128012799993 -120.769315,49.081096,1840.059849600041 -120.769569,49.081123,1840.4043915199345 -120.769865,49.081148,1842.2194815999378 -120.770144,49.081136,1842.6703513600198 -120.770592,49.081057,1837.9423577601249 -120.770941,49.080998,1832.6041692800602 -120.771168,49.080982,1831.0767999999541 -120.771496,49.080977,1830.150864639911 -120.771935,49.081005,1830.213999999915 -120.772383,49.081052,1827.930705919951 -120.772852,49.081091,1825.2146905600066 -120.773229,49.081098,1823.3106716799841 -120.773668,49.081072,1820.1616716799872 -120.773821,49.081054,1818.632702720021 -120.773974,49.081008,1817.001871360059 -120.774057,49.080969,1816.1984406399956 -120.774135,49.0809,1815.8027199999915 -120.774162,49.080838,1816.0005644799708 -120.774166,49.080762,1816.259751040027 -120.774089,49.08062,1816.683001600002 -120.773971,49.080548,1816.0316403200213 -120.773876,49.080519,1814.5789875200999 -120.773722,49.080504,1812.719383039962 -120.773561,49.080523,1812.2539428799826 -120.773354,49.080567,1812.545473600056 -120.77293,49.080671,1817.0045488000683 -120.772794,49.080694,1817.957906560025 -120.772555,49.080697,1818.4819784000272 -120.77229,49.080661,1818.3544447999868 -120.772067,49.080597,1817.6031999999086 -120.771826,49.080499,1815.583230080091 -120.77163,49.08037,1812.6283520000566 -120.771485,49.080218,1809.3066816000783 -120.771394,49.080093,1807.263920639936 -120.77129,49.079881,1804.56560000015 -120.771161,49.079375,1799.1274999998425 -120.771018,49.078852,1793.6752563200334 -120.770959,49.078711,1794.3263508799428 -120.770884,49.078647,1794.8229017600231 -120.770779,49.078591,1795.5218385599492 -120.770584,49.078538,1797.0853516799962 -120.770342,49.078545,1796.469200000004 -120.770229,49.078574,1795.6302601599914 + + + 0 + clampToGround + -120.770229,49.078574,1795.6302601599914 -120.770342,49.078545,1796.469200000004 -120.770316,49.078434,1796.7751999999732 -120.770527,49.078354,1797.822800000014 -120.770649,49.078232,1800.3399187199902 -120.77082,49.078147,1802.3822015999622 -120.770926,49.07804,1804.038963199925 -120.771031,49.077896,1806.4174860799956 -120.771137,49.077753,1809.073513440053 -120.771137,49.077603,1812.9038414399906 -120.771105,49.077497,1815.956400000028 -120.771129,49.077374,1814.7415881599538 -120.771292,49.077289,1810.7301484799511 -120.771178,49.077145,1810.1973151999032 -120.770853,49.077017,1809.9063718399502 -120.770621,49.076925,1807.0017240001032 -120.77056,49.0769,1806.0643200000607 -120.770381,49.076735,1800.7554407999944 -120.770145,49.076506,1794.6485504000318 -120.769974,49.076267,1789.0749433600838 -120.769804,49.076032,1783.1142195199402 -120.76969,49.075835,1779.9066079999607 -120.76956,49.075686,1775.5716607999375 -120.769324,49.075569,1772.4022137600123 -120.769137,49.075483,1768.5110961599632 -120.76895,49.075297,1761.1596240000174 -120.768934,49.075137,1756.6926870399548 -120.768934,49.07494,1751.5636863999475 -120.768836,49.074828,1748.9039999998768 -120.768673,49.074701,1744.6558684799436 -120.768527,49.074594,1740.8223449599293 -120.768275,49.07444,1736.191440000138 -120.768262,49.074378,1733.6049062400639 -120.768242,49.07429,1730.0728511999155 -120.76821,49.074147,1724.4448704001938 -120.768104,49.074029,1719.2099347201884 -120.767877,49.073944,1716.614879359927 -120.767787,49.073864,1715.0910252800295 -120.767681,49.073715,1709.9325903999818 -120.767519,49.073635,1708.2248944000492 -120.767421,49.073523,1704.628235040017 -120.767429,49.07338,1701.5359776000682 -120.767356,49.07322,1697.3637503999607 -120.76721,49.073081,1696.1787392000654 -120.76699,49.073055,1694.3237040000229 -120.766839,49.07308,1692.53619520006 -120.766727,49.072958,1691.0674387200365 -120.766108,49.072407,1686.9484000000662 -120.765419,49.071953,1682.8790825599083 -120.765029,49.071733,1683.7210692799317 -120.764604,49.071479,1683.0268000001686 -120.764358,49.071313,1682.3486198399823 -120.7641,49.071113,1680.5328000000259 -120.764013,49.071027,1677.9721268801097 -120.763926,49.070922,1675.8479353599853 -120.763883,49.070797,1673.8402648001027 -120.763917,49.07066,1669.9958559998936 -120.763977,49.070587,1667.4507352001287 -120.764066,49.070545,1665.2583424001236 -120.764182,49.070527,1663.4923254398686 -120.764272,49.070522,1662.2461926397884 -120.764426,49.070551,1661.9706889600877 -120.764631,49.070613,1663.6396868801587 -120.764841,49.070699,1665.5087873597536 -120.765186,49.07087,1667.1087487999566 -120.765954,49.07134,1668.382508800205 -120.766287,49.071517,1668.457136319866 -120.766936,49.071818,1666.3255999999842 -120.767097,49.071898,1663.5114787196103 -120.767255,49.072009,1661.1678231998076 -120.767743,49.072436,1659.2459078402667 -120.767993,49.072626,1656.334399999839 -120.768325,49.072838,1654.1658959996869 -120.768618,49.072997,1655.741572159928 -120.768918,49.073116,1653.1375910399443 -120.769425,49.073285,1648.9378199999674 -120.770128,49.07351,1643.444537599924 -120.770541,49.073648,1644.015841280039 -120.770973,49.073766,1642.0199999999875 -120.772083,49.074001,1629.0740529603338 -120.772295,49.07406,1625.1144320001663 -120.772512,49.074155,1621.1558431997219 -120.772646,49.074233,1620.9884336001564 -120.772804,49.074339,1620.1050342402511 -120.772925,49.07445,1621.1587999998917 -120.773059,49.074666,1632.0131292798953 -120.77314,49.074748,1633.8932224000632 -120.773271,49.074813,1632.4285321601742 -120.773456,49.074863,1629.5956057601559 -120.774446,49.075072,1613.401303039968 -120.774793,49.075142,1610.732692479931 -120.775222,49.075222,1610.2686380797795 -120.775559,49.075298,1607.1115891197821 -120.775862,49.075354,1604.795045759822 -120.776154,49.075447,1603.0462185598515 -120.77643,49.075564,1602.7289023999274 -120.776584,49.075628,1602.5096422400627 -120.776737,49.075721,1602.2995822400985 -120.776914,49.075868,1601.8903999998686 -120.776997,49.075943,1602.4976153601815 -120.77708,49.076031,1603.0633568000596 -120.777171,49.076096,1602.1630745601171 -120.777275,49.076131,1600.5731999999427 -120.777416,49.076136,1599.086399999962 -120.777544,49.076122,1598.0597721599866 -120.777643,49.076075,1597.6769240000258 -120.777735,49.075984,1597.014329599998 -120.77778,49.075903,1596.5096000000017 -120.777785,49.075815,1595.959999999975 -120.777737,49.075716,1595.4308000000065 -120.777606,49.075622,1594.6208000000306 -120.776072,49.075075,1590.9548479999387 -120.775935,49.075026,1589.7480272000305 -120.774414,49.074629,1584.4036547202522 -120.774167,49.074553,1586.2453931199702 -120.773977,49.074483,1587.0951726399896 -120.773891,49.07443,1586.5820095998326 -120.773832,49.074335,1586.0045087999824 -120.773792,49.074186,1583.3018764799242 -120.773753,49.074041,1581.6418281600447 -120.773703,49.073919,1581.1973174400266 -120.773596,49.073745,1581.3534208001215 -120.773502,49.073632,1581.6231385602696 -120.773336,49.073485,1578.6722751998752 -120.773194,49.073403,1577.1465961598642 -120.773006,49.073309,1576.7564000002035 -120.772719,49.073209,1577.5556000000274 -120.772019,49.072976,1576.9119999997731 -120.771852,49.072903,1573.8307475198176 -120.771606,49.072753,1568.8562374397666 -120.771488,49.072678,1569.0163251200095 -120.770946,49.072328,1566.3750950400438 -120.770812,49.072253,1565.097271680051 -120.770594,49.072176,1566.3622297599013 -120.770412,49.072134,1567.737735680044 -120.769766,49.071976,1573.4257932798976 -120.769685,49.071935,1573.868000000203 -120.769623,49.071839,1573.5800000000604 -120.769602,49.071787,1573.0615999998345 -120.769591,49.071719,1571.909599999878 -120.769629,49.071651,1569.6284000001601 -120.769677,49.071569,1567.9507999999448 -120.76976,49.071524,1565.5270015998512 -120.76986,49.071511,1562.8477664001614 -120.769943,49.071524,1560.879562880004 -120.770087,49.071568,1558.324254079901 -120.770149,49.071608,1557.9221369598731 -120.770388,49.071798,1557.434414080094 -120.770622,49.071955,1557.212910400048 -120.770935,49.072125,1557.670000000093 -120.771216,49.072267,1557.7082617599503 -120.771651,49.072475,1555.4897239997576 -120.77205,49.072626,1553.8304000000821 -120.772252,49.072694,1554.2841369600117 -120.772426,49.072742,1554.0734246399572 -120.772681,49.072791,1552.615383840046 -120.773239,49.072851,1549.2850371199656 -120.774036,49.072959,1543.164844800007 -120.774269,49.073007,1539.5843889598477 -120.774451,49.073102,1537.8930163199866 -120.774542,49.0732,1537.9847040000705 -120.774586,49.073299,1538.433546239999 -120.774604,49.073511,1538.6770137601136 -120.774663,49.073756,1540.3820153599252 -120.774749,49.073861,1539.8095214400205 -120.774798,49.073898,1539.8253913599033 + + + 0 + clampToGround + -120.774798,49.073898,1539.8253913599033 -120.774859,49.073944,1541.015568639737 -120.774993,49.074005,1542.2150656000579 -120.775126,49.074031,1543.0115382398774 -120.77531,49.074049,1542.9904000000965 -120.775481,49.074033,1540.8376000000067 -120.775666,49.073987,1537.2536000001382 -120.775833,49.073948,1534.4060000000632 -120.776042,49.073924,1531.8048550398526 -120.776208,49.073925,1530.8177440002078 -120.776562,49.073966,1530.5782009599113 -120.777045,49.07404,1534.9042879997444 -120.777249,49.07405,1535.424575999984 -120.777423,49.074019,1533.47688095999 -120.777893,49.073842,1526.9799817600158 -120.779228,49.07334,1513.8390016001272 -120.779542,49.073275,1511.50375200015 -120.77984,49.073238,1507.8128000003007 -120.780095,49.07324,1503.6869119998523 -120.780314,49.073264,1500.766739200035 -120.780553,49.073334,1500.079644160178 -120.780733,49.073461,1503.3436300801097 -120.781071,49.073787,1505.364136320218 -120.781339,49.073996,1502.0987462399373 -120.781656,49.074191,1497.6029638400348 -120.781843,49.074328,1494.560616320311 -120.782013,49.074494,1492.0160291197285 -120.782147,49.074642,1489.9994070401601 -120.782254,49.074824,1489.0508838402309 -120.782308,49.075023,1489.042831359983 -120.782313,49.075168,1489.1587513598952 -120.782294,49.075342,1489.2341683203374 -120.78223,49.075532,1488.2193024000035 -120.782171,49.075701,1490.959103679698 -120.781927,49.076246,1490.5071043202884 -120.781774,49.0766,1488.297792000068 -120.781723,49.07679,1485.870215999949 -120.781704,49.076966,1482.3951705599077 -120.781725,49.077118,1482.4435920001085 -120.781755,49.077258,1482.437441599917 -120.781841,49.077471,1482.359121440023 -120.782018,49.077774,1478.41840256014 -120.7823,49.078249,1473.3827839999124 -120.782396,49.078344,1473.3679769599682 -120.782509,49.078394,1473.2850761600514 -120.782621,49.078405,1473.224784799903 -120.782757,49.078398,1472.686986559989 -120.782855,49.078354,1472.632799999903 -120.782927,49.078299,1472.6544000000295 -120.782972,49.078223,1472.208000000031 -120.783094,49.077887,1470.5872377600897 -120.783212,49.07763,1469.5535104000498 -120.783383,49.077362,1466.5888000000277 -120.783582,49.077091,1464.070092479992 -120.783751,49.076901,1464.0498587198097 -120.783952,49.076699,1461.1300460798802 -120.784161,49.076531,1458.5578168000877 -120.784351,49.076414,1456.5531894400049 -120.784783,49.076152,1454.0792000001152 -120.785022,49.075978,1452.0868473599342 -120.78532,49.075758,1447.5055999999952 -120.785532,49.075532,1441.8343731201408 -120.785805,49.075207,1438.7831895998875 -120.786047,49.074912,1437.0807999998542 -120.786197,49.074722,1434.2308947200538 -120.786315,49.074534,1431.5099168000334 -120.786401,49.074302,1428.7188419200052 -120.786457,49.074094,1426.3606556800073 -120.786505,49.073926,1424.6802448001013 -120.786562,49.073773,1422.352800960124 -120.786642,49.073671,1421.1348547199425 -120.786744,49.073571,1419.5161990400184 -120.786857,49.073498,1418.37018656001 -120.787664,49.073082,1415.1226060800143 -120.788461,49.072667,1407.7610230400687 -120.789928,49.071944,1390.213214720077 -120.790089,49.071849,1390.2199999999298 -120.790164,49.071767,1389.528800000089 -120.790266,49.07163,1388.5664000000297 -120.790381,49.071341,1388.276736159899 -120.790448,49.071233,1388.0364966399577 -120.790555,49.071121,1388.1343287999373 -120.790668,49.071035,1385.9586848000522 -120.790741,49.070953,1383.2632020800972 -120.790759,49.070883,1380.6513531200537 -120.790748,49.070821,1378.0198380801055 -120.790665,49.070742,1373.9999168001175 -120.790573,49.070682,1371.8647193599566 -120.790452,49.070661,1371.1238553600435 -120.790302,49.070649,1370.6920022400911 -120.790121,49.070691,1371.3503971199107 -120.790006,49.070746,1372.1432179200208 -120.789928,49.070855,1373.4312000000834 -120.789879,49.07097,1375.2096000000442 -120.789841,49.071101,1377.4919999999092 -120.789772,49.071215,1378.4527584000289 -120.789662,49.071322,1379.3101945600959 -120.789526,49.071414,1379.7679999999443 -120.789273,49.071515,1378.9538087999497 -120.788984,49.071577,1378.6070534399637 -120.788812,49.071583,1378.4106278399324 -120.788586,49.07157,1377.8805408000212 -120.788363,49.071544,1376.9559628800791 -120.787809,49.07139,1370.1484000000155 -120.787524,49.071325,1366.7928000000697 -120.787271,49.071302,1365.607455360088 -120.787031,49.071293,1365.902292959901 -120.786826,49.0713,1366.7528960000182 -120.786655,49.071316,1367.5564832000694 -120.786503,49.071338,1367.0153657599028 -120.786256,49.071422,1366.4043494398934 -120.785848,49.07159,1364.816627200028 -120.785569,49.071657,1360.2157470399145 -120.785316,49.071683,1357.757509119946 -120.785174,49.071682,1356.9988454400584 -120.785014,49.071672,1356.2020646399997 -120.784823,49.071643,1356.1569105600245 -120.784609,49.071584,1355.9082982400132 -120.784351,49.071478,1355.4579855999582 -120.783735,49.07121,1351.9716720001102 -120.783268,49.071018,1350.7451609599696 -120.782841,49.070844,1349.7099398399864 -120.782484,49.070743,1345.4429369600132 -120.78198,49.070645,1343.4915679999035 -120.781417,49.070552,1345.456990719905 -120.78116,49.070482,1343.6930047999315 -120.78102,49.070424,1341.316800000117 -120.780929,49.070387,1339.7183999998551 -120.780704,49.070285,1335.3119999999299 -120.780392,49.070122,1331.9218009599078 -120.780269,49.070064,1330.88936128004 -120.77756,49.06841,1315.9923840000515 -120.777351,49.068317,1313.4120000000146 -120.777095,49.068225,1310.5579999999254 -120.776575,49.068081,1310.7137160001057 -120.775836,49.067876,1308.1795891199458 -120.775623,49.067795,1307.8765463999293 -120.775364,49.067662,1307.1365747200043 -120.773756,49.06671,1301.860230399934 -120.773604,49.066647,1301.2308249600717 -120.77338,49.066579,1301.2339583998587 -120.773242,49.066549,1301.7504000001118 -120.772892,49.066517,1303.7895999999184 -120.772236,49.066518,1307.0897459200708 -120.772115,49.066507,1306.61945440006 -120.771991,49.066476,1305.1392812800698 -120.771877,49.066368,1300.2684243199012 -120.77185,49.066237,1295.5210640000391 -120.771901,49.066135,1292.34515120009 -120.772035,49.066048,1289.8268000000921 -120.772107,49.066029,1289.333600000033 -120.772263,49.066028,1289.5620000001304 -120.773714,49.066266,1284.5367999999874 -120.774024,49.06633,1282.2495168000592 -120.774312,49.066419,1281.042885120203 -120.775805,49.066963,1275.3399863999323 -120.775983,49.067018,1276.1205484799034 -120.776248,49.067074,1278.06319999991 -120.776495,49.067103,1278.2744000001733 -120.776746,49.067125,1276.036279999828 -120.777021,49.067128,1272.701483519892 -120.777286,49.067094,1271.9515833599216 -120.778298,49.066854,1263.2179673599524 -120.77848,49.066813,1261.3546496000558 -120.778656,49.066806,1260.1842828800093 -120.778759,49.066816,1259.5831635200873 -120.778874,49.066851,1259.115537919982 -120.779088,49.066936,1257.019809279955 -120.779492,49.067146,1257.2911999999787 -120.779684,49.067211,1257.0895999999761 -120.779835,49.067237,1258.364003199986 -120.779968,49.067205,1258.3284576000083 -120.780089,49.067167,1259.0810865600142 -120.780165,49.067109,1259.2491431999765 -120.780202,49.067037,1258.6958908800436 -120.780183,49.066944,1256.6019999998332 -120.780135,49.066848,1253.3188000000393 -120.780069,49.066742,1249.3659999998897 -120.779987,49.066534,1245.3840966400478 -120.779974,49.066356,1244.0225177599177 -120.780013,49.066184,1241.4991999999493 -120.780152,49.065946,1238.297144319936 -120.780332,49.065777,1235.362399999905 -120.780593,49.065638,1232.237599999967 -120.780822,49.065548,1230.5931097599919 -120.781066,49.065489,1228.5610220799654 -120.781332,49.065448,1226.091901440153 -120.781622,49.065456,1225.2008307198826 -120.782363,49.06563,1227.7539999998612 -120.784497,49.065949,1215.441565760191 -120.785318,49.06599,1215.3151999999502 -120.78554,49.06599,1214.5159999999728 -120.785761,49.065971,1212.6943999998907 -120.786103,49.065903,1206.0930766399547 -120.786243,49.065851,1201.9774081601188 -120.786382,49.06578,1198.8928383999055 -120.786578,49.065654,1196.367675520058 -120.786693,49.065506,1193.8158876799914 -120.786798,49.065358,1193.186944640051 -120.786821,49.065269,1192.4292000000219 -120.786796,49.065188,1191.172799999971 -120.78673,49.065108,1189.7832000000517 -120.786547,49.064978,1187.5691999999956 -120.786391,49.064927,1187.007599999981 -120.786187,49.064907,1186.7566673599927 -120.785769,49.064987,1189.1956388799986 -120.785574,49.064985,1189.930014400012 -120.785491,49.064968,1190.616882560065 + + + 0 + clampToGround + -120.785491,49.064968,1190.616882560065 -120.785407,49.064951,1191.3213838399467 -120.783554,49.064161,1194.144400000002 -120.782095,49.063558,1193.266800000008 -120.782212,49.063382,1192.2119999999854 -120.782528,49.063027,1190.5907999999831 -120.782617,49.062937,1189.298399999972 -120.782854,49.062751,1186.8771478399938 -120.782997,49.062644,1186.1383212800017 -120.783174,49.062536,1184.8328000000017 -120.783385,49.062444,1184.2016000000026 -120.783413,49.062431,1184.2484000000122 -120.783678,49.062345,1184.5579999999982 -120.783818,49.062302,1184.712799999991 -120.784035,49.062268,1184.8351999999888 -120.784324,49.062257,1184.9457132799953 -120.784694,49.062262,1185.0 -120.784966,49.06228,1185.0 -120.785322,49.062319,1185.0 -120.786386,49.062424,1183.8179545599928 -120.786637,49.062445,1185.2035464000176 -120.786862,49.062442,1186.6771718400084 -120.787162,49.062411,1188.1035999999917 -120.787473,49.0624,1187.1321919999737 -120.787806,49.062415,1186.4907999999812 -120.788217,49.062484,1184.361199999981 -120.788459,49.062543,1184.0847684799944 -120.78896,49.062677,1184.0928768000033 -120.790057,49.06294,1183.8745632000157 -120.790588,49.063074,1186.2340889600007 + + + 0 + clampToGround + -120.790588,49.063074,1186.2340889600007 -120.791961,49.063421,1195.2435902399734 -120.792178,49.063476,1195.6225651200034 -120.792332,49.063513,1195.010415359989 -120.792688,49.06325,1188.5726399999999 -120.792964,49.063364,1187.1371878400164 -120.793127,49.06342,1186.9105536000172 -120.793451,49.063588,1187.7856435200279 -120.793563,49.063658,1188.9317084799227 -120.79394,49.06376,1191.074624000081 -120.794435,49.063821,1195.224490399938 -120.795237,49.063812,1190.0 -120.796096,49.063895,1196.9259968000938 -120.796729,49.063956,1198.0007999999514 + + + 0 + clampToGround + -120.796729,49.063956,1198.0007999999514 -120.796837,49.063967,1197.4607999999846 -120.797131,49.064026,1196.28689824003 -120.797547,49.064031,1193.4482372800096 -120.797574,49.064028,1193.3689868800088 -120.797671,49.064018,1193.1082691199863 -120.798036,49.063944,1192.3967999999952 -120.798152,49.063763,1192.3472000000063 -120.798232,49.063629,1192.6351999999954 -120.798561,49.063125,1197.7745000000298 -120.798703,49.06306,1198.336092800012 -120.798862,49.063063,1198.9274057599978 -120.799122,49.063049,1200.6113948799732 -120.799274,49.063018,1201.0330412799835 -120.799558,49.062885,1200.9297968000187 -120.799883,49.062702,1198.750903360032 -120.799884,49.062701,1198.7334246399448 + + + 0 + clampToGround + -120.799884,49.062701,1198.7334246399448 -120.800338,49.062491,1196.4011999999793 -120.800603,49.06232,1197.5829215999759 -120.800864,49.062143,1198.1104000000153 -120.801046,49.061992,1198.7655999999981 -120.801296,49.061505,1203.4677792000518 -120.801496,49.061149,1206.2890041599962 -120.802076,49.060768,1212.8847999999327 -120.802528,49.060633,1216.1917030399657 -120.803285,49.060692,1219.2638687999822 -120.80369,49.060892,1218.0728000000017 -120.804381,49.060895,1221.49249520005 -120.804938,49.060951,1224.2345049600272 -120.80564,49.06107,1231.311999999983 -120.805919,49.061156,1233.6639251200465 -120.806234,49.061308,1235.643200000084 -120.806543,49.061502,1237.5904000001096 -120.807465,49.061848,1246.6657471999383 -120.80786,49.062078,1248.15368320001 -120.808267,49.062231,1248.6814921600294 -120.809305,49.062669,1246.89360000002 -120.810295,49.063101,1247.2875999999858 -120.811239,49.063474,1254.045999999996 -120.811364,49.063494,1253.6680000000038 -120.811607,49.063512,1252.8580000000281 -120.81171,49.0635,1252.506399999982 -120.81185,49.063448,1252.140351999991 -120.811948,49.063292,1251.9743999999928 -120.81206,49.06321,1251.1679999999824 -120.812219,49.063167,1250.0232000000078 -120.812625,49.063145,1247.4220000000164 -120.81266,49.063145,1247.1700000000387 + + + 0 + clampToGround + -120.81266,49.063145,1247.1700000000387 -120.812928,49.063145,1245.414537599996 -120.813356,49.063177,1243.74687552003 -120.813723,49.063209,1242.25440000002 -120.813828,49.063225,1242.0492000000265 -120.814242,49.063281,1242.2974940800418 -120.814462,49.063297,1242.6075999999848 -120.81458,49.063268,1242.2944000000084 -120.814876,49.063199,1241.8169209599735 -120.815307,49.063113,1241.7256000000293 -120.816015,49.063081,1240.9238127999486 -120.816795,49.063092,1241.5953855999594 -120.817482,49.063084,1241.2696000000312 -120.817943,49.06309,1238.0584000000208 -120.81805,49.063203,1238.101600000014 -120.818455,49.06336,1238.3840000000428 -120.818794,49.063508,1240.5151999999825 -120.81904,49.063535,1241.0530560000218 -120.819493,49.063508,1241.4323999999706 -120.819712,49.063486,1242.062399999966 -120.819858,49.0635,1242.6887999999703 -120.820148,49.063598,1245.4788518400244 -120.820412,49.063433,1245.9063443200769 -120.820728,49.063271,1245.4290924800202 -120.821013,49.063084,1244.1226323200324 -120.821758,49.062679,1247.4505574399993 -120.822204,49.062504,1247.041310719969 -120.822842,49.06243,1246.3230624000096 -120.823428,49.062379,1245.5643999999998 -120.823646,49.0623,1245.154400000014 -120.823901,49.062226,1244.01419296001 -120.824032,49.062177,1244.162800000006 -120.824119,49.062114,1244.3895999999959 -120.824145,49.062033,1244.6812000000016 -120.824125,49.061909,1245.1276 -120.824195,49.061799,1245.5721928000034 -120.824314,49.061728,1245.8963123199972 -120.824449,49.061657,1245.9487999999835 -120.824362,49.061395,1245.0219999999908 -120.824345,49.060877,1252.126277600056 -120.824394,49.060807,1256.9727372803561 -120.824915,49.060733,1261.3063816001197 -120.825387,49.060737,1271.604229439855 -120.826168,49.060829,1276.8183846401234 -120.826493,49.060858,1271.0280822397422 -120.826774,49.06083,1270.6531263997751 -120.827895,49.060937,1263.3910903998442 -120.827914,49.060936,1263.407651840097 + + + 0 + clampToGround + -120.827914,49.060936,1263.407651840097 -120.829174,49.060875,1269.6971999998814 -120.829588,49.060858,1268.2429836798365 -120.829914,49.060875,1266.359599999856 -120.830202,49.060814,1266.6156262399768 -120.830656,49.060778,1263.233569279983 -120.830962,49.060512,1270.035999999858 -120.83163,49.060394,1265.6488575998308 -120.832206,49.060049,1279.294400000042 -120.832745,49.059535,1285.7609360000279 -120.833265,49.059391,1273.4817392001717 -120.834036,49.058982,1267.9209561600562 -120.834214,49.05895,1264.653023999823 -120.834661,49.058637,1267.842941280116 -120.835028,49.058629,1262.9463084799636 -120.83519,49.058549,1263.5822848000619 -120.835574,49.058436,1262.3661414400872 -120.835708,49.058457,1259.493525760141 -120.836002,49.058195,1261.2568144001489 -120.836197,49.057907,1261.2980000000207 -120.836307,49.057643,1260.5213630399396 -120.836291,49.057422,1260.776399999979 -120.836186,49.057122,1260.3033433600144 -120.83605,49.056805,1259.77044 -120.836075,49.056802,1259.4566640000783 -120.836452,49.05676,1256.8991999999578 -120.836707,49.056713,1257.5997806400403 -120.836657,49.056678,1257.0467999999944 -120.836595,49.056661,1256.680799999991 -120.836461,49.056634,1255.9068000000268 -120.836461,49.056419,1253.5847999999805 -120.836523,49.056319,1252.9796000000133 -120.836615,49.056169,1252.2307999999598 -120.836702,49.056088,1251.9110169600556 -120.836785,49.05604,1251.7189440000857 -120.836804,49.055971,1250.7218246399968 -120.83689,49.05589,1249.7840159999514 -120.836916,49.055819,1248.7480838400036 -120.836966,49.05575,1248.2319200000602 -120.837029,49.055674,1247.7133961600455 -120.837067,49.055602,1247.0165686400242 -120.837175,49.055493,1246.6048000000017 -120.83733,49.055378,1247.52479999993 -120.837438,49.055324,1248.4968000000595 -120.837462,49.05531,1248.7056000000184 -120.837811,49.055255,1253.812414400077 -120.838092,49.055284,1262.1749222401018 -120.838246,49.056129,1263.641447359997 -120.838407,49.056506,1262.5368166399999 + + + 0 + clampToGround + -120.838407,49.056506,1262.5368166399999 -120.838707,49.057212,1266.2956966399972 -120.8405,49.058588,1268.8329600000404 -120.840679,49.058647,1268.9275999999718 -120.840789,49.058683,1268.8051999999739 -120.841395,49.058864,1270.5179424000685 -120.841748,49.05902,1272.3405952000037 -120.842162,49.059203,1272.7239574400783 -120.842781,49.059479,1272.5139999999624 -120.843546,49.059819,1269.8837299200275 -120.844141,49.06027,1269.8686127999329 -120.844277,49.060448,1270.3353958400405 -120.844966,49.060302,1274.877599999998 -120.84618,49.060247,1278.9155999999798 -120.847204,49.060427,1279.5724403199795 -120.847249,49.060638,1280.2681884799858 -120.84715,49.060848,1281.8190720000086 -120.847604,49.06096,1280.114547199988 -120.848149,49.060848,1280.2658380800221 -120.849332,49.060676,1282.1616000000113 -120.850005,49.060758,1284.0131183999893 -120.850796,49.061367,1284.0 + + + 0 + clampToGround + -120.850796,49.061367,1284.0 -120.850836,49.061398,1284.0423999999928 -120.851562,49.061797,1287.8535891199754 -120.851968,49.06187,1288.5751999999839 -120.85235,49.061751,1285.0580000000134 -120.852558,49.061637,1282.5823999999861 -120.852999,49.06158,1279.9551232000124 -120.853547,49.061514,1278.6928953600336 -120.853671,49.061505,1279.106362399995 -120.854301,49.061463,1280.0844000000968 -120.854811,49.061474,1281.655749119949 -120.856658,49.060976,1302.369600000032 -120.857845,49.061121,1302.7950152 -120.858061,49.061274,1300.0842131200666 -120.858454,49.061555,1296.804000000011 -120.859769,49.06163,1292.9713711999661 -120.861066,49.061681,1304.7943798399788 -120.861211,49.06159,1306.5784991999437 -120.861311,49.061567,1307.3103849600805 -120.862124,49.061646,1307.0104921598468 -120.862921,49.061838,1313.5711337599628 -120.864052,49.062094,1328.3642060799398 -120.865043,49.062278,1333.599032320013 -120.865753,49.062496,1333.827906559905 -120.866285,49.062525,1339.461660000037 -120.86669,49.062616,1341.1118784000469 -120.866923,49.062517,1343.6280753600063 -120.868235,49.062972,1337.3540000000053 -120.868888,49.06304,1335.9441792000034 + + + 0 + clampToGround + -120.868888,49.06304,1335.9441792000034 -120.869199,49.063072,1338.2919999999558 -120.869939,49.062941,1345.1198369599904 -120.870281,49.063071,1344.9779100800558 -120.870488,49.063354,1344.7567999999806 -120.871285,49.063705,1346.3644120000133 -120.87252,49.064034,1351.140774399961 -120.872835,49.063981,1353.8864903999647 -120.873109,49.06409,1355.0735023999953 -120.873348,49.064125,1355.7055999999907 -120.87338,49.06395,1358.4560000000042 -120.873748,49.06395,1360.6128000000454 -120.874005,49.063848,1363.2160703999775 -120.874224,49.063652,1366.2944179199997 -120.874865,49.063719,1368.6116000000038 -120.875993,49.063681,1367.1785803199696 -120.877066,49.063621,1373.8887571200255 -120.877802,49.063758,1372.4136000000003 -120.878615,49.063873,1373.1152007999924 -120.879348,49.064203,1371.9545862399978 -120.87995,49.064643,1370.9842640000497 -120.880728,49.064935,1368.3792672000134 -120.882365,49.065112,1370.8040447999913 -120.883247,49.065504,1367.1828844800048 -120.88369,49.065647,1368.5197071999994 -120.883686,49.065895,1369.2219999999913 -120.883686,49.065932,1369.3551999999877 -120.883697,49.065996,1369.5855999999942 -120.883705,49.066061,1369.8195999999916 -120.883714,49.066115,1370.028000000025 -120.883711,49.066157,1370.3303999999778 -120.883584,49.066327,1371.5326547200077 + + + 0 + clampToGround + -120.883584,49.066327,1371.5326547200077 -120.883711,49.066157,1370.3303999999778 -120.883714,49.066115,1370.028000000025 -120.883705,49.066061,1369.8195999999916 -120.883697,49.065996,1369.5855999999942 -120.883686,49.065932,1369.3551999999877 -120.883686,49.065895,1369.2219999999913 -120.88377,49.065892,1369.211199999993 -120.883945,49.06592,1369.4509759999883 -120.884086,49.065912,1369.791841279988 -120.884232,49.065917,1370.3060422399863 -120.884274,49.065909,1370.4916553600192 -120.884604,49.066033,1372.293199999976 -120.884708,49.066134,1373.0312000000054 -120.884761,49.066295,1373.8940151999711 -120.884887,49.066435,1375.1554576000383 -120.884983,49.066487,1375.5676316800182 -120.885049,49.066492,1375.6942796800033 -120.88537,49.06655,1375.8605600000074 -120.88603,49.066688,1374.4455999999755 -120.886707,49.066806,1374.28723231999 -120.887464,49.066809,1371.1160070399992 -120.888084,49.066899,1367.7784473599506 -120.889031,49.066998,1364.4797635199811 -120.890516,49.067331,1366.0881638400444 -120.890601,49.06731,1366.1116000000038 -120.89135,49.066549,1365.5550080000066 -120.891761,49.066289,1365.3596000000086 -120.892274,49.066154,1367.0607801600095 -120.89354,49.065841,1365.3041343999878 -120.894071,49.065545,1367.6484000000064 -120.894614,49.065248,1374.9256000000412 -120.894663,49.064996,1379.2450700800155 -120.894575,49.064743,1381.8707559999777 -120.894407,49.06453,1385.3871184000143 -120.894041,49.064355,1390.9978544000053 -120.893603,49.064193,1393.5915999999518 -120.893291,49.064058,1394.2268377599955 -120.8932,49.063986,1394.78598399996 -120.893192,49.06391,1396.1098623999978 -120.893192,49.06383,1397.3391999999544 -120.893113,49.063706,1398.84039999995 -120.893053,49.063599,1399.9815999999623 -120.89297,49.06354,1399.3840000000387 -120.892978,49.063493,1399.4415999999956 -120.89303,49.06347,1399.8159999999712 -120.893223,49.06347,1401.8276896000461 -120.893265,49.063458,1402.4164192000258 + + + 0 + clampToGround + -120.893265,49.063458,1402.4164192000258 -120.893314,49.063444,1403.1693145600864 -120.893465,49.063359,1406.9128048001357 -120.893588,49.063341,1409.4229926398689 -120.893675,49.063338,1410.58147199998 -120.89377,49.063364,1410.9273024000356 -120.893888,49.063403,1410.9856051200077 -120.894177,49.063429,1412.0208116800077 -120.894438,49.063486,1413.1928492799889 -120.894576,49.063504,1413.5842201599685 -120.894698,49.063488,1414.5333529600373 -120.894936,49.063421,1416.3492857600359 -120.895133,49.063322,1418.1011350400051 -120.895343,49.063188,1419.6924473600327 -120.895612,49.062952,1423.5428940799638 -120.895968,49.062584,1428.509207039999 -120.896118,49.062485,1429.2159999999662 -120.896359,49.062389,1430.5983999999548 -120.896525,49.062317,1431.802387999994 -120.896624,49.062237,1432.8322284799642 -120.896687,49.062156,1433.991898239977 -120.896778,49.062073,1435.118980480023 -120.896842,49.061959,1436.2253497599625 -120.896893,49.061887,1437.671099360009 -120.897031,49.061778,1439.571200000093 -120.89732,49.061654,1441.195200000144 -120.897486,49.061571,1444.183199999967 -120.897589,49.061498,1447.1605468799928 -120.8977,49.061423,1450.4379840001013 -120.897937,49.061353,1452.8842276801322 -120.898091,49.061307,1453.5652000000914 -120.898301,49.061224,1454.2851999998595 -120.898491,49.061159,1455.6105457598364 -120.898645,49.061138,1455.8250096000058 -120.898811,49.061122,1455.8774083201117 -120.899163,49.060972,1462.009600000018 -120.899262,49.060902,1465.1184390400492 -120.899305,49.060801,1468.9896328000655 -120.899313,49.060724,1471.3350515199772 -120.899404,49.060612,1474.4904140800177 -120.899484,49.060603,1474.5155219199853 -120.899564,49.060625,1473.677199999959 -120.899703,49.060741,1470.3498020799932 -120.899842,49.060804,1469.170400000089 -120.899989,49.060835,1468.9239248000772 -120.900242,49.060809,1472.2268000000872 -120.90046,49.060739,1476.7784223999824 -120.9008,49.060607,1483.039872000114 -120.901357,49.060376,1491.0767865600826 -120.901484,49.060322,1493.248800000027 -120.901674,49.060275,1495.1202640001216 -120.90267,49.060107,1498.609999999883 -120.902967,49.060063,1501.124703840037 -120.903208,49.05995,1507.1511999999436 -120.903509,49.05995,1506.6999999999668 -120.904007,49.059739,1517.1832000000263 -120.904327,49.05969,1516.645289599873 -120.904663,49.059612,1516.6761977599353 -120.90498,49.05948,1519.48678400004 -120.905339,49.05928,1525.737446399981 -120.905686,49.059033,1528.7507715199533 -120.905929,49.058798,1531.1049753599068 -120.90602,49.058547,1536.8686048001343 -120.906182,49.058137,1546.1231999999145 -120.906391,49.05793,1547.1100704000455 -120.907074,49.056788,1569.096029440236 -120.907198,49.056526,1574.5302419201744 -120.907325,49.056433,1574.7033320000494 -120.907609,49.056033,1580.8346857601425 -120.907712,49.055735,1588.0493727999985 -120.907657,49.05558,1593.5506624000243 -120.907633,49.055481,1596.3138900799436 -120.907665,49.055393,1597.9667912000207 -120.907736,49.055282,1599.50811392004 -120.907831,49.055093,1600.511199999974 -120.907914,49.054945,1601.0475999999508 -120.907933,49.054834,1602.6352000000097 -120.907823,49.054756,1606.4152000001864 -120.907578,49.054717,1611.6847209600423 -120.907431,49.054714,1614.3973526400098 -120.907249,49.054743,1617.0764107200803 -120.907083,49.054766,1619.8246326399778 -120.906989,49.054871,1618.4749307201103 -120.906854,49.054679,1626.4929267200641 -120.906902,49.054513,1630.1137779199532 -120.90687,49.05443,1632.867999999983 -120.906731,49.054533,1632.8888681601475 -120.90653,49.054633,1634.0187391998425 -120.906451,49.054565,1637.649915200003 -120.906435,49.054491,1639.9411567999973 -120.906435,49.054302,1644.1616751999659 -120.906462,49.054178,1646.186738559892 -120.906538,49.054085,1646.2275616000456 -120.90666,49.053986,1644.776819199914 -120.906818,49.053864,1643.8951718400456 -120.906917,49.053665,1646.4892655997803 -120.907036,49.053312,1655.1565132799208 -120.907116,49.053186,1657.6907750400744 -120.907115,49.05318,1657.946927999971 -120.907108,49.053179,1658.1727372801627 -120.907008,49.053204,1659.9321292798388 -120.906763,49.053354,1661.4330019201038 -120.906497,49.053567,1660.953014879915 -120.906435,49.053583,1662.3659975998412 -120.906443,49.053484,1665.1750214401925 -120.906589,49.053144,1673.68417535996 -120.906799,49.052887,1676.0431999998582 -120.907004,49.052675,1676.770399999956 -120.907249,49.052465,1676.1578536003165 -120.907566,49.051905,1682.1606783999807 -120.907882,49.05148,1686.4467968000267 -120.908198,49.051027,1688.3456000000933 -120.908463,49.050628,1692.4110828799976 -120.908598,49.050247,1694.9899702400799 -120.908631,49.050148,1696.3782969600038 + + + 0 + clampToGround + -120.908631,49.050148,1696.3782969600038 -120.908724,49.049863,1699.6211999997997 -120.908855,49.049619,1701.4098647999408 -120.909037,49.049394,1702.9992873600538 -120.909321,49.049267,1700.9774827198758 -120.909484,49.049192,1699.6236262399398 -120.90959,49.049086,1699.6798304000654 -120.909751,49.048826,1700.5670899200304 -120.909855,49.048707,1700.2580112001506 -120.910235,49.048539,1694.2220000000311 -120.910405,49.048469,1691.7993744001008 -120.910639,49.048427,1687.9859051199999 -120.91135,49.048445,1682.784279999986 -120.911595,49.048422,1683.8267999999725 -120.911883,49.048381,1686.6616420800408 -120.912152,49.04837,1690.8787392001393 -120.91259,49.048388,1699.8491263998392 -120.913078,49.048619,1702.5961052800035 -120.913125,49.048631,1703.7684999998708 -120.913169,49.048626,1704.6429097599053 -120.913204,49.048377,1710.20521535985 -120.913234,49.048359,1711.0415155200865 -120.913273,49.048377,1711.2769123200173 -120.913491,49.048465,1714.60319999983 -120.913693,49.048512,1718.7631692798318 -120.913824,49.048524,1721.6470860801157 -120.914006,49.048596,1723.9103999999256 -120.914333,49.048767,1728.2351999999983 -120.914395,49.048789,1728.8255999999913 -120.914439,49.048786,1729.3116000000177 -120.914726,49.048765,1730.9177311999892 -120.915043,49.048731,1733.7803532800244 -120.915356,49.048815,1735.4042112001182 -120.915898,49.048908,1740.5815667200573 -120.916376,49.049009,1745.8563366400554 -120.916737,49.049183,1749.6288881599498 -120.917004,49.049401,1752.4727321600767 -120.917143,49.049624,1751.7715999998686 -120.917353,49.050044,1756.6378252801771 -120.917455,49.050375,1762.110700000118 -120.9175,49.0506,1763.84000000009 -120.91761,49.051074,1764.382294399895 -120.917714,49.051027,1767.5355228801125 -120.917977,49.050677,1775.5831999998322 -120.918072,49.050515,1778.560399999887 -120.918131,49.050336,1780.691600000057 -120.918244,49.050213,1783.748800000035 -120.91837,49.050129,1786.5893007999334 -120.918448,49.050034,1787.7017267199642 -120.91848,49.049942,1788.8369536000143 -120.918458,49.049893,1789.163122240049 -120.91833,49.049788,1788.9504831999282 -120.918293,49.049468,1791.7045740801748 -120.918281,49.049246,1794.4042070398593 -120.918474,49.04912,1797.5890751999762 -120.918523,49.048983,1799.435067359987 -120.918684,49.048758,1802.360000000016 -120.918818,49.048666,1802.720000000028 -120.918861,49.048585,1803.4171127999211 -120.918752,49.048158,1808.708221440054 -120.918448,49.046224,1818.0208000001517 -120.918358,49.044084,1826.4680806400936 -120.918873,49.041482,1846.8523999997117 -120.919203,49.039757,1835.936800000176 -120.919428,49.03876,1809.4122624002796 -120.918834,49.038903,1830.308438079858 -120.918615,49.039329,1848.0126184000017 -120.918079,49.039822,1855.3174505600593 -120.917543,49.0403,1859.951583999955 -120.916319,49.040933,1852.5837259200725 -120.912608,49.043058,1822.4085785601897 -120.911556,49.043718,1816.8130086400706 -120.911063,49.043705,1805.53042480011 -120.911049,49.043714,1805.7220643201647 + + + 0 + clampToGround + -120.911049,49.043714,1805.7220643201647 -120.91074,49.043915,1811.8155839999854 -120.908959,49.044703,1816.6935999997809 -120.907393,49.045532,1831.200535040091 -120.907006,49.045873,1838.2064889599899 -120.905955,49.045673,1841.8107727998522 -120.904475,49.04542,1875.3953599998738 -120.903895,49.04542,1881.6805280000015 -120.902136,49.045673,1883.901962880048 -120.901535,49.045462,1870.3891568000658 -120.900828,49.045379,1870.4659999998353 -120.899584,49.04559,1870.0059904000393 -120.898511,49.045449,1861.5707999999352 -120.897652,49.045548,1873.2616192000162 -120.896149,49.045898,1891.4585539200395 -120.895269,49.045954,1887.8962729599593 -120.893493,49.045502,1858.100021440046 -120.892866,49.04549,1849.2324928000023 -120.891772,49.045603,1841.7489958400236 -120.889969,49.046193,1835.3178990401184 -120.887952,49.046967,1879.0389427200757 -120.886987,49.047459,1896.5066862399922 -120.885588,49.048206,1915.6797644800279 + + + 0 + clampToGround + -120.885588,49.048206,1915.6797644800279 -120.885485,49.048261,1917.7759751999092 -120.884562,49.048444,1929.5752115200496 -120.882824,49.049273,1969.8062041600094 -120.882116,49.049667,1982.709978880004 -120.881472,49.049653,1977.2777606400145 -120.879112,49.049442,1934.5377318400201 -120.877674,49.049372,1918.2329651199743 -120.876515,49.04902,1920.1205599999796 -120.876172,49.049175,1927.9110719999842 -120.87467,49.049358,1937.458188799959 -120.873039,49.049681,1943.981555360036 -120.872224,49.049639,1939.9053651199813 -120.870955,49.049106,1926.0897632000701 -120.870334,49.049175,1920.1289440000553 -120.870315,49.049048,1919.594531200078 -120.870265,49.048923,1918.7587024000634 -120.870379,49.048795,1915.7380655999366 -120.869929,49.04871,1908.403031999974 -120.869948,49.048602,1906.590420480018 -120.869647,49.048551,1898.3176067201387 -120.869438,49.048307,1886.4685980801053 -120.8688,49.048323,1880.0490080000561 -120.868153,49.048267,1874.0134289601085 -120.867599,49.048126,1858.6119999999928 -120.867164,49.048142,1847.850400000041 -120.866945,49.048064,1844.9776000000295 -120.8659,49.048136,1829.8487039999263 -120.865186,49.048373,1819.9055055999822 -120.863553,49.049043,1823.4781283199566 -120.86186,49.049583,1814.2389055999313 -120.860315,49.050666,1802.6572864000018 -120.859136,49.050922,1785.215080960138 -120.857622,49.051596,1755.6808000002684 -120.85747,49.051395,1747.0300000002196 -120.856195,49.051608,1716.5982176000186 -120.855272,49.052072,1707.9359027199935 -120.853899,49.052578,1689.2643155199348 -120.85347,49.052733,1686.0717104000485 -120.853448,49.053042,1684.1701107200208 -120.853341,49.053211,1678.7495100800604 -120.853105,49.053197,1675.6719248000331 -120.852719,49.053028,1669.7855814398715 -120.852354,49.053127,1663.8976000000775 -120.852182,49.053492,1657.9315097600165 -120.852463,49.053661,1658.5140772800698 + + + 0 + clampToGround + -120.852463,49.053661,1658.5140772800698 -120.852697,49.053802,1657.4084755202084 -120.853878,49.054252,1638.687157760066 -120.85465,49.054238,1634.540864000043 -120.854993,49.054041,1639.993400480202 -120.856045,49.053802,1638.5097680001927 -120.85789,49.054434,1612.9343711999359 -120.858512,49.0548,1604.465792000131 -120.85922,49.055334,1583.5245567996574 -120.860336,49.055644,1574.067199999893 -120.861087,49.055489,1567.7432027200089 -120.861388,49.05518,1571.5983999998698 -120.861581,49.055194,1566.7711999999688 -120.861259,49.055939,1541.9463748798091 -120.860937,49.056052,1540.2581830398249 -120.860354,49.056227,1534.8678390400103 -120.859681,49.056243,1529.2997809597782 -120.858987,49.056253,1518.226029120104 -120.857957,49.056045,1516.3364000001482 -120.857423,49.055981,1516.8943190398093 -120.85696,49.056024,1511.7216767999564 -120.85661,49.05612,1503.914943999979 -120.856181,49.055913,1520.5842411201118 -120.855684,49.056538,1491.27935103985 -120.855298,49.056823,1484.2963436797745 -120.854895,49.056852,1485.6945984000395 -120.854493,49.056966,1476.8284710398445 -120.853895,49.057008,1470.8238335998099 -120.853449,49.056987,1465.7202075199898 -120.852838,49.056896,1461.0487999999873 -120.852421,49.056759,1462.400669120107 -120.852255,49.056757,1460.5172528001315 -120.851287,49.056963,1447.8975999998188 -120.850664,49.057012,1437.9135999999091 -120.850085,49.056901,1434.7665415998688 -120.849188,49.056851,1434.869035519866 -120.8487,49.05697,1431.4268800001757 -120.848355,49.057126,1422.6582576003066 -120.847998,49.057217,1412.2174092799007 -120.84743,49.05726,1404.891456000078 -120.84684,49.05727,1397.970015999769 -120.846177,49.057145,1406.903941600116 -120.845563,49.05723,1398.4928496002053 -120.845178,49.057349,1386.0555388799132 -120.844313,49.057505,1368.8097176000526 -120.843659,49.057485,1358.7446192000564 -120.843243,49.057397,1357.0695999998957 -120.842747,49.057253,1357.3653532799144 -120.842123,49.057028,1350.3743084798987 -120.841792,49.056818,1349.7690137599425 -120.841458,49.056239,1353.2038524801067 -120.840857,49.055481,1344.5262646399888 -120.839652,49.054368,1327.852029440008 -120.839445,49.054074,1317.6877327999146 -120.839355,49.053845,1315.1751239999412 -120.838803,49.053208,1300.3123110399426 -120.838761,49.05278,1291.8252000000984 -120.839124,49.05206,1289.0991999998714 -120.839622,49.051893,1298.3883798401014 -120.839341,49.051637,1288.3111446400872 -120.839396,49.051385,1285.1812832000126 -120.839618,49.05112,1279.7672063999432 -120.840018,49.05055,1267.0670080000177 -120.839467,49.050719,1267.6450219200133 -120.839331,49.050761,1268.029200000035 -120.838362,49.051389,1265.7227174400919 -120.838304,49.051516,1264.9779225598952 -120.838339,49.051663,1266.0893385600798 -120.838304,49.051819,1265.6355110399227 -120.838214,49.051891,1264.6285449599227 -120.838123,49.052158,1266.4081292799162 -120.838059,49.052471,1268.7599054399939 -120.837921,49.052926,1267.9155241598055 -120.837883,49.053171,1266.4731414401692 -120.837811,49.053352,1263.9056742400699 -120.837771,49.053529,1262.1175873601032 -120.837799,49.053607,1262.7765385601435 + + + 0 + clampToGround + -120.837799,49.053607,1262.7765385601435 -120.837861,49.053777,1263.801600000159 -120.837986,49.054038,1265.6176294400445 -120.838016,49.054232,1263.9534924799054 -120.838056,49.054398,1262.6869350397926 -120.838097,49.054553,1262.917907360205 -120.838067,49.054663,1260.9120161598125 -120.838031,49.05479,1259.4268304000188 -120.838024,49.054955,1259.7204032001518 -120.837951,49.055032,1257.3227225601288 -120.837849,49.055107,1254.1368694401845 -120.837767,49.055128,1251.7454790400166 -120.837673,49.055218,1250.979626559958 -120.837622,49.055248,1250.4822822399317 -120.837462,49.05531,1248.7056000000184 -120.837438,49.055324,1248.4968000000595 -120.83733,49.055378,1247.52479999993 -120.837175,49.055493,1246.6048000000017 -120.837067,49.055602,1247.0165686400242 -120.837029,49.055674,1247.7133961600455 -120.836966,49.05575,1248.2319200000602 -120.836916,49.055819,1248.7480838400036 -120.83689,49.05589,1249.7840159999514 -120.836804,49.055971,1250.7218246399968 -120.836785,49.05604,1251.7189440000857 -120.836702,49.056088,1251.9110169600556 -120.836615,49.056169,1252.2307999999598 -120.836523,49.056319,1252.9796000000133 -120.836461,49.056419,1253.5847999999805 -120.836461,49.056634,1255.9068000000268 -120.836595,49.056661,1256.680799999991 -120.836657,49.056678,1257.0467999999944 -120.836707,49.056713,1257.5997806400403 -120.836452,49.05676,1256.8991999999578 -120.836075,49.056802,1259.4566640000783 -120.83605,49.056805,1259.77044 -120.836186,49.057122,1260.3033433600144 -120.836291,49.057422,1260.776399999979 -120.836307,49.057643,1260.5213630399396 -120.836197,49.057907,1261.2980000000207 -120.836002,49.058195,1261.2568144001489 -120.835708,49.058457,1259.493525760141 -120.835574,49.058436,1262.3661414400872 -120.835445,49.058474,1263.0293056000107 + + + 0 + clampToGround + -120.835445,49.058474,1263.0293056000107 -120.83519,49.058549,1263.5822848000619 -120.835028,49.058629,1262.9463084799636 -120.834661,49.058637,1267.842941280116 -120.834214,49.05895,1264.653023999823 -120.834036,49.058982,1267.9209561600562 -120.833265,49.059391,1273.4817392001717 -120.832745,49.059535,1285.7609360000279 -120.832206,49.060049,1279.294400000042 -120.83163,49.060394,1265.6488575998308 -120.830962,49.060512,1270.035999999858 -120.830656,49.060778,1263.233569279983 -120.830202,49.060814,1266.6156262399768 -120.829914,49.060875,1266.359599999856 -120.829588,49.060858,1268.2429836798365 -120.829174,49.060875,1269.6971999998814 -120.827895,49.060937,1263.3910903998442 -120.826774,49.06083,1270.6531263997751 -120.826493,49.060858,1271.0280822397422 -120.826168,49.060829,1276.8183846401234 -120.825387,49.060737,1271.604229439855 -120.824915,49.060733,1261.3063816001197 -120.824394,49.060807,1256.9727372803561 -120.824345,49.060877,1252.126277600056 -120.824362,49.061395,1245.0219999999908 -120.824449,49.061657,1245.9487999999835 -120.824449,49.061739,1245.9878705599967 -120.824501,49.061793,1245.8889972800052 -120.824594,49.061854,1245.8246969599882 -120.82465,49.061918,1245.9295519999944 -120.824734,49.062114,1246.6362809600184 -120.824851,49.062237,1247.5453364800019 -120.825485,49.062002,1245.668399999996 -120.82567,49.061975,1245.1746799999808 -120.826028,49.062,1245.1999999999914 -120.826252,49.062027,1244.9407398400112 + + + + + + diff --git a/docs/release/fixtures/fatdog-120-gaia.xml b/docs/release/fixtures/fatdog-120-gaia.xml new file mode 100644 index 0000000..44ae02a --- /dev/null +++ b/docs/release/fixtures/fatdog-120-gaia.xml @@ -0,0 +1 @@ +2141Cathedral Aid Station Minor Aid Stationred-pin-down.png1810Trapper Aid Station Major Aid Station. Relay Exchangered-pin-down.png852Start Line120 mile Start Line red-pin-down.png1190Ashnola Aid Station Major Aid Station, Crew Accessible, Relay Exchange, drop bag red-pin-down.png2046Heather Aid Station Minor Aid Stationred-pin-down.png1244Finish Line Major Aid Station at finish, hot food, first aidred-pin-down.png1192Windy Joe's Aid Station Major Aid Station, Relay Exchangered-pin-down.png1370Strawberry Flats Aid Station Major Aid Station, Crew Accessible, Relay Exchange, Drop Bags, Pacer Exchangered-pin-down.png1012Pasayten Aid Station Minor Aid Station, Drop Bags, no crew accessred-pin-down.png1796Blackwall Peak Aid Station Major Aid Station, Crew Accessible, Relay Exchange, Drop Bags, Pacer Exchange red-pin-down.png953Grainger Creek Aid StationMinor Aid Station red-pin-down.png1791Nicomen Aid Station Major hike - in aid station (no crew access) red-pin-down.png1021Bonnevier Aid StationMajor Aid Station, drop bags, crew accessred-pin-down.png1396Calcite Aid Station Major Aid Station, drop bags, no crew accessred-pin-down.png1609Hope Pass Aid Station Major Aid Station, drop bags, crew accessred-pin-down.pngFatdog 1204ABD32851.8495999999386852.6443200000434853.599523840069855.1471999999535856.5440000000588858.5959999999923859.5018553600423859.7518336001073859.7518336001073859.7671264000688859.5018553600423858.5959999999923856.5440000000588855.1471999999535853.599523840069852.6443200000434851.8495999999386850.9436383999773849.1512905599872848.2340940800298847.790400000011847.5708000000027847.3799999999984847.1207999999878852.5490111999321853.5476079999897853.8519059199965853.3791945599385853.8760329600581856.0271452801512858.7023859199929859.682504639827859.682504639827861.3939360001707863.4104012802308865.6678796799433871.8484799998027879.2137516800129884.728983999982891.9305656000633898.6044404800786901.0968268798529902.4032000001648900.8402729599619907.5343999999596911.9331903999948913.7591999999963917.3039999999619918.0009497600694918.0009497600694918.0009497600694925.6567783998107936.9060824001699946.4330892800623962.5281551998196964.046000000074963.0498751998429975.2050680002397989.4595999999209999.37412384000381005.05600000017391014.86245439982451020.34407232008581026.7126297600061039.51619200004621046.62240000010431051.60480000011011055.68000000002441058.97234367993771063.29440000001571066.4119999999971067.54599999997341072.09727807993231073.18615936004381077.98575184002581089.25572672002551097.22274751992361107.5074480000241107.5074480000241128.79519999987681129.25599999994121127.3356000000871124.79760000020361120.9168000000341119.24640000000551118.01482591998731116.32616959990261122.5119999998381134.91807711997261142.5216000000151148.7837308800191150.31317776010841152.63402720000861155.13899359999051158.1066790397941160.12947007981571161.08067840009881162.3676182401561163.51279999998721165.4892000000111167.40440000003451170.46468608001651173.52399999996211176.0412841601071181.06369472020831197.10080000020121207.27375871983671217.81097391996061226.33800959985021231.34350720012121237.72023823997551241.3336734401041240.02201663992151243.3740521601771251.4549526400711255.72471679996471268.007452799841285.191891360171289.95482239993981293.3497222399781296.95981631996571302.60760000007461309.6636633599111314.67319999987241317.11039999986131321.23182848000221326.626766079991335.7611763199081339.2667684800061343.17285392008671345.5501743998981349.5541120002671361.03427775984321381.19278207978411388.24624480029621391.4313702398711392.35439295980721395.36239999987951400.73754112009671414.70641039991531419.82105343996051431.95200000008981442.2118105597551442.2118105597551475.31200832007651481.92450144008581480.75993312010361483.05149248004681493.10384816002831499.63415759982381504.17362399987361518.83139311998461514.65479040011471506.20254399991591503.53882815971021506.31846463999661510.5539225599981523.3475051199111530.63633856007531531.32637696020271544.92784991995521568.1628000001411592.26399999993991607.62973360008781617.33049376011921622.6802902401021630.3371007999991632.2602927999921629.01155456000041623.4518800000411627.23280000001931641.26568447984231654.55846080005241664.6151638399851674.08039631996421678.82232288014531684.63003999994861696.58857087992711698.30164767985841703.28307135996281705.78458687997321705.78458687997321706.57137120009661710.06755999998061716.14880000003771732.44639999994051744.91139456003251746.39616863993681752.54834560015371749.23208671983141751.21382591999651751.12768224011141747.246931200021746.24280000006931747.71671680000781747.70639487996751747.00094463997631745.61014560010931746.18413216014141749.57105920007941752.02240000000071758.4987999999821759.26199999999881759.7299999999941761.0223999999891771.04135456005681776.36737280002561776.36737280002561779.07640000012681798.67319999995471807.99999999991951814.9897456000151817.80280000008841820.75270400000891821.96389888014691823.48953728020251839.45180064023341850.61599999988971859.98295904014121864.8006012798511871.1508000000391877.2542099202291884.85600000005341898.56342799993631902.078355999881905.47800000007781909.52269087994981912.15840000005231918.7971193600171920.93960000000021921.83239999999571922.97181696002371925.14070080004381926.63672592011928.56978944003441928.08840000002921925.9860756799631923.35306479990961920.39172864004381922.3384620800181923.95136063992141919.16547711995261917.28079999996231907.96463711998471907.59882527996871907.59882527996871905.662800000011903.81870432003281899.5776000000121899.48038656001361899.08464000003321900.17759999997541901.90640000002441903.84841407998151902.63077359993551899.8191999999651897.18200000001621892.76107760004831892.04942463999561892.53379391999671895.87799999991131900.4279999999641904.53266591999361905.61320000002521907.22240000002941909.97639999998271918.12600000002771920.07247680001821921.56999328001731923.54799999994071927.92946559998431937.91680000000181939.04322688005481941.1278918399551942.3876000000031947.95825631996171948.79273567999921951.0572000000151952.89679999997471954.72560000001321956.60200000001121964.28320000000621965.43240000000151966.30719999999251968.52960000006081972.53239999999031976.1474857600091977.66156416000541980.37472624007481984.10160000001571988.08109999996331990.61235391999341993.65893024000231994.90319647999581995.04647472000652002.19559999997752006.40342400003282012.31520000003522016.3163999999762018.90022719994162021.42279808001422025.09000000001012026.03595807998892027.87520000000592028.78960000002482028.78960000002482031.43198208002452033.56960000001692034.95200000005662036.87119999992662040.04767376002162045.5071999999412051.33767808012132057.31252367995332071.2643150399422075.13505999998872078.00214032001352082.51551040000552090.97400000003562097.4628000000832109.64301504002652116.9744127999872120.08942848002972123.936000000042136.947911680092142.6144000000262147.431999999972156.41535999994042162.73599999993852163.65071104002252166.04476767993262168.653549439972169.95879999998852172.02800000000842175.3039359999962179.90863375998152190.4926999999622188.1685854399122178.54702688001862176.1535999998982176.1535999998982164.46029023999842158.23907840005862150.52599999998262146.99878016000782144.44284864002432142.80871616004422141.304358240032133.10560000000442128.6894055999632128.5279999999292128.394800000012129.25039999996032131.67817520005932134.5863879999232139.0943999999112140.9654806400892148.39189408006222149.371310079972150.43600000001472152.2000000000632156.065273599912159.2310128000932160.2328208000912161.06242111994922164.31620047991372165.2332097599552168.7310086399252171.14380351984032173.63981968001832175.5085193600912178.14681823996172180.20884783996552182.1226144000252184.17732735989652187.1242982399732190.30590160009072194.32682303984572195.9863078401062199.80181248001742201.5671315201142203.3218060798532204.79818047982742208.102588800122210.11236367977242211.9608537598612214.11528511994672221.83705615996362225.9906563200372230.358799999892232.88522799994872243.4636607999792258.57327487984372271.1220000000092278.4451999999512283.4736588799412292.41156767990972292.41156767990972294.15853888000632295.83327423995162300.51837376003372304.10150047990372304.19947200011762310.3473599998852322.80640000008732325.00157823999552332.26000000002752317.7951999999552311.45637776003062271.17538399993052267.40582208003252264.9676399999862253.65622591991272253.09759999996642256.12880000003042266.95616383996282262.9528089600042262.9528089600042263.0693496000242253.45920000007752240.72601984002452226.50327376015732214.5656179197822205.25613568004162207.5319999999112216.16236479993272214.90764480018832211.8687612801342214.28560000002562210.98589008013232213.46046655995772210.0924531200632211.10446159997952209.4174460800632212.0452851200822198.480712960052194.5600735999752189.3081420800482159.12300800000232161.26967615999142162.72542720002642159.3103999999432159.3103999999432151.73120000002842150.48601088000442145.1855999999482133.64920000004352129.42368320004562121.68879999997442111.48611328009062109.7151897600642097.7240000000222090.71283520000952080.48600000003352079.84519999996342076.83627520005352075.90020607995672068.7388207999962065.45362399999842065.11796863998822061.81245935996732053.53919999994652046.0440736000082050.45399999997242047.5006169600242047.48680000005452051.0606396799642056.379600000012056.379600000012060.623096959952068.21611247997862073.49400000004652083.08231519993432085.40759999989772085.99146463999662088.86419840006242091.28392239994172099.3719999999762101.56064480001762101.84065440003222102.55703999999652101.85058240002542093.361019520092089.8218009600022078.38893824007252075.0026726400812072.2239999999222068.0772000000552061.7359999999742060.5737079999532053.8683679999482049.27804399987552047.16560000006282046.29079999999392046.37773600002582048.38560896002762047.84962959999782042.26185760001482036.3467347198972034.2443999999682033.2868000000582022.6465543998842020.3821414399042019.56564992011112017.85606400003052005.89992064013152005.70878080005881984.42509760003281970.28519391984921960.95782400010171950.14040000004871932.75927999980261932.75927999980261931.1888476799051922.46252127980351922.78171648012811917.0199436800891907.94947104005151898.16920000001281885.38577856015721874.01068352010721857.00209888005951842.90555967997491825.7736268798021830.34887359986851828.3461359996271820.13200000004261824.17667775986821820.41358623994871809.49240000020521798.65595136019121791.69878303998581794.44301984020241786.52466816011341777.90316800000941764.58320000018421768.66919999992111771.81090400003041766.10942528015581763.25176399992461762.39953600014681751.03878080009461743.28289232009161739.72940384000051740.80131679986931740.4705848000491738.40608048012311737.3505446400581735.10354655965651735.68762304016151732.87056447994381727.93879999984941728.78404224013661712.15719999989371711.04559999979411709.22353024007451709.22353024007451708.90111999987081683.98216127998881681.87949920000231680.48508240020061667.80044704000781669.56972032003551665.37960000002271649.21318960009651648.1808000000721641.37598480004771636.7970268801021627.11599999990811617.0716000000961611.43039999992541607.80721040006161601.38599999987951602.60577024011761604.0035999999891600.5472000001451597.82559999995651597.02511360009841588.52720000006231583.64680000010981583.57046992008941579.76438591996661575.28360000007141572.72039999994261568.86969760006221566.97708303995141562.73479599989511543.88720000005291545.20839999999131543.98799999990071543.78042880010341524.44926143999531513.85480000013831508.52998639993261505.37800000011071508.22245008006581504.17160000006651501.16541808016651496.5179999999741492.21906239996681482.55553311997161474.3996000000861475.72626240000731475.84664639998751465.77704479994961459.90480000012531457.5192000002081457.5192000002081456.34760000010531444.06867327991861446.1055791999841443.24366720006131437.44116271979151415.68949375995821404.80931648010441393.53919999989671381.72268992007281380.52090400005731365.02398975992331365.9278182399171363.75770944011011353.61974144005011351.91526240002941351.11462239994941340.48622752009621336.99986544006241335.723082239861329.93927199999871326.83360000003271326.95931039997961319.47035072003841305.00865919982331291.7790468799581280.07139327994741271.205840959961260.9234854398691246.33849296013071233.00351424007731223.72553599999561223.72553599999561222.72469855995811216.42440000002721211.53770896001421206.82825920003571198.39654463998641192.09359999989971188.93626752002051188.12173823998361190.99760959995461190.99760959995461188.47720000003441195.03315520000361195.10278655997721192.90726783999071194.1840207999221192.5842556799721198.90111839985841203.69639999991251204.47759999998651203.14880000000811195.45888959999271195.94080000000331195.77444319998361196.00802032000121202.1848000000091202.54119999997871202.54119999997871202.6372297599781203.25400000002081205.17799999996491207.1040000000141208.59799872002491209.36457840002191211.7708000000231211.08720000001361209.2295999999991213.68587135996561214.66368319998041215.01213.81972127996071214.61202944004251214.5283136000431214.9486063999781215.18912960004511215.28672415996721218.00111600003951219.09791359999491220.3903352000321226.70879999985121234.78877919990371236.72497152003191237.8530015999981251.92245760009251252.49047424004361252.49047424004361250.49440800007781257.09279999998221261.13212480006971272.44510976019771284.38920351991941302.78078592013641316.32631919992241316.50476000000371319.30442368005081317.40921311988471320.33245119987711320.2856540800531320.2856540800531320.33245119987711317.40921311988471319.30442368005081322.24713647996371328.27960000001671356.0429535999791364.24094080003671378.84826239992731383.29817487980431395.35573056012571396.5025407997591398.42705183987411412.28268000006851413.6509247999291414.71139200013891422.4734312001481423.7468462398211425.05794751986061432.04410495972071450.10443455978321483.79041008018931493.77720000020871517.73606287993151541.00724000019041545.9923976001331575.23250047988361581.42240000001041583.63082239991421585.79208959985681607.48239999999421620.02688240001771622.641105920061624.29111552024781635.4126908800041640.28960000010151640.48999999988771640.64124095999951655.64219008017431662.8831907200051662.8831907200051665.44525120009731681.16262912013241681.83364399992121685.07360000010521693.36199999989141696.24479999999811699.88799999995721698.28051647998061695.6472000000231699.27789056001421702.7655999999871704.14919999998261709.548800000061713.37079999998061721.01977248001871728.22067839995571737.36686224000051745.9111875199261754.5197968000871763.103113600041764.12974143994231766.48383167996761766.3667999999721767.28408351999361775.74716160002141784.25505919993321784.25505919993321793.2016000000641802.3648703999311806.6903743999391806.97520000004651803.46720000002731809.28799999997071808.03119999998221809.1101491200251812.18760000000161811.92479999999981811.87199999997621814.23520000000741814.40719999997331816.80193984003541817.81760000003351818.1119007999821819.51639999998721821.4441011200231821.99128240001161821.91629311998671822.1546320000221822.1546320000221823.0359999999971822.4721048000081824.38564928003551827.69267360003731831.52477455995151831.73639999999891831.4198985599811831.50094479999031833.85826815995731835.24000000001431836.64174464000231837.16549024000321837.64639999998371838.58547008001781841.32593200004071842.59082879997531842.19573247996071847.4902348800061850.40593679999691852.9833616000021857.66000000005281859.717454080011862.43560000000751864.40079999999971866.68813824000131866.7721982400061866.34726000000981866.97480000003681867.18639999999351868.0359999999971869.56960000001321871.03298751999861869.87645567993791871.0212000000631872.12899088003631877.45366656005351880.625974559961885.71086912001921890.38204575998091890.38204575998091893.0535999999831891.67827039998841893.77071551998421896.80764672001961896.60760000000351896.60574463999611897.71600000006131899.56199999997171898.98175231996171898.58159999998451897.85039999999191895.41840000001431893.11439999999951892.4983999999971893.2687999999961893.81811600002741896.5755999999921897.22840000000751899.34951488002571903.12679999998931907.08368383995411911.82920000000471913.0569873600031912.959180800011918.77439999994521924.49867936000031924.49867936000031928.25320000001331930.66799999997221934.29478399997951937.792799999991940.01941.24164352000071943.21239999999171945.00181184000031944.7159910400081948.05223760001831951.9970537600011951.79320000000231954.12227840000561956.2512000000121956.58119999999461955.0354240000021955.51979967999391956.11887424000381956.52597376000381957.03160000002531957.6723999999931957.67441600000831958.21078719999971959.08999999997541960.01519999999281961.23531600002321976.09760000015381976.33119183995131976.33119183995131979.79918207995621982.2524544000261985.73600000006631988.25237759995051992.82665407990981991.11565999996471993.43790975996651993.38599999999381995.0628761600421995.03830256006131993.54288000002881990.01001056001561984.21760000010641984.38982272008181986.30971840002531986.1776617598781984.6725107200311985.1658604799311983.88640000006671983.90450640000241985.21253328002671984.42707840005621983.46066928001161978.56257727999881972.10067664002211972.10067664002211971.33624832001851968.17444640001081966.37936832001381964.12200000000481964.31764447999741964.92287935999521966.23570047999831966.42560000002481966.46879999999241967.01967.01967.01967.25090895999531968.0223999999921969.7004000000051971.92239104009951975.96243552001421985.56076096004241988.02237759998731989.9251172799871991.07513599999861992.21180864001331993.2431999999581995.24966160000351997.69922816003992001.7076000000832005.50519999993872010.92640000002532013.38880000004972016.3823999999642017.64040000002612018.39362944000412019.08977151995352019.08977151995352022.9615999999832025.22240000000492028.08151952002852029.04184399994352031.10564015991992037.378399999982041.39951231996222043.04706751996862043.82840000006562048.61439999990132052.6909521600322058.7237264000242065.32186528005742068.61479999995842080.02080.20620560000272085.1264433600172096.8709939199842102.77943215995262102.77943215995262103.68043296001772111.11839999999352114.11227648001342119.6031999999292124.50601408001472149.16619519996682153.6950976000342156.56744351992942169.3984000000332174.6855999999972180.03115599991272185.9003609600132188.70533631995162188.70533631995162197.7556787200352201.28342959997742202.5279999999922201.98192544002682212.47360000004572215.1307999999982210.6785856001032208.04100095989132210.85364736021942204.3432404798072203.3396563198752198.446402560082193.595078399852187.60600000013572180.51039999990832177.68800000007652173.8176115199962166.18799999988372163.89840000003732162.9618886400542161.849200000042161.849200000042161.51439999996462147.96151839997132136.09799295995662123.9326822399692122.27280000008252121.53471392007852117.8714944000132118.7751270399552119.9812390401152122.71476416004232120.28318592000372121.1961599998592119.17936704014442116.8656262401852118.1464000001482123.82489919987852117.1358694399892118.04894080007762121.96321360008052122.4812505599172125.61566400023552129.03197328018722126.2079999997652128.33199999983342127.33200000001762123.09415248021332121.42607231986632119.7475124798722119.71682304004342120.61004159989672120.37663487968672123.98930079996762126.20440000013832131.12000000030962130.5569542402332131.16099327981962129.8854360000312130.9285214399482126.4204891199312127.45598144003542127.6212819199262123.83636255992722122.4212505601052114.1504216000672114.5074118401682112.4818358398612111.16397567986772112.9559987199872116.12399999999252116.9047407999012118.88439999992352119.5335691199472115.81022079994052118.29760736005072116.71479999990332117.32817823995672115.32498463999172117.0048678400972118.6210835200742120.3164835198942118.5084000000792117.25640000006522117.274400000022117.28611536013162118.8427366399482118.8427366399482119.939744319992119.5701715199762121.41745792007942119.6739999999122115.9143040000382106.52492608005742101.32516480002232096.93280000005552092.70469216000672088.9289087999752086.6672000000562083.5531238399712078.96232656000942070.1628000000562062.68959999995242052.54859936001732050.62027519990762045.71746544005082040.7202787199932038.73659968012042036.15279999995072036.15279999995072034.05040000004062032.3669030399182024.94503359993272021.12960831992812018.6388000000162010.75875744004592008.57956544002882004.54570783996162000.02960000011721995.30559999990961993.9678926399441991.18939039994711988.79597791993271977.27078352008471977.38879999994741976.31200000005261972.6658879998671970.3315999999541966.3893995200011960.80639999983851955.12360000012771951.71245360003721946.50520000007581944.77720000003891938.52365279991321930.24369696004621928.52879999994051927.28679999985341922.66882880009981918.09719999996651904.9552012800591897.76996223987931893.87091168005991890.81599999995891889.53440000002271884.3476000000241882.35519007992681878.85839999999671877.30679999994941875.74439999998961873.20639999991041870.73104943995741866.38049503994531864.65126240002311863.67339007997981863.1345588799531861.91918784001591860.34954752003641858.59584559987751853.80120000003281852.5640000000141850.72640000000931844.15421439993591829.83640000003771829.29522655998311829.29522655998311824.1159999999451820.13200000004261815.8512000000191813.45359999998441807.12260719995041804.85721215992841801.74851776002521795.61239999994681792.13328319992341791.59302399999961787.39463903994121782.8041958398591780.1647406400721778.5410268800041771.7083999999771768.70600000004921759.06909248004151751.75580240011051743.87449471986041740.134822400121724.92559999984061720.3212000000061711.3720652798811708.22683520004741707.30696320003471706.3260000001031701.40490271999261694.68360000001941691.70134896000261685.4771097601041681.28324704000121679.1755647999971677.53561215999481676.44045952006421674.0476000000441658.6022774400041655.2183999999391652.80997072001951650.30879999995251645.29999999996881628.57683199996811625.6064000000211621.43399999993971618.36000000005291618.6290793600041618.50519040000431611.32880000010981608.27475183992831594.62949631986451587.37200000001711582.10648560003121562.42840527985661560.55871168001611553.6895999998931553.6895999998931549.13424000008851534.43399999986011528.50560000013021521.75160000007961519.8040595199741515.89641791985511511.3345641600551505.39920000006121502.91986848001441500.9945279999331498.80472576005421490.88880799998291485.64349103997281483.35942783984431476.49549248025191468.0604591999391463.21152063995531453.9167999998191448.57424479998211448.29800000005341440.3519859199291437.28364799995571437.56581888001691439.25999999989591439.50842111991481439.29357888005261439.29357888005261437.70772735990041442.10577536018511446.8306144000451449.76067328009981445.71562383985541444.64349951989081441.03199999999381438.715878399991431.6172979199521433.7675999998961430.92629791999391430.18875392012661431.53014271980061430.22519999999941430.66756992001391432.74867199994151434.84624799996371440.8579935999711436.8483999999181437.755600000161431.45997568011061429.63672112015251434.83789455990131433.82399999999231440.2612147201191440.0415513598231435.81199999994421432.0218241599321432.13600000011461432.792000000011434.03040000005511434.10898048005791432.42330560006371437.58067455994161437.54425920005631424.19642880002741421.40400000012641422.66544000017231416.16976960017751413.42935872003231407.28376799997821402.81199999996061402.81199999996061403.21516000000021403.1072767999871402.238294399951401.72855600004071400.80983728000751396.85803664003221394.71679759998781399.03870240013041397.52959999992781395.21089855999841393.47168896003631396.02863120005621396.02863120005621395.69850399997591394.42388799991841393.15307359992381391.70986384009671389.67115647999621387.1887999999991384.74600000004561382.80677695989721379.67605871990351376.57033695999121372.72240000009081371.65331711994651371.6472640000431369.56960000001941365.22999999997271362.31400000004461361.27021999983341360.1571878400611358.33029151991651357.62492543996591357.5280000000241356.00032223998531355.44706560002081354.56089839998751355.72040000001081357.08483392003061357.08483392003061357.56239999999051360.44727600001171361.62933599990381364.22077456004351367.3659257600351369.86459935998871372.7083328000741375.32023552001651378.90009279990451382.87354399999731385.81406144002361385.6103600000251389.31017007999251389.03240000001321387.8279999999641386.69759999995311385.5562072000171383.63923887990091382.25159999994571379.99799999995681378.92879999994491378.89999999996641379.12720000006421379.24480000006541378.69759999991221377.4799999999431374.2903999999641372.59559999992551372.03440000000641367.44522223998141366.52948895995631365.34040000000321361.7824000000411360.02675904005741358.62120000004531358.075976159991358.94160000002011353.91519999997561353.00719999999821352.80940736001231353.47759999997521354.28039999999461354.28039999999461355.64080000003561355.60360000001631352.4535416000821348.133172959951346.7190079999711346.32562319994851346.29510400001281345.57200000001061344.37680000000981343.28878207998541341.56319999995821340.83640000002331340.01440000004961338.48800000001531335.67009599998121332.72400000002631330.53520000002731329.83720000001971329.18584927998751329.01326.84765567999081324.137941121323.32124800000721322.77252769618741323.0228650738441323.71903935546451323.6855763199411322.87576352001771325.01324.97731136000361324.01324.01325.3541484799891321.83240000001891317.80528639993261317.74200000004111318.10479999995251315.20960000005451317.64529359988681312.844924159971312.844924159971311.37252704012461306.72600000006741304.42599999994631305.85368208010251304.66575584003891315.16159999988641314.17604319998261315.56679999997161313.12919999989661313.31727008001121313.31727008001121313.35568639999131310.67365680005471307.17479999997321298.68720000006351296.20107519996871294.83545696006881294.5426639999611294.2833369600091295.78999999986131294.73113871995431296.46012063998641296.40832144003071297.80533503997641301.5799999999541300.5251748800321301.42208320005221301.5295763200341301.5295763200341301.571600000041301.74625407997521300.1799520001341294.2205753600411287.32324159995851282.52401311995941281.08672511992221277.841479039981275.23095840008341270.12627328012471266.77894703986641264.73595136020481264.73595136020481263.545474560111264.58500607995171265.99239999996371259.99319999991921255.06378479991241251.43560000006661249.73240000006721249.93193855988871249.67022240004231246.7492774398961246.7492774398961244.34703631999291242.95121872002871238.61582079990131236.76959999996781236.82000000004521237.06639999993561237.10240000010051237.25891360001921236.12479999991141235.1072102400851233.27599999997261230.97035712002281224.81842880004961223.55425279985021223.55425279985021222.19995872013921217.7930368000831213.6391788798841212.59686031991621206.22800000006461198.98472959989021191.22463327995161189.64473952002121184.79754400007761182.9061923200241177.07520000000341173.31393264001781171.60680000002231171.62119999993481173.93960000004151173.72359999999841173.72359999999841173.46911167992771167.9521145600461158.32060543990381153.22271215986831148.58341952007641146.22256192006471139.4285529601531138.6919843201171130.72311808006021123.81240000004981123.81240000004981121.84480000003671119.46548879994221119.30443328006941111.03559999998561107.97529600008261103.09240000004431098.21319999986741095.92624000002731092.8250406400251089.19519999993831088.766799999921090.52061919987321092.83027423996281094.06217215993181089.63108608007311086.77393216003751085.21662000018361078.93316928002561080.29778951945381072.41943594939631065.1037866729131056.01584647706821037.11800961605881018.31448457584791017.57784691070311017.57784691070311012.77455428787441013.55471943215511014.64323478558641014.64323478558641015.41292422475261015.35653814058041015.53839999996171015.39800000000921014.03591679999691014.21984368000871015.12399999999681015.12399999999681015.62035488000311016.63219584000991021.3544339199231021.38001215996741018.15359999999111017.52422512004351018.09265967994161021.30599999992521017.09629439990471016.00929600000711016.61692031994881017.77217984001441015.03555247998581015.03555247998581014.8941593600161007.72899040004921007.37079999997751007.47880000003741007.77400000000831008.19160000000271012.70345919991291014.69813184000021017.1185899199771017.88578735999541018.60454608002061020.09520000000821021.7862236799761022.07039999994411022.88240000005611023.05520000012731023.30069952003161023.02430240011561022.64183232006451021.05612479994861020.88207487991331021.14803327989111025.20625920002041026.9597313600111028.16793599994661027.82587776020251026.38779967977141023.65990687969871021.28164160003731018.62661039998831017.19782799998721017.70660479999921015.5998406399991015.2930457600081014.26303104006121013.15362304002841012.2933888000081011.22760256000031012.30024608000621013.88340480002081015.232034881015.68070479999051015.68070479999051015.91103712000291015.81927936000271015.3085760000181014.62435711999161014.30085104000391014.89854847994271017.5367948799591016.17778047997261015.29501503997961015.50599999995351015.09099648002381015.7386198400781016.86664959998821016.86664959998821017.30656080004851015.65280000003261014.89399999997411015.81278720004451017.04479999998191017.0886624000291015.18278800007011013.19639999999231013.62597888000281014.51839999996911016.01016.72319999999721020.99320000000721021.75399999996561021.75399999996561024.69371487996751026.0152998399971026.40021759990941026.16722960006131027.17805632005141027.36247552002121029.00725504010691029.2072803197971030.1616000000851033.9248000000551036.74210175978421048.06767440018731049.3906921600411049.3906921600411054.03547775993371057.80990639998051063.85920000002491070.34025583988681073.52120144014271076.41600000014391078.57679999979751080.41832959999741085.916515839961097.68559999987021099.75520000014331101.64160000000951102.65919999992181105.75359999992751106.43847743994771107.33879999998381109.75673679993841111.58170223997331112.4772684799811116.13268608011661118.43456848006961119.40885903989261120.00568111997541121.7928118398661125.22308096022471124.87720000008581126.71489551995251126.71489551995251125.7774387199891132.5821964800871138.74449216002861140.7495999999681141.33276416005451141.08716064005851142.2932319999951143.41041999997151145.83272464011147.62667375985261149.27919999996131147.9361256001171152.4453996797621157.2567459202341161.58906239977821161.76943199975451168.78131999995871176.31642383983561178.92720000011791183.0211483198951183.844161919921184.03200000001931184.19039999987511191.93146959988711194.90268352002071196.79272256002881196.53209152006431197.89601503998691198.26160000008171196.30799519986981194.65839999992951193.03840000010591188.37086976006641186.64840704000771180.5360000000361180.5360000000361179.23615103997641170.03377119991271171.28998399993541174.18135936006321174.7280000001291174.67951392010511172.69097472003681163.90549504002341159.32856959995271157.45142880000441153.48852352000971155.1231999999391155.47703295999031156.55000000001471157.09348864001191157.09348864001191162.1199999998881167.74880047997021164.55512240009261164.47799999993551163.9746927999811164.53070591997741168.48652639997431167.27614399999151166.48735808000881165.71199999999951165.30520000002911163.6716000000491162.60599999999611162.08840000002561161.2676000000261160.78160000002531160.56560000003331161.01132752003721163.30119999994581164.06079999994651164.01040000004831163.20480000005771160.73520000005151159.3288000000331159.27119999999951159.54840000001581160.2971999999671161.01719999996521162.27000000002541164.98350159995471168.19558847995721171.12960000000231173.0239999999961173.94199999998051173.94199999998051174.7951999999751177.36560000000831179.017999999981182.93207951997651183.9308476800271184.36040000000911185.62770175997781190.2584000000431194.54599999996121197.1010224000331200.95279999996231204.5404156799841206.53144319991571209.50200000010771211.50134559989061214.16460015989541223.26759999992961223.50519999996941223.91560000000751224.46279999993071226.12959999996841228.18221023995671228.03080000007071229.73719999990641232.33359999998671235.28199999990971238.27720000012371242.7807999999051245.30120000003081247.66640000004491251.23781119994941257.12351312004381261.39040000008961264.42960767996491267.58902527998731274.99307152008241281.4520000000061291.25119999994241300.18960000007521311.10799999990831324.00960000016951331.191199999981337.59955839999321338.90611599984661338.90611599984661341.45041600007811344.33874560013081345.98688799977321350.395030400181356.23533695994571368.22221871983171372.3360000000171374.95319999990621377.26079999991231378.01799999982361376.876800000071376.65019776015011379.87891200006151388.47035776015291391.2052000001511394.11300272006751398.94958335986461403.90450671987541406.7046995199621407.79119999988141413.07680000005231423.33424191993441429.5680000000971433.6047040001081436.31162192003421439.29796735992341441.21448640009661442.4095999999151443.21600000007881443.5831999999191444.87818240002751446.20640959999291447.600620479971448.79900000000631457.66450879997021472.80039999994031477.58595007999341484.20398912008521487.47188352008631491.24486976006481491.91663840001771492.81879999990951507.9704000000171509.1332000000231509.73079999999751513.9010271999661515.17944479986041516.88820640010521518.28445487998761520.35032767979781528.46799999992961532.78800000014961535.06519999991631538.0131455999511540.21615999992791543.49834559998321542.31106800006391544.1815999999231544.67839999999891545.61799999992881546.81679999997161548.4439999999561549.48935136000021551.70800000001671551.65040000000861552.25520000001641553.34959999986241556.35824959985671559.10297599996831560.73394016002571560.82777984006881559.95881904005931559.00860912003441558.88192080008461560.3473499200971565.22908543986681572.2081919998991576.63280000009631579.21919999994771582.1911999998531592.02368448005681595.94800000004351603.31693727995821605.98790399997121606.87806303998761607.68679999995581608.734399999921609.99599999995461611.08439999991791613.26983071996941614.24165807996021614.99560000005771616.05400000004581617.1628000000091617.55879999998111617.87842528004581618.36314688007721619.25334336002721622.20846399991481624.31583199996731625.3704260799671624.26080000000551624.37599999991931624.39197567994821624.46371071995741624.79021920001131625.4132582399571626.37772703996031627.98624736006331629.58839999997851630.75119999990761632.08320000012581632.68885183998211632.51435120004431633.20749087994451634.14139279991291635.46179936005961638.35639999994241645.25540064003641645.25540064003641645.95000000003641650.8252064000181655.934617279891659.95039999993331664.81200000007521666.22799999998461665.96520000000871666.24476512001961668.16999999998141670.24480000001431672.10625567996251673.82719999995541680.0984015999021688.77976447997031691.10060672000081692.6160339199251694.67614399986951696.40770751994821698.08748480004491699.9883999999021700.78399999988851701.20879999994141700.5924134399361699.40149312001861699.83957760010031700.27045120001071697.3645744000041696.05360000003631693.50120000000881690.82679999996121688.25248959991221689.09799999999541687.87878495994231688.25760000008541688.61679999990451687.1936070400661679.74443136009861678.23829503996031677.0442151999311674.33448319997821669.83848063996241666.7547833599691663.50798080005231661.49917760003151659.34248959997971657.59717504000291657.6943308800631657.70168928011656.75152000008121657.48762735999941658.12137280009281658.32000000011271657.81282752001431656.01440000000871654.18055712010961653.89199999992481657.42875503998221654.66341312002581654.20519935993021654.93555871993231655.95628175992421656.2208000000961655.54039999997211655.25960000006721655.10240000007391655.95759999993241657.82280000008271655.9062271999141655.26879616009071652.55419424014331651.46599999989871652.4703999999721655.20921360009771656.6343999998231656.45799999991031653.80093264009571653.24679967985411650.820168640121647.23600000015841645.0255999999581644.16865888013081644.17196320000131644.55563967999141644.55563967999141644.93150847994641644.71911999998451644.2658598399611642.9632243200251641.96807360002461641.38675712000621641.2928863999451641.7702982399931641.84379199998381641.64445279996381640.03382464002291638.8049128000481637.90594079990051637.61935775990921635.55141536002861633.13318591992361631.55010464002181630.4619814401071630.97950016002281632.01006864004561632.05717103997721630.35000000003011630.67260416000481629.69520000009291628.24910239991191626.00670975995061626.67764704007571626.18732543992751624.76171360008721618.09934175992631613.98919999999041607.78267199988981602.29039999991481595.87297839999381593.0443520001031588.74654528005181587.90553343992361587.8887372800481587.01119999995441584.93399999998061579.80879999989431574.74777280004261570.389692480081564.12559999995281562.47389168007841560.04616399993551557.2985964799711554.7032027200471552.9000908800371547.66113344005431543.13279999992851540.90138111995681539.15279999996911538.40759999998321538.6077635199661538.7191072000121538.8704000000131539.1018617600011539.0816254400141539.01539.01538.61980927998431538.10352608003091537.4853208000011537.14485920000971538.18817711998351540.1966847999631543.08200000002381544.99000000006661546.48144832001641545.6269638400151546.25799919997421549.1416755200431552.46519999996991553.9951999999951555.5525343999431556.2809584000151557.03731264001931557.81095551996281559.10060944006751561.32680000000031562.80093439992421563.81795743992441564.43476960005551565.022399359971566.089200000061567.68326415998941568.6052903999971569.5642137600451570.3219072000281570.84641280004871571.3739999998741572.68721663991961574.67680960004871576.75200000006771577.59440000008951578.45119999992151579.92079584006051582.2711167999681585.16058239997761588.26134735998951591.57496320009391593.93201471997871601.53054544010481610.75176607998041622.5706598400931632.77301055983621643.19058880012931648.65439999987071653.58299359995361656.7184768000151657.0527999999581658.45998672001221660.2251358400631661.7126848000431664.15230975986421666.09530160007241670.60799231995841672.43076191999531674.69682367992051675.90701663987031679.29597599994371680.99504063998961682.4743655998841683.7819662401121685.3576128002251687.17550032001491687.17550032001491690.04235391986441692.77859872002051694.585882879951699.59914752011741700.51873151990141701.6072563198791703.02766079974661703.91432640004131704.28567040009581705.23093536001671707.70840703981181717.8050343999021726.01239999998281727.08879999987451727.9383999999291728.72320000009631729.60520000006551742.36620160002241740.40411568007561740.43472864013031741.52485760011361743.40820479981831745.89697839994981748.65759999987591762.74039999981621767.19407600002021769.17388016008891773.95985567985991775.19590559979091776.49424992010311778.04425439994471778.77427392009031785.38561023992251791.83120000008061791.97159296002681790.47199999987271790.28480000003831791.33239999995131793.36640000003421795.78169280002951801.0164095999981814.46920000003371825.0651999998091827.5069491200011828.65877199996161830.57146495987381832.24073600023171833.925847040271839.1913222401431838.93399999991221838.51519999989451838.78880000002231839.36479999994941840.49999999998361842.65999999990191844.8223999999321846.05618624008531849.28266879992571855.83834655988651857.05252096003661862.01483360007111864.98807616004521868.17360000000331868.92239999992881869.8259999999751871.29479999989921877.33818480007431880.34976383997831883.75280000000071885.70362880005021886.76685727999621888.09200000002471889.71200000002731892.53800000005461894.21882624000611895.8527999999851897.20420160002821899.43077120005641901.24514592000421901.32767231996261901.86411264005761903.95178240001541905.7578896000061905.6846367999991906.0607763199661907.37534223999751908.431963360051908.89966815992671909.65325327992261910.36689136003131913.48493951997631913.55349504002081913.55349504002081921.74077312001831923.51944384003351926.36358016001171926.94528255994621927.47114432006331926.96851519998131927.23148800000261926.45600000001651926.70541824001931928.8632719999671928.80239359999311928.44079839996651927.9474367999031927.27657503985571925.17982527995671930.99593280007461933.76034831998941937.54238112000081939.14011391992631941.92521999995071944.25578799999651946.59199999993871949.5679999999521950.29160000004341950.9288000000461951.52280000008111951.93319999996581955.83078320002621956.75725760007231957.52919407997661958.6377910400161960.02288959995081961.19408768009731963.9306247999871968.95640000000751973.15759999999051975.06439999995591975.36626240004921974.76479999991671973.71359999993621973.87354176009131975.36818399995671977.60417280009161979.79600256004371980.54477087996951980.9713363200141980.86753151991161979.9812086399631981.0516000000751982.2202521599151983.43080432004421985.02199999992831989.77726080002551990.6055475200661991.78799999992361993.79519999995051995.30088319999621996.6724000001131997.12498511993541996.93600000000041997.24662240007771998.49714719995772000.16000000000212000.95920000003072000.95920000003072001.99004895998222001.33436319995441998.99661120000681997.77800000000341998.61486751997841999.15619328001431998.25769216001031998.14197759995071998.48442752000821999.44251744012062000.97138848000872007.92669135999542010.17022431996662011.30680000001172011.97279999996732013.07799999999072013.87992000000922014.43520000004082015.22873952003742015.24827744004122018.59479999999262021.23360000000642021.62599999996222023.23923008005842025.53800000004142029.012400000032029.1556095999542028.64200048001612028.81104255996622029.53132480007732030.50789104002662033.32960000002642036.69496480000522037.6548079999682039.91140736006242043.15639999990982045.25039999997262046.40122159998282049.52319999994052050.74599999995142050.86206799997172046.93026704001342047.37337600000392046.82831552002272049.4420000000592051.8971999999292052.15640000004172052.35080000001152052.07079743995432051.6015999999282052.56615760006072053.00794959995942053.03622112002772052.52069471991262052.3971600000162052.51709184008452052.81419839999852052.53355824009672052.3017984000972051.5541964799962051.3613919999872051.20048640003232052.88205952009142053.027343999912052.9005663999862052.58459264003842052.0669155199642051.4620233599552052.79845120001852052.72111456008132052.771804000032052.9615758400582054.91196175993352055.58199999998672055.90599999993632055.90599999993632055.88204240003962055.82340127997942055.28907696004672054.2972000001012053.39000000008952052.52240000000352052.6289999999532053.10108976010272056.66737920003422056.96996352003132057.12281088003372057.2248895999712057.2659929599382058.1802499198832058.8351647999112059.38089600009152059.6365103999792060.0701407999852060.0701407999852060.0485494400592059.9002726398662059.73921152007962059.7756636799762059.9790924800662060.5366431999422060.8948000000422061.08919999998622061.16119999995772061.26560000005252061.55720000000662062.49827135998482064.56856063996432068.5004404800162068.81205936016072069.2193719998722069.98879999994822075.5720000000132075.85640000006152075.95720000011442075.7699999999222075.474800000132073.9520000001122073.84399999997552073.9880000000472074.29039999997442074.3624000000742074.20040000002252074.9025497600732074.88796511999272074.97986160005082075.2960438399532076.3356000000742082.4724976000472083.7211619199612084.87918399994762088.5548671999872091.30479999996442093.7996200000292096.22160000001442096.52759999995852096.63972800004382096.7421940799742096.68353167999572096.6037600000272096.44897152001752096.25989375998552096.0291179199952095.63032576000342095.39335935996722095.20826464000862094.92525600000732094.19114832000332093.9135200000172093.73321472002142093.56963072000332093.3799423999722093.098410880032090.5300019200032088.8965465599542088.17631615994742087.62568319997542086.4174323199072084.19012927977252085.78244576002772085.3421873600262084.5869952000482083.94078079998832080.7328755199432080.24339360018082079.991312960012079.98792176000822078.82534912007532076.8999251197242077.07536896011242077.4229393600732077.75843919980072078.19508319999252078.93160576016132082.13598912000772082.5851999999882083.10556384005942083.7048716799232084.1331391999632084.3693817599942084.59587504002272085.13963775996852086.51571759995342090.8505472000422096.317571682099.62080000003972100.1932000000532100.78000000005482101.86719999994472102.95439999996232103.61680000002932104.26079167992932105.76593584008152113.5337196799852113.7045785600292113.9092443200272114.23489312003682115.59827071991372115.99886000006152116.28352640003232116.53968944000872121.74589280003872122.186387360012122.46170672001562123.22759999998832123.73969519997762124.4935337599642128.81719999994632130.43680000001722130.76080000001782130.9796352000172130.05948319999422129.8242761600152129.64823760001952129.6356764800692129.73439999999162129.42479999995482129.0575999999612128.5464000000242126.6528000000472123.69865824001232118.9551999999652118.2351999999412117.7743999999792117.42879999993062116.9823999999322116.67999999997942116.0752000000742115.12239999994242114.41710399996462113.0488851200412110.6631737600042109.19567359989562108.32117599997262107.12070848004442104.449592000022103.0913638400252102.1395999999952101.30799999994542100.5951999999042100.2928000000532100.2366086399312099.89569152003832098.87005440001762096.20428800006272095.35611072007532094.8553132799272094.43111232005232093.9914831999832093.6040339199862093.05042879995472092.39770704005062091.43890031992032090.7366540800052090.48083520003552090.5066572800292090.66005760002872090.86530367998742091.11407999998662090.5200742399632090.23659296003872090.29314176005352090.7273075200332091.3402579200092092.10887808005152093.07441759996532093.71515136001842094.3170080000432094.66479104002932095.0166680000042095.3767572799472095.64154880004032095.92113920005572096.0053503999412096.0407441600442096.31408639994562096.59264000005032096.90719999997052097.65600000012642100.08500543996572101.1308342399982102.44934143986172102.96228095997282103.62999999986962105.0250086400212106.28376592002452107.2096239999522107.87812960001472109.2313479999662111.1161660800112111.4601932799392111.5347392000942111.45120000011782111.77472383990932112.39086383986072114.29959999996842119.4130439999662120.18275999984052120.8903471999742121.040260000012120.69629439997742117.9659750400832117.8689881599572117.9924390399652118.2993268800842118.9088000000352119.28320000001082116.51089183994332116.5394700800732116.7210007998472117.37233440006282117.52251200001052117.4880887998352117.3149144000012117.05225983996842115.7185446399622113.18896575993452112.0380518399182111.04520000000142110.92719999997462111.15760000005782111.2619999999992111.36640000004262111.2259999998342108.0275999999342107.89800000015842108.1896000000872108.65760000003142110.189159519952110.537010239842110.537010239842110.54113439996952110.4898560000042110.37993935998252109.6918195200072108.6091456000022106.9797222399612104.749430080062101.91214880006652099.86999775992672098.260061440042097.00340479991472095.34412447999882094.30080000002732091.8922119999282089.11639168012742082.6759999999342080.47560000008072079.63320000005842079.75638240000942074.13999999984162068.3244512000392067.86554624004932067.71400063994132067.66570303991882067.5980806398922067.41199999996572067.10960000001252066.6055999999042065.9360000000092061.0760000001542060.1074575999962059.3488108798872058.9100817598592058.72603263992642054.796799999982054.4080000000922054.4066843199032055.0131561599892054.97578239994072054.8737425600462054.68217360002152054.41126207999652053.87138735997042052.95141599991582052.2228000000082049.7243999999472046.81471039992682046.27692512009822046.02018464004372045.99733760007052046.1348892799482047.02236847992912047.21480000010482047.35103232013242047.19789695987522046.76167776006562045.5662560000132042.41807824000562040.49772271998542038.45434687998022034.36978783992412034.08369151997862033.99566719993872034.09264831996852034.34913823999022035.7705465600242036.09041088000752036.1703308799952036.15408479993362035.96816640004932035.64876864004622034.88887199996092033.7196416000112031.71495199994232032.15233279998922032.17043072003782031.68630272003682031.04897600006642030.8783521600052030.96615040006712032.97282033.22960000002242032.83720000004132031.33599999991772026.04301439997152024.91679999982722025.01658384003032025.7632742399472027.39879999986442028.00165440005412028.57440000002432028.81990112005062032.60561920004942032.6189567999942032.22812639999852030.97051679996232031.06725568002182031.28854911999572031.45443359998442032.2902067199832032.69082623999362032.67068415998532030.77980576005072029.1699174400412027.0002492799782025.95010048016122025.36369504006922024.90164511996322024.45400191998762024.09313792007772023.71872880007322022.70871135989552022.21732864001072021.75263936003782021.24979136004692020.5102332799972019.49350399996252017.62911584010132013.3476751998912004.78079999989062003.5243999999422002.67480000004071998.1408486400671997.61666560005281997.0684288001041996.46719999995781996.02079999995931995.54920000002451994.62039999999071993.1083999999711990.57160000003071989.50600000000991988.42599999999951985.3557699199571983.82153487997581983.35079311998671982.97497471998211982.74299935996371982.5853884800381981.85239999999521981.60759999992251981.36999999998531981.13959999995311980.6428000000821979.4871999999811978.5800000000461977.90679999995521972.8820870400411971.42601088001041970.1335343999941965.04619295999371962.25974400001161960.72133632000031960.17642159999561959.39991167997621958.34387999998421956.822845439931955.8622271999911954.98598719989511953.686919999991951.59960127997741951.17534991993351950.28543423995121949.17051999993851948.02166784002841945.96998880001641945.96998880001641945.62905727996261945.17474880006421944.95229472003671944.55220799999281943.77121536012921942.88098528012371934.67458591986381934.44490592016451934.52640000000471935.69279999987341935.49839999998061934.4508000000421933.64881792008921932.6704243200861931.7342918399241927.74021375987171927.45766783997831927.39899359995051927.93330944007061927.97632512003521927.6641331200471925.89750079985241925.82582047991171925.90251056013061926.05812991995551926.37506384012521925.69904096008491924.49693696010331922.97431936006111921.55164832009561919.73996992008121918.6657472000431918.20492272000871917.97874048002931918.01600000004241915.74330959998721912.96854016008341912.25625280006221911.5037145600391909.86741375995731909.5409760000871909.43258719995721910.25279999998521910.56518240004221909.98512640005971910.57733360003611910.72714159999651910.7229440000791910.50796928006871909.6848000000991907.60399999998071906.71799999993571906.28959999999421905.36400000002251906.25535039999181908.21493279994661908.87927791995341909.449024000021909.87319039994281910.17056191998271913.6640000000251913.71441913.84399999997981914.16799999998031914.29760000001121914.39840000001251913.24858399999471913.46074640002371913.66058815996961914.10080000004021914.38880000002931914.74880000004121917.38479999999411917.51439999997391917.53240000003071917.42439999994531917.26239999996961916.3413756800041917.32599999995861917.41599999998741917.48800000006121917.77159999998731918.12799999998291918.57076496000221921.11349183990271922.61268479992261923.52810720006481925.92063920006421926.81554304007181927.30281600006861928.06170751996641928.7328000000461929.50319999994261932.81520000002181933.23280000004181933.57839999998781933.86279999993491934.10400000009331935.14700224001541935.30303199993731935.72473743993871936.46905184001751937.35172736001461937.72286431996831937.8048895999661937.58022944003621936.5566105601111936.36450303998121936.16963359999681936.21793840004531936.58629696006531938.8341312000231939.1706079999861939.28497280002011939.19516288007531938.74559199990471938.26854880005111938.0464000000731938.19039999993951938.83120000003561939.87159999994131940.1589735999951940.2617536000291940.0810897600581939.67583487986921937.22559999994341936.79360000001091936.7215999999881936.95200000002021935.22912608001871934.7059999999981934.02080000004531934.33039999992871934.5608000000121934.94842239981221944.67012608007851950.39690640014991954.52885824010741955.8128854400841959.49877296002251962.51132159991271962.51132159991271963.74240000003121965.99159999994691965.91960000002631966.07080000005391966.30559999999761966.0012000000071966.2963999999781966.6323999999591969.31416480002241970.73071871995761971.5781414400231972.76735520003131974.87858095993741976.38284240001741977.14806272006331977.77408831993781978.93164816008431978.93509120002361978.84780703989741978.53546672008431978.35102720002031982.76399999989481990.49274943992171990.86577279991981991.0797625599781991.18369599993661991.22151760007071991.69080000005241991.58639999995781991.5107999999441991.60800000005681991.70481264002251992.7121719999831992.53840144001171992.4981721600161992.48707840000591992.56147024000191991.8664000000231989.82520000000931988.48599999998851987.8623312000471987.42039999994181986.85160000007341986.2647999999691985.55726240001971977.47966528001031975.32467840008531975.33654976006481975.59359408007821975.8925481600041976.2437094401011976.21407855991741975.93696928007531975.03840783991021973.62050144000771971.63935424013681971.24240000000871971.1631999999531971.48000000002281971.83999999998351971.77519999999371971.4008000000181970.78159999989321969.48960319996921968.49424640004531967.82614400009221967.27400000010651965.98974800002471965.55400255998691965.01952639998291964.14180608007361963.19207279997731962.12465343997841961.31215615995441964.22314624003161963.585599999991963.34439999998521963.20097343998281963.48086272003321963.57444400004281963.51806944003851963.23470336004741962.57054656000561962.71372208003231964.12879999994241964.21917008001671963.93483200004131963.5522931199911962.9995147199991962.06025312001541961.75439999999141961.6608000000231961.63199999999351961.85608527998491961.96314495998041962.26146399999741962.89572928000781965.59800000002631965.9543999999961966.20280000005961966.49080000002321966.85439999994921968.47520000006171968.87479999997371969.23120000007161969.43639999993731969.53359999999881969.66320000005521973.613999999951973.66440000002741973.93799999995051977.05560000003451976.992800000011976.19359999993031975.67520000001131979.55159999997271980.71800000009741981.26982496006171981.01409888007721981.36071424004971981.6763999999661982.82751343994121983.65132320001541987.7123999999591990.24132639995131992.33082143999951992.47309632002611992.53758399993261991.0947552000831990.65759999999051990.2760000000331987.14319999995931980.06295679990541974.7363849599161974.47703807999071974.30166080009461974.46391999993741974.56513471999051974.43847808013521973.97640191995631971.0548368000631971.0922912000731971.43218880000041973.87898943993221974.29333647999211974.51924799993661973.73195903992561973.85732928008161973.50255360007781971.4024249600111970.24747008001371969.52652544007881969.37199999986771970.06320000011781972.18688512001021972.92640000004371972.98759999988921972.6498332800191968.3856479999631968.26978560001111968.28817151993371970.80719999989011968.7100527999811969.38071344008471969.65668512003531970.77945983993231972.22786079995261973.65070607996361973.33575936000171973.1203366400311973.03381280000871972.74498048001831972.55462399995961972.404000000021972.14120000001841971.35279999998831971.07559999994621970.89559999999141970.23480000000841970.1987999999971969.95119999999631969.59439200000821969.15641600002051968.7334880000081967.59867391997051965.89479679999681964.95944767998321964.06224127989121961.73416575992041960.84431968004381960.38189824011491960.10000000002951960.10000000002951960.10000000002951959.84080000012111959.840800000071959.30599999988021959.2699999999711959.04540224002861958.87284703989641958.32642176012631955.74783519996621953.6615751999511952.02243328005281950.5463915199931949.05450336001331946.79140800009281946.20300528005121945.98078016000071946.27880000000911946.29680000004051945.9115999998621945.50839999993331944.8748000000751942.3480000000171941.76119999998951941.13120000001961940.64520000009541940.37880000002631940.32840000002581941.3179999998211941.93359999995481942.06679999982321941.9843244798951941.74042591993451941.28655808016771939.97551583998191936.44252480005751935.64967999997271935.37944240000891935.25342079989671935.40339679998491935.18240863991041934.90838527998131934.48400000001081933.9260000000641932.64441928.19199999993951926.1219999999221924.39400000006411919.65960000006631918.39960000012681917.97480000002271917.329784639881915.28757680013951906.63800000003081905.61336656007531904.07627888007531901.99429952006721900.55093439997151899.21694720005781893.85651200013261892.74394496017821892.04558463996111891.0371267200931890.0842873600111884.7236163200331884.31460447996141884.57640000001421885.04255295999811886.57759999996281890.65496479997861891.15968768011891.50397440004831891.65933600008981892.11401599996821892.80543488003221893.71903999989351894.77767615996571898.03714623994371898.87857999987571899.80682432001051902.28811999998241903.74734815996541904.41159999997441905.54963567996041905.65390751994191905.82025631998611906.41841280003131906.8802495999521907.09463680008141907.77999616000781910.53426047996161911.73451199993861912.57956832009861915.49200000002591915.74759999994331916.54320000008331918.61112223996571919.25656192003571920.13595520002131920.88658400004581922.13243887991031922.92560000001661923.77880000001121928.5961600000991930.12262912010921931.51060480005371933.47514495994051934.73890048006461940.36047840005951940.5734039999871940.85161344000881941.02408799997331941.13588671996631941.13796032001941941.21176800000281942.28691519999871943.40960000000091945.53359999995881946.64959999990381948.780592640041950.79530240000261952.03184191995871952.849226239971954.1880000000171957.60920000000941958.00019504001351958.4764739199811960.16438528005921965.5096883200261966.18146559996221966.76792000003751967.47239679996441968.02349343997231968.95767616002881969.69703392007271970.34380992007351971.14428431999071971.82114623999541977.16415296002741979.48600000003761979.90286559998251980.32155711995481981.05773120000161981.43740159999381982.1403999999771985.51036544005551989.0812423998911989.44061599987821989.9321427199731991.60439999991741992.3747999999421992.8319999999391990.31080000004891990.0687999998991990.77876480002921992.14320000004931992.14279999999781992.14279999999781991.2175999999551990.80559999999061990.39160000001271995.29120000000941995.64760000008161995.86039999996821995.86760000002691995.9323999999911996.14840000003411996.42559999999911996.78199999996921997.29319999995731997.56680000000851997.78999999995651998.06719999999861998.74399999997791999.6224000000112000.05920399998222000.33227264002382000.37245439998862000.36806527999832000.23958847996622001.99750287996742002.3729483200452002.60122720000372002.865147200032003.1580000000022003.5647999999982007.16360000001762007.61399999998572007.48439999998032006.52691328000742006.20836799993752005.75907360002882004.45013087996272003.76110240001572003.35139840010772003.48998400006872013.2051420799942014.1391999999592011.81399999999032011.11199999994662009.70227903995672005.50720000007192005.61520000010612006.37895520001212007.06696560011532007.50293504006462008.57600000000682008.64080000004792008.4464000000272007.67247679998762007.25734784010642007.35132799999772008.1696339198782009.22034000014742010.79961471999832011.76810687999642012.07798239999222012.61444863999712013.05845248003672013.53640575997882013.99352720002842015.34600544003232016.68610400000332017.7800345599632019.15037311994342020.01392815999432020.6421875200442021.37385727999322022.07855840001342023.42767999991882025.31326831999562025.540733599972025.62254720004012025.51796576000722025.07548544011622023.64641215978242022.8353465599662022.18129856006342021.788835199842020.9431836800492019.35559999974842014.20759999999382013.1024000001242012.06560000000472010.6616000000452008.46919999992721999.21454144002631995.64215983997541992.388479999931991.83449760002441991.25966400005651989.28627456003761988.96081152000171988.63163904002931993.45084160001081994.2849835200411994.7574935999941995.03335584001871995.23653840003071995.23653840003071995.25233519999071995.5039492799681995.98148495999181996.41981952005342000.3629302399051999.5045376000591999.2026905601661998.85391680004271998.35421952010261997.9950777599381997.3676639999481997.1421772798741996.75422399997141996.37833216003041996.10932576003731995.28909599990631995.09435615985651994.85487551997581994.5447743999141994.3887071999491994.04762880009661993.7851110400081993.29440000005431992.25760000001171978.9362359997651977.47596799994521977.0443999998671976.27673600008621975.49818560014961975.0080600000981973.14426080012071969.45601472043521969.6394764796891970.2264262399691973.865836159861974.81899119979361975.29400399971341976.03611520006961975.9697504000811975.054031359761972.99858976000591972.3210467199551971.5343199996761970.58846464012571969.6865868799611961.93621888027841961.08611487982331960.22868991982111959.1771731198391958.45369407987571957.9083776001521957.26651648008031956.83579903994521951.15464479998791950.32738687996271947.88357952056981945.3886239998821942.45724192035961940.26835328012881934.78487359987781919.91490304002131919.1574975999561918.96073600008161919.16297823994611919.53886816003181919.02202623973811917.5791808001121915.0127999998931913.82119999989661912.4532000002041909.83600000000771907.93880000016771906.76880000005211905.96239999993941904.36416847983451901.63495167999581896.44557055982481895.97479999984881895.39519999987971894.42922495992231894.2452318401231893.6600201599921892.88832704007041888.89959999984241887.98880000009561886.93759999991021885.9403999998191882.53279999997591879.82559999992991878.65560000004461877.54079999993251876.67679999996521876.02880000006641875.6779999999261875.1452000000691874.8787999999741874.45759999988651873.96439999985271873.50360000004411867.72640000020871866.9847999999581866.35839999992821865.92279999987681865.6744000001971862.5980000000551862.30999999988671861.96799999998281859.95199999995681858.3538860800841856.81791151996141855.1672524800741853.2575256000941852.00821184002711850.04730191995081842.89240319991951841.93439999996461840.98400000001081839.17444159992711838.45057520000271838.00537759999571833.66062335999141830.63158415999241830.43401472000761830.1738974400071827.36821136000191826.91564799995221826.69150624000351826.3743839999851825.93002879993631825.41826432000241824.00140800006471823.02755039995851821.98000992001431815.5535075199941814.78320543999331814.44520000002741814.33719999996771814.5783999999981814.46320000000741814.22200000002821813.9952000000381813.34626944003321812.74861039999311812.48603360000361812.25341455998861812.14124287994851811.22337663999131810.81820960003231810.3960217599951808.75506880007541803.95680000006811801.6985152000251799.77939839997481797.99639999998361797.4298175999791796.42463360001331795.84990943990371795.35949151991171793.60310335994091793.1152088000181792.84495391997731792.592072640051793.23439999998961793.31359999999431793.31359999999431793.5007999999821793.90400000003841794.61519999999771795.4648000000271796.21360000000371796.79085280001441797.78679999999671798.71991199998141799.2222876800081800.77766623999241802.15360640005521802.78895743993831802.93744351990951793.5127161600021792.79960064000031793.03760000002671792.85760000002071792.90937872003631793.33919999999941793.50119999999971792.84735423997271792.7043795199791792.63090352001971792.50712687995861792.32060367994771792.08137360002981791.01504575999571790.19279935997161789.36556640001371787.0236755200021787.25029071999781787.72639999997051787.9279999999731787.81199999998721787.79760000002351787.79760000002351787.92000000002141787.9343999999851787.92000000002141787.15360000003921784.13999999999831782.31691936000171781.12699839997251779.6043510400281779.06919999999971777.82959999995891773.22537711995351771.67543455998931769.61471679998071767.21834560005121765.33956607998041763.86117472003431763.29562623996641762.91941887999041759.87093631990931758.87584160008491758.19337167988081756.4468000000161755.6537369600861755.60704000007631754.7009711999491753.5755248000771748.35862143993681746.2263040000541745.64799999992941745.69356303994621746.10646432021621746.11478464020751745.00143488000371737.46599999997441736.67323632017971736.72370112013441738.0337176001211739.09506239993331738.8863300801961738.13208976006381735.95480000024281736.17079999992781737.17519999992421740.220019199851740.56864416004081740.83157152017861739.77652223992551739.36899936006971738.98519999997371734.55919999994651731.3606206398561730.48825280014531730.25446879984431729.39599999999471725.94721008003631724.160799999981722.18079999986321719.0108876799881717.07859199979521714.73178160014821705.05359999995331702.7223091199331701.75401423986181701.74432448002881702.21630463982341703.05443424012451706.06148831991621706.61008080008331707.08734720001831706.92783120004221706.2583097600691704.68529840003611702.86742656007371692.71659647998351691.45534079974121690.42687840015221689.31335232026941688.79249279979011688.5810835199461688.72209839974721689.21243712002051692.36496000005471693.14300927992081693.43710336012581693.5368348801161692.5002532799051690.44941824001441689.33045327983311688.52098608012421681.66400000009251680.49759999981441679.97920000020241679.68760000001771679.62280000018131680.18721280000981679.95157840011481679.425349120121678.56207168011541673.33719999993581670.58319999998251669.61572160005681668.36797888016711667.67040000004731666.5255999998681664.95599999994281659.6424000001881658.45542528001121655.795581439981653.47682927972981650.95873967993951650.57318832005761650.24584607985141650.56489888010541651.39450016002641654.0601201599181655.68784703992631655.59604703984341654.9911088001931652.67324159982081650.53242239997941649.70841408021221649.64962031986991652.41375680016521652.558908800021652.3927212801471652.10683519990651651.25395199992111650.06563967991971648.79021439990541647.50619231983341644.80700159994831644.80700159994831641.83487984012961640.60631360000931639.96394399996481637.88100864005291634.37222399998361634.42358079995531633.98017312010031631.21306272007471628.31322816003151627.51189119984451626.67252095985921625.92252288003491625.44599808013141623.68934144003421623.27534143999331622.75732351995861619.91119664008281612.25188415999721610.45279999998391609.58880000001661608.731999999981606.06639871996341605.57632928006431605.07939968000821604.56895999991161603.86148799995771600.79918592011341599.80491199995661599.10800383993321598.83425984007021598.84137920004921598.91169727998641598.65935455987781598.2085049601461597.621503360171596.79522559999651595.50182911995381594.41074688005621590.2870879999081588.59357232001981588.25342272000631588.08994671999741588.29797344002371588.5473529600781587.59087040004941586.50061759989671585.49320511994871585.16310191987281585.1715734398661585.30405632014641585.05587456005651584.88289744010741584.17459887997051582.85763391996151581.00314432005551579.15973119999881577.24677440004261575.02984976000431574.0623497600561573.04961599991681572.47433887993571570.19759999993681569.1968000000591568.65884159986831560.86120831989931560.79112064011861559.92427967988211553.42009632012261552.20227104011631551.8914956800271550.51326368007061549.80046080001671548.88760592004591548.07140095987961547.68904784015921547.77482944001991547.54717695996991547.48907151995131548.31453024013581548.91024960010581549.26272272013971548.3505784000341548.33527840000331545.9467438400371545.61687872003311545.10576639990131544.56649792001421543.81015808007261542.8909311999321535.48623040004371534.85380639994221533.743206399971532.21359999995051528.64599999984531525.50155264002861522.3011328000871518.91130800001251513.46695536008311512.35157696001081511.98163840001481513.09074943997051513.41440000007331514.35040000006351515.22880000009671515.84799999996591516.3160124800151516.73157055997241517.08775839997181517.49883520007141517.880008960021518.36319232004781520.600158719951521.16595199998621521.61472799996161522.17777951990231523.11595968007081523.46187359999771523.5011299200571523.3934942399541521.44169920010131521.38687120007221521.37775743992171521.54209312011151521.7161660800921521.99548576004171522.26567439992191522.90365919994451529.34215792011651530.39183872006771531.65044192004421533.15076496003121526.3168756800741524.58000000007271527.50870528012571530.35399263981661530.02559999997041529.5791999999721528.06410879993041527.90350079997461528.27743999995961529.78382111991071531.96839839989571531.48834559995951530.87400000005621522.38727391999961519.0558560000421517.54157792016781511.70324640004451507.63089023991621506.65375232001131506.4720000000691496.64457056008751495.31759999994261493.88671071993121493.23827584001761492.48640000003271492.3919999999821492.08419999988181491.17896128006081490.05475199997481486.57225328005731485.88416799996121484.68026176010471482.0547020800141481.2756951999441480.8343136000151480.66900159993181480.8541006401931482.345395199931482.78005632011421482.87912255996781482.13620224003941482.64195328000051482.6671000000291481.84312479989881480.79229344005671472.41205760006031470.29993472017781469.8559999999951470.30078720010491465.22482400005631462.48111136009581462.61683936001621463.40436719990611462.29769983985471461.3839680000851459.85849103998611451.47988863980981427.27751200007761421.92227200002411419.85030528006041418.44737088010631418.23937247986851418.7836607997981414.51400000007291414.14680000023261414.44920000023651415.2732230401111415.17853440005661410.65632320028861403.37746367979231405.62313359968431405.50346959994481404.8690876798531403.45344528011881402.19314431991921400.05847679985551390.45687935985281387.7498860799791386.35779199995361385.4782111999721383.67274560026271378.21338783974421374.6633135999191376.6373625599681379.43346144034921380.74755328017891379.04003968007851377.964541120071376.65090688027161375.25113759979721373.5172724800251373.41954976003881373.90784944009121374.6570944001181376.22913536017061376.74879999983661376.9683999998961376.78839999999241374.06479776009541373.94849472011381374.16862463987881374.98030080008041374.78711040018381374.1315072000291372.79704192012881370.6749739198731366.54128256012811359.01609216004521357.70025472007551356.40782592004421355.60600000006271355.13439999987211352.02846783978951351.27120000006381350.77799999997891349.20258671994771345.14091775983231342.91847759993951341.5766755198831339.84839999999371338.85479999999551338.9352102399611338.15623936007681336.76024575993161335.56999935991441334.5661207999471334.06841215995561333.5357619200471333.02034848006021332.6814617599651332.52884000001971332.13283855996021332.10693199998761333.0782598399611335.44865536002321341.41564799991991344.10363840016541344.87399999984541343.3467999999631342.49079999986861341.32879999994821340.85360000012471341.26399999990711341.72678768005881335.79112192000321336.14928240003931335.8728384001871336.31478879993941334.03747199994561332.11553536009611325.25935359989081287.68302944020931282.14799999995511281.83020960000181279.37165872005861278.61045824025471262.32598160006861255.55570687999371254.6412979199461249.8105433600381249.8105433600381236.42622944011621224.53183519976351214.56724383997361212.40208720007441210.2007999999921209.3084000001581208.0016000001321206.49381648012851203.8271020799261200.29228799984181196.22339968001481194.99872288001371193.4914355203021192.5728000000951191.16336095981361190.18305119961161166.08107904007721159.4082399999011159.27693440001211158.54240000019131157.32559999998921156.08719999984171153.74719999991751152.32880000001981151.48639999979331151.29200000018181151.53679999987071152.53759999990191156.53227520003471157.87640287991481157.99408479997671157.25619712003911156.1378441602151154.4091529600781152.13442671998131148.46909119987871145.6539430402111143.19388960013041140.59782880021681139.5325081597891139.00353567988171138.89877791988581138.05591999987181136.25126592001861134.69999280000681133.65306239990791128.65261120011661128.13409599995631128.66206335988141135.07887679995951135.36979136015931134.91157760016181133.6554511998481120.6799359999331120.14160000023461120.37919999978821120.32160000013821120.155999999841114.49999999978721112.90533119985461112.2288767998961112.8412800000191113.70883839998671111.66188320020961108.3901743999031104.95085888001311100.08503519995181096.77093056005291093.26168320004851090.95373567994261090.18493119989761089.83734400003691089.705082879891089.95888720001651090.45485344002261091.03876864009271090.91679199994021090.66779679994491089.85711424007061088.95394048001411088.1291977599591086.29104927989491085.69939936006841085.14839199990871084.69169024000711084.26558560000741083.4898143999041080.58645760016021076.24697039989341071.98912800015961067.33484640005121064.10360927993591062.6672524799291061.08261600013221059.4034622399871058.08322272015581057.41686271994261056.61040511999451056.38329888010341056.79414384010171059.477600000141060.06799999987721060.98156800013631063.50140672016531064.03278016001041063.48339279993431050.9983999999071047.7092000000451046.16839999994481045.04519999991571044.64919999999461044.46919999993751044.43680000007041044.60599999995021045.29719999994451047.09720000005541047.32759999998531047.57553663999991047.78223280001111047.78928447993081047.59787968003021047.1042630399531040.65110815987031039.64547840000391038.84363599997231038.73644096004321038.923999999891039.68879999992331037.7643609600581037.22778239999141037.2611673599151037.71732192018181038.48313983992631038.93560000004411038.25879999991121037.2723999999971036.04839999991551035.12319999994931033.65841471976891032.7339519997861022.73615199997531018.98406399998841017.05861119994051015.59829567999651014.52392895999091013.69014016000461013.11661984006841011.54851039996791011.09728639996041010.41942079996771008.53119999996761000.7036297600948998.9128256000593997.691561600009997.1130847999754996.6392399999701996.3470207998959996.09201407995061001.95362400005351001.2336960000418999.2932441599382997.7702944996.0118384000614992.6897391999447991.2982225599906990.3159999999098989.7724000000544989.7328000001032990.3339999999155990.5607692800412988.9724636799539981.631398400064971.7515359999331969.2302745599503968.5760911999877968.5760911999877966.1369039999416961.1669825600665959.6024000000984959.6024000000984957.5028172800493957.4257136000307957.3341180799418957.1297792000289956.5354278399662954.1507999999935953.5477262399618953.4739999999629947.8976000000735942.0115731200292942.0115731200292943.1806764800181945.5815999999918947.2099999999954957.4448000000323960.5137078399277958.8659248000631959.725185759893959.6726924800108959.6726924800108961.8968000000177959.6476787200742964.1095033599302969.8937241600444974.7688960000214982.1175763199973978.1103999999835979.9080576000085986.5892268799556985.8352646399366985.8728000000053991.9936000000187998.73158063995631001.24120000001621002.34142000001191002.98715920002831002.98715920002831004.37161840001731007.30440352000411009.9093529599811011.67787072000731011.19600000003811012.75119999998131014.74172351992291017.14927072008271014.28724320000621012.96514175998031012.26558031998681012.31920016000311016.98655616011671022.84318591998461023.90662656006761031.45045232007921042.49378640007061047.5417385599671051.7141916800471055.75959999998531056.70780320004161050.17159999988331054.64760960011521062.68633344021551070.82568191990231081.51547519979071086.98287936004071088.2984419199861089.49520000000941089.49520000000941090.83696896003931088.592432000061084.25982720011171088.7044255997991094.77665663991931104.48069807998971109.32663167996541119.49679999983441118.80366848020481115.9016000002111118.33159999998771117.21398527999941115.27540799999021113.29878271989971113.29878271989971110.61789056003641099.61039999987321110.39206271992041108.92159999998651086.26036000010051079.81389696003251079.78039999998551081.41680000000721082.87099904004531083.42089616005641083.42089616005641091.015342081092.36731695997131092.4285491199961095.71999999999121098.41839999994891095.09626303999561099.53759711994141097.6483999999841101.48761984004021103.0037760000151102.62960256005271106.66914272010381119.82942879993131116.97440000003421128.01994240005681136.43694015996131136.78016640004441140.33039999997781137.8460787199871143.79023200000371149.63625119995121151.0544780800361152.76647296007081153.51747616005671162.5241471999561171.66299072000061175.27220704025741171.62320000019051158.90241920003221165.72744704011051167.43728992002781166.94640000003531170.16958464007081175.9522963200471185.29439999997251193.75876448005471196.49639999999481195.58240000001481192.99564063997851194.26972527993921198.08644592000451204.73478687997931210.94492319997651228.48280000001731233.72084639999911238.0355833600331242.87919999993551253.17947119993851251.54160000012331245.91490367993361248.3581692800321240.5461094400811241.60240000013531264.4312000000611266.04574527999281270.44624543995541289.64167999987831308.5673491198251314.06741343987121321.17304063995831328.12108415995641328.64450240008141336.72080000008371347.42272959999351342.2623999999881348.97759999984741360.69228799989511367.80173248005351364.55968224001441364.78225871998941365.17430111997151367.95234687988861368.38554880004041367.38394879994671367.88044928002481369.81628735995741375.15736320002361362.68507663992431376.59200000009831372.62571296025161388.83737968012041392.64160000022641392.64160000022641393.121199999991391.37777407990441392.26250240012681396.69895104009831400.68639999991571410.1312000002181411.09708288012831410.48424704007951417.05135680001721425.00428767979451438.3320825597421447.1728265598991467.85760000002481489.77246111989641513.61522048033381515.29209760018171532.03120000022861550.82501696011061566.3707631999821573.41150720019371581.3411067197771595.50799999985751612.55495999991651604.09793792018471618.35363040007381644.64216784013371658.9705459197271667.49902512001521667.9801683200561663.16319999985581669.30054399995421681.73746560016341681.71461760024431681.79039999971431691.29039999988411702.51083071984611711.83160640010761723.06334591997421731.00209535996171742.76090399997041754.5019872000321765.61559999990691774.67305024013921776.6033871999341776.6033871999341779.6027814400211794.10959999996771797.74726352002041808.06080000002841815.01202879998871812.99084799999921810.23764559996931809.53520000000281809.06766848000481809.73576111999611813.24694431991881816.82582720004261819.54200000003771821.2169599999571822.20136703998671822.64080000003151823.04399999998551823.40764704004351823.62740400000961823.62576959996481823.43520000003261823.4064000000031823.1916249599641822.51079999996451816.37268991998051814.16884480003431807.18240000000541803.26039119999971803.09835808000571802.07200000000651798.65627296000621789.44688479999511781.8121763200411776.1283660800121767.09519999993861761.0811430400031760.26955007993891749.54760000003381747.33725679998631746.5273840000631743.29679999999441741.5839424000531740.8295487999951731.2681271999711723.06731136003051719.91214336005961717.72605199993681714.23200000000861706.8455199999551704.26159999992611700.56497551994681700.56497551994681700.54875679994761700.40159999995921696.93517759996211694.2347999999511691.58033920001621690.69439999999831688.08599999994021685.8684000000651682.79440000000521679.22000000004941674.38359999995241672.64000000001671672.11623743999871672.0586752000231671.2528268800251669.47960000001941666.43480000000071665.68200287998841666.50280000000071662.32042208003211661.95372032002141661.29535935992751654.36657695995721654.93000000002531655.61165888000071655.28360960000441654.3472000000371652.9759795200691650.731491839931646.91919999992681646.23719999991451645.2940000000451642.15522240001041642.29437344000551642.29437344000551642.65600207997841643.48702463999961645.35759999995121647.0262361599981644.0565964799591643.2568089600221642.88560000002331642.61560000001421642.4211999999681642.14920000004371639.88559999999871631.5207091199011625.1415999999441622.44719615990271619.61325280007831616.69240000001861613.70600000001041612.26043600001121610.43880000003081608.76095584000381608.76095584000381610.43880000003081612.26043600001121613.70600000001041616.69240000001861619.61325280007831622.44719615990271625.1415999999441631.5207091199011639.88559999999871642.14920000004371642.4211999999681642.61560000001421642.88560000002331643.2568089600221644.0565964799591647.0262361599981645.35759999995121643.48702463999961642.65600207997841642.15522240001041645.2940000000451646.23719999991451646.91919999992681650.731491839931652.9759795200691654.3472000000371655.28360960000441655.61165888000071654.93000000002531654.36657695995721661.29535935992751661.95372032002141662.32042208003211666.50280000000071665.68200287998841666.43480000000071669.47960000001941671.2528268800251672.0586752000231672.11623743999871672.64000000001671674.38359999995241679.22000000004941682.79440000000521683.90399999995861683.90399999995861685.8684000000651688.08599999994021690.69439999999831691.58033920001621694.2347999999511696.93517759996211700.40159999995921700.54875679994761704.26159999992611706.8455199999551714.23200000000861717.72605199993681719.91214336005961723.06731136003051731.2681271999711740.8295487999951741.5839424000531743.29679999999441746.5273840000631747.33725679998631749.54760000003381760.26955007993891761.0811430400031767.09519999993861776.1283660800121781.8121763200411789.44688479999511798.65627296000621802.07200000000651803.09835808000571803.11689952000851803.11689952000851803.26039119999971807.18240000000541814.16884480003431816.37268991998051822.51079999996451823.1916249599641823.4064000000031823.43520000003261823.62576959996481823.62740400000961823.40764704004351823.04399999998551822.64080000003151822.20136703998671821.2169599999571819.54200000003771816.82582720004261817.85785024002261817.35519999998661817.32319999998981817.65440000007451818.20159999992071819.03679999996071820.08799999986471820.47440000002761820.8704000001021823.6640000001091825.1956684799461825.4750387200751825.9667180799831840.47487712004161842.30320000000441842.70280000006981843.13839999996751845.4459999999481845.88519999993921846.34871280003341848.41547519999861849.01171872000441849.25197119998481849.65758176000261852.32028416001371852.52442432001451852.72327968001541853.96322047999141854.4069363200281854.79282176002021855.60440000004751856.0299185599761855.76194464007631856.19171839998281856.6784000000891856.57759999993411856.70720000001621857.59359999996011858.70527199989421860.00668799999541862.58415936002071862.55992991999961862.32291232000061862.1488000000151862.32520000000431862.41520000000721864.41526879998151864.91284015999461865.46879999997711866.10959999999641866.69999999998941867.61199999998761868.56786239998541869.6684126400111870.78042688013561873.37223663996821873.84917759994731874.97358720005561876.33557664001021879.13567999997051881.64846847986561880.98320000011471880.80679999999731881.1992000000811882.50959999999551889.36480000005991889.5159999999851889.17039999993681888.50080000009261887.72319999990711887.10688000013621886.2720486400461885.88812160006931886.24322560000451888.0936320000651890.03765119998021890.02999999999251890.08400000000961890.17400000001271889.79759999997651889.52399999997671889.18122239997571888.94679040000461888.60959999999431888.25319999997331887.37120000000031887.37120000000031886.1582351999871885.7750353600131885.1352880000471883.01039999998371882.65760000000481882.37679999999751878.33720000000951877.45159999999441877.13480000000121877.13050463998871877.12650288000531876.87768704000361876.80527232002171876.75526399999671876.7752569599651876.65501983996931876.44992640001351874.76954080000041874.29104320003221873.84449344001971873.45799744001651872.37046207999451871.54879999991891871.0375999999561869.99719999994821868.76239999999621867.93800000000561867.0544803200041859.6714028800041859.32862384002121859.06879999999231859.03999999996271858.95359999997621858.8427948800191858.61815200000571854.3231999999511853.97040000002311853.6860000000251852.14519999997581852.06240000003141852.19519999994961854.01784463994251854.60722943999831854.9039385600271855.1289184000251855.22938576001251855.28079999996631855.18360000005831854.91720000006581854.7212371199631854.56970880003841854.3577551999261854.34683999997671854.4698577599251854.31569280000171854.27327040003681854.1334752000281850.3115999999921849.8392000000281849.51520000002761848.9169433599851848.514022720041847.99686400001811847.48679872002841842.44614975998251841.63380256001531840.46533760003331839.29873599997841838.15360000005151837.13120000002371835.98905856000171833.06360000000041840.29990399999951841.33519999997451842.82278335998171844.1982752000221845.0547307199451845.0420486399711844.92801360004361844.72722576000731844.87560000001211845.12547904002241847.45487200002161847.62444208007761847.68759999996861849.29733600001491850.95701471994811851.5357176000231852.1377384000641852.95970911999871854.21559999999571854.79880000000681857.15320000001251859.27679999999371859.93601152005481859.29478815995661859.2390774399641859.19169279994621859.46971648008431859.62583263994751859.96950016004961863.04999999988381863.44599999990711863.896000000051864.07520000004931864.2795929599851864.41310400003891864.66786879997381864.88351599994851865.3855475200171869.93989376001041870.10040000000821870.298400000021870.80119999998081871.57160000003071875.0456000000081876.35007008004731877.1708052799981879.08010335999481879.17283071998421879.2946662400111879.41631744002351879.52968576002831879.67560960003631882.8886351999881882.837702399971882.6992838400481883.59635520007461884.04092640005871884.61195840001071885.39340031998311885.80083264000931886.01886.01885.90719999999331885.5112000000211885.02880000001121885.02880000001121884.92301760000281884.72880000000051884.74680000000631884.79359999999041884.15749903998971881.95695999997661881.1017094399831880.55919999999471880.4223999999821880.04818336002221879.79760000002971879.3008000000051878.97239999999261877.8417609600151877.50725759999361877.30745760000261874.17400064002691873.85999999999561873.4711999999541873.003200000011872.48479999998881871.95919999998571869.0964000000091868.74359999997891868.32960000005191863.1082710400751862.16109808006761861.14908480006061860.39799807997631860.15604352006021860.36897855995041863.09759999999411864.3320000000381865.1923999999891865.8404000000411866.62879999991791867.20142799996821867.70774352006521867.9367479999241867.9475724799651868.13162175993241870.1972000000511870.40239999999361870.40240000004451867.93786352006851867.54726352006511867.30501664003711867.19595392005071867.38315823994981870.08320000002871870.43600000003311870.58720000000951875.74877184001731875.89080191997621875.9737113600211873.97645088001721869.78538560000221869.0960633600441868.53040000001421864.0621497600461863.33901119994061862.97546880000371862.76257023996911862.47872751996421859.76489919995881859.62551007997721859.60681600004731858.57399999996461858.35080000001651857.879934400071854.3656620799891853.85765888004951853.13883104000391851.23435808003961850.6992512000351850.49349823997521850.4745953599811850.32601471995941849.77035904004241848.49680000005741846.71839999999431845.16461887998431843.8964511999521842.89499471995051842.34000000003471841.63562112005461841.62268415993571841.34070911993831834.33475968002861833.14252896006951832.13449151988881825.09760000004641824.46057519997771823.96761999999261817.26417584003911817.26417584003911817.23315968005391814.12999999996551811.32137600002371811.04265535997931811.01811.96902144002731815.0632963200111815.1920000000011815.32620736000351814.49009663998731814.39874304002841814.36885583999561814.29968399999351814.1940139200131814.07894144001761811.2573363200191808.92119999998641808.45320000004241808.01400000002581802.08071807999071801.34856448005851800.8172143999871800.44259263994041799.95538879996931798.796404800021798.19210591994241796.790330239991787.42400000006771786.64639999995921785.80400000001421777.96320000009651777.12799999995421776.26400000008931771.77119999997291770.6239776000041769.03602336007431765.99057600010381764.97067007991764.26731360008441760.0204000000031758.57319999992231757.4860000000071756.03520000003751754.84720000006971749.66498816006541748.28391439998131747.61537280002061746.66388127992791739.71999999999121738.10719999997041736.4900000000341735.20119999996251734.19286447995071725.99529471998181725.09867007988051724.24807936004821721.16000000011471720.47886127994181719.7941427198641718.39183616003581712.01359999994071711.06319999993571710.4511999999461708.85559999998991708.33315200005931707.78803856000331707.28934352000061706.71609536002781705.0976937600051703.50080000002621701.19664000004791700.31009600003741699.49054591998061697.96404480000751696.1162713599561695.23421952004671694.45505040004581693.68625888001471693.7087027200091694.45689504000421694.93262783991441694.90455935994241694.44297599995641693.69834048004371693.14159999989991692.69880000002041692.28712607991361692.3894289600991692.32363391996751691.02551264003141690.87443359993811690.80510623995811690.73024928006661690.67923007996141690.59951743992471689.31706031996331689.22531360005681689.24762928006041687.57986799990111687.19805920000391686.80403759999881685.06054464003021684.41399999994521683.6687999999851682.04879999993111680.48680000000131679.77400000006151677.89977024005821677.42305391996431676.65450047994481675.82663887997571675.86328879995931675.63947680001751674.6559999999691673.9000000000361673.78079999998071673.69080000000331673.31640000002771671.68711679996751671.2633795200551670.74457343999481670.7803999999211670.76634399998781670.49999296002121669.48055935995791669.0988744000271668.8260591999531668.0136198399451665.2456327999911667.19563775998381668.38582847995491668.18538768005561667.50624768014061666.50003648001481664.28711935997241663.2758275200221662.75431712006161662.39150240005371662.2510419200041661.75752512007631660.88866080005551659.12151951993811657.77576896006711656.95235680000131656.52169600003251656.02093311996551655.62275583999851655.23712384001151654.93985600002661654.76196127998581654.8315999999981655.68563455999081657.25215728002741657.8846787200361658.45758271995741660.96199999994951661.59200000002151662.15000000001961665.84361279996411666.2147742400121666.72892192001271667.76639999998841667.8455999999931667.92120000000681669.97163583996371670.21603839996711670.55784543993581672.4143999999351673.26039999997351674.3763999999951678.10504527995951678.97663999999171679.56188480000881680.0768000000791680.14159999991531680.14159999991531680.14879999999951680.5385824000211681.14160256004741682.19123583999271683.46910047994721684.32439999999771685.74999999997951686.11199999998761686.06596799999261686.04299280000761687.0665668799711687.51727104007611688.1297087999751688.51843967996071688.60337087997281688.56653856003251687.7679616000141687.54113280007031687.51558864005071687.6646272000261689.3622598400011695.49595519999661696.21087808002241697.26481551994291697.88480608000051698.50835199999031699.28695855998761700.61716799999541701.0227728000091701.4408508799621704.28272255998261705.28112064003431705.5438860800161705.92603904002161706.34960000000571706.59799999999251706.6880000000211706.72760000002341706.71679999999951706.65919999999141706.4455278399741706.20529840001341706.01706.01706.01706.41612735997961706.57514800002131706.78105792000751707.1830785600111707.55629199997751709.42849168001271709.49320000003081709.43920000001341709.1439999999661709.06120000002151709.0467999999811709.21335296000571709.25140351997881709.25098447999451705.56391520000881705.3776684800411705.20080192003681705.11056576003471704.98513023998341706.51418576000221706.808192000031707.20159999999851707.75600000003121708.21679999999331708.95119999998091709.30319999998191709.45800000000031709.47719999997911709.43400000001161710.34908080001741710.60217087998721711.0267999999681711.31479999995711711.7467999999921712.4684569600041712.50381760001961712.4319385599921712.3689371199991712.31345391999321710.34240000005461710.32080000000681710.37479999997281710.43599999997171710.5583999999441710.66112703994031710.41774399998491710.23191487999661709.92590912002651709.7214675199761709.64434400001231709.6095974400011709.66987727999481709.6737940799991709.8073107199881709.97594624000071710.28564992001631710.67559040000541710.97196544000031711.12134752000381711.13883775999831711.1796272000031711.99879999999621711.99879999999621713.39559999999961713.57919999999651713.75920000000251714.0230329599521715.94097823996781716.39209280004021716.78872064000831719.85649664006221720.2419399999981720.6703241600541723.35051968009471723.56976975997991723.7804000000081724.03239999998571724.28799999995431729.0856307200111729.35207391997371729.54896512000231729.71506623998081729.85974016002021729.91157440000031730.14709727997251730.34716656000461730.42254047998721730.4291526400191730.56449536001971730.62133072001371730.77877887996851731.26960000000261731.44240000002671731.75560000000341732.24160000000421732.58719999997581732.95439999996961732.8367702400131733.08479999998241733.27920000002881733.53119999998081736.49039999995331736.65239999995361736.74240000000781737.37577199991731737.46656255994281737.57039999998911738.9887999999381739.34159999996811739.78079999998481740.01371744002631740.199399680011740.48854592000841740.88929935998251741.4393560000091741.82174223999941742.03694879998241742.24354560000171742.53867359997751745.22703296000051745.5511481600311745.5187999999931745.5404000000151745.69159999994031745.9940000000211746.4692000000491747.0740000000571747.85159999993541748.52120000000971749.16614656006571749.79642160007421751.70199999995571752.55880000004341752.76039999999511752.9871999999851753.18879999998761753.5307999999941755.81483600002251755.92839440001941756.012294561756.07274575999851756.07565600000731756.02546911997681755.99679999998171756.01264448002031756.03924416002971756.09550783997021756.25219712000031756.2933609599971756.3260633600191757.38738176003151758.09255552003881759.17313152010681759.9469529600731760.53573583995791761.23874240003491761.83199999991941762.760800000031763.44065983996231765.08479999992621765.68600000001971766.55719999996881768.50181439997691768.52049983999061768.8244383999771769.09702896008481769.22069471999821769.52854944000231770.13840000001781770.3579999999751770.66760000001181771.15720000005471771.7800000000171773.28001679999081774.21994799999791775.31728240004241775.02650464000431774.99391743999671774.9815881600091774.97365376000241775.34254719996971775.51229727998251775.66756528001061775.80792640000941776.061599999991776.71679999997291777.16319999997131779.05720000003341779.0752000000391779.16160000002561779.66919999997191779.77720000005731779.76279999999131779.6728000000651779.4772000000141779.36919999997961779.32599999993521779.28280000004431779.30439999998981779.36919999997961779.48799999998661779.8336000000351779.90919999999761779.9307999999431779.9307999999431779.9524000000421780.29654591996131780.35454191991561780.4397639999681780.83894352006251780.9760761600091781.04605727997161781.10720000001131781.20799999996141781.43839999999361781.82720000003521782.16746911992351782.52119936003671782.8599867200091784.86815167995451785.21829200002281785.60584479992671786.0567260800451786.5607808000341789.11744768001681789.25985791994751789.37985023998231789.3615852800611789.26440832002621788.79039999998441786.3584000000921785.92639999990341785.62039999993431785.5412000000321785.5231999999751785.78960000004421790.50537728006431790.66464704003531790.7125904000261790.71436735994121790.4832876800011790.60024881790.76232511998251791.18336959993531791.23423903996261791.2422151999731791.2072390400191790.88283008003941790.81671679999611790.89870480003361791.8260633599651791.8997712000251791.90852640002411791.88162719999951791.8117273600191791.83000959996251791.64162448003281791.06051680001471790.9469526400341790.82408688001871790.56739200000861790.5018086400251790.4661254399891790.43772288001151790.4403724799871790.4350617599751790.48468992007041790.63559999994961791.84159999997431792.00361792.111600000061791.4628000000391791.28280000000751790.93719999998481789.9328000000651789.6411999999571789.32439999998951787.29279999996131787.12719999997011786.91119999995271786.25599999999531786.0796000000061785.91399999998931784.52684959990851784.53315968002131784.58182880001371784.85658367994981785.26900112003391786.60700159997081787.2938657599511787.52408095990131787.7655920000591788.83249664002161790.66640000003671790.72400000004471790.70959999997881790.57120000005921790.2111999999451789.64960000008391789.19240000006151788.88639999996441788.86479999996781789.09783295997231789.20095999996531789.4197507200121789.36556640001371789.36556640001371790.19279935997161791.01504575999571792.08137360002981792.32060367994771792.50712687995861792.63090352001971792.7043795199791792.84735423997271793.50119999999971793.33919999999941792.90937872003631792.85760000002071793.03760000002671792.79960064000031793.5127161600021802.93744351990951802.78895743993831802.15360640005521800.77766623999241799.2222876800081798.71991199998141797.78679999999671796.79085280001441796.21360000000371795.4648000000271794.61519999999771793.90400000003841793.5007999999821793.23439999998961792.592072640051792.84495391997731793.1152088000181793.60310335994091795.35949151991171795.84990943990371796.42463360001331797.4298175999791797.99639999998361799.77939839997481801.6985152000251803.95680000006811808.75506880007541810.3960217599951810.81820960003231811.22337663999131812.14124287994851812.25341455998861812.48603360000361812.74861039999311813.34626944003321813.9952000000381814.22200000002821814.46320000000741814.5783999999981814.33719999996771814.44520000002741814.78320543999331815.5535075199941821.98000992001431823.02755039995851824.00140800006471825.41826432000241825.93002879993631826.3743839999851826.69150624000351826.91564799995221827.36821136000191830.1738974400071830.43401472000761830.63158415999241833.66062335999141838.00537759999571838.45057520000271839.17444159992711840.98400000001081841.93439999996461842.89240319991951850.04730191995081852.00821184002711853.2575256000941855.1672524800741856.81791151996141858.3538860800841859.95199999995681861.96799999998281862.30999999988671862.5980000000551865.6744000001971865.92279999987681866.35839999992821866.9847999999581867.72640000020871873.50360000004411873.96439999985271874.45759999988651874.8787999999741875.1452000000691875.6779999999261876.02880000006641876.67679999996521877.54079999993251878.65560000004461879.82559999992991882.53279999997591885.9403999998191886.93759999991021887.98880000009561888.89959999984241892.88832704007041893.6600201599921894.2452318401231894.42922495992231895.39519999987971895.97479999984881896.44557055982481901.63495167999581903.27084416001781903.27084416001781904.36416847983451905.96239999993941906.76880000005211907.93880000016771909.83600000000771912.4532000002041913.82119999989661915.0127999998931917.5791808001121919.02202623973811919.53886816003181919.16297823994611918.96073600008161919.1574975999561919.91490304002131934.78487359987781940.26835328012881942.45724192035961945.3886239998821947.88357952056981950.32738687996271951.15464479998791956.83579903994521957.26651648008031957.9083776001521958.45369407987571959.1771731198391960.22868991982111961.08611487982331961.93621888027841969.6865868799611970.58846464012571971.5343199996761972.3210467199551972.99858976000591975.054031359761975.9697504000811976.03611520006961975.29400399971341974.81899119979361973.865836159861970.2264262399691969.6394764796891969.45601472043521973.14426080012071975.0080600000981975.49818560014961976.27673600008621977.0443999998671977.47596799994521978.9362359997651992.25760000001171993.29440000005431993.7851110400081994.04762880009661994.3887071999491994.5447743999141994.85487551997581995.09435615985651995.28909599990631996.10932576003731996.37833216003041996.75422399997141997.1421772798741997.3676639999481997.9950777599381998.35421952010261998.85391680004271999.2026905601661999.5045376000592000.3629302399051996.41981952005341995.98148495999181995.5039492799681995.25233519999071995.03335584001871994.7574935999941994.2849835200411993.45084160001081988.63163904002931988.96081152000171989.28627456003761991.25966400005651991.83449760002441992.388479999931995.64215983997541999.21454144002632008.46919999992722010.6616000000452012.06560000000472013.1024000001242014.20759999999382019.35559999974842020.9431836800492021.788835199842022.18129856006342022.8353465599662023.64641215978242025.07548544011622025.51796576000722025.62254720004012025.540733599972025.31326831999562023.42767999991882022.07855840001342021.37385727999322020.6421875200442020.01392815999432019.15037311994342017.7800345599632016.68610400000332015.34600544003232013.99352720002842013.53640575997882013.05845248003672012.61444863999712012.07798239999222011.76810687999642010.79961471999832009.22034000014742008.1696339198782007.35132799999772007.25734784010642007.67247679998762008.4464000000272008.64080000004792008.57600000000682007.50293504006462007.06696560011532006.37895520001212005.61520000010612005.50720000007192009.70227903995672011.11199999994662011.81399999999032014.1391999999592013.2051420799942011.13850800011032011.13850800011032003.48998400006872003.35139840010772003.76110240001572004.45013087996272005.75907360002882006.20836799993752006.52691328000742007.48439999998032007.61399999998572007.16360000001762003.5647999999982003.1580000000022002.865147200032002.60122720000372002.3729483200452001.99750287996742000.23958847996622000.36806527999832000.37245439998862000.33227264002382000.05920399998221999.6224000000111998.74399999997791998.06719999999861997.78999999995651997.56680000000851997.29319999995731996.78199999996921996.42559999999911996.14840000003411995.9323999999911995.86760000002691995.86039999996821995.64760000008161995.29120000000941990.39160000001271990.80559999999061991.2175999999551992.14320000004931990.77876480002921990.0687999998991990.31080000004891992.8319999999391992.3747999999421991.60439999991741989.9321427199731989.44061599987821989.0812423998911985.51036544005551982.1403999999771981.43740159999381981.05773120000161980.32155711995481979.90286559998251979.48600000003761977.16415296002741971.82114623999541971.14428431999071970.34380992007351969.69703392007271968.95767616002881968.02349343997231967.47239679996441966.76792000003751966.18146559996221965.5096883200261960.16438528005921958.4764739199811958.00019504001351957.60920000000941954.1880000000171952.849226239971952.03184191995871950.79530240000261948.780592640041946.64959999990381945.53359999995881943.40960000000091942.28691519999871941.21176800000281941.13796032001941941.13588671996631941.02408799997331940.85161344000881940.5734039999871940.36047840005951934.73890048006461933.47514495994051931.51060480005371930.12262912010921928.5961600000991923.77880000001121922.92560000001661922.13243887991031920.88658400004581920.13595520002131919.25656192003571918.61112223996571916.54320000008331915.74759999994331915.49200000002591912.57956832009861911.73451199993861910.53426047996161907.77999616000781907.09463680008141906.8802495999521906.41841280003131905.82025631998611905.65390751994191905.54963567996041904.41159999997441903.74734815996541903.73222527999521903.73222527999521902.28811999998241899.80682432001051898.87857999987571898.03714623994371894.77767615996571893.71903999989351892.80543488003221892.11401599996821891.65933600008981891.50397440004831891.15968768011890.65496479997861886.57759999996281885.04255295999811884.57640000001421884.31460447996141884.7236163200331890.0842873600111891.0371267200931892.04558463996111892.74394496017821893.85651200013261899.21694720005781900.55093439997151901.99429952006721904.07627888007531905.61336656007531906.63800000003081915.28757680013951917.329784639881917.97480000002271918.39960000012681919.65960000006631924.39400000006411926.1219999999221928.19199999993951932.64441933.9260000000641934.48400000001081934.90838527998131935.18240863991041935.40339679998491935.25342079989671935.37944240000891935.64967999997271936.44252480005751939.97551583998191941.28655808016771941.74042591993451941.9843244798951942.06679999982321941.93359999995481941.3179999998211940.32840000002581940.37880000002631940.64520000009541941.13120000001961941.76119999998951942.3480000000171944.8748000000751945.50839999993331945.9115999998621946.29680000004051946.27880000000911945.98078016000071946.20300528005121946.79140800009281949.05450336001331950.5463915199931952.02243328005281953.6615751999511955.74783519996621958.32642176012631958.87284703989641959.04540224002861959.2699999999711959.30599999988021959.840800000071959.84080000012111960.10000000002951960.38189824011491960.84431968004381961.73416575992041964.06224127989121964.95944767998321965.89479679999681967.59867391997051968.7334880000081969.15641600002051969.59439200000821969.95119999999631970.1987999999971970.23480000000841970.89559999999141971.07559999994621971.35279999998831972.14120000001841972.404000000021972.55462399995961972.74498048001831973.03381280000871973.1203366400311973.33575936000171973.65070607996361972.22786079995261970.77945983993231969.65668512003531969.38071344008471968.7100527999811970.80719999989011968.28817151993371968.26978560001111968.3856479999631972.6498332800191972.98759999988921972.92640000004371972.18688512001021970.06320000011781969.37199999986771969.52652544007881970.24747008001371971.4024249600111973.50255360007781973.85732928008161973.73195903992561974.51924799993661974.29333647999211973.87898943993221971.43218880000041971.0922912000731971.0548368000631973.97640191995631974.43847808013521974.56513471999051974.46391999993741974.30166080009461974.47703807999071974.7363849599161980.06295679990541987.14319999995931990.2760000000331990.65759999999051991.0947552000831992.53758399993261992.47309632002611992.33082143999951990.24132639995131987.7123999999591983.65132320001541982.82751343994121981.6763999999661981.36071424004971981.01409888007721981.26982496006171980.71800000009741979.55159999997271975.67520000001131976.19359999993031976.992800000011977.05560000003451973.93799999995051973.66440000002741973.613999999951969.66320000005521969.53359999999881969.43639999993731969.23120000007161968.87479999997371968.47520000006171966.85439999994921966.49080000002321966.20280000005961965.9543999999961965.59800000002631962.89572928000781962.26146399999741961.96314495998041961.85608527998491961.63199999999351961.6608000000231961.75439999999141962.06025312001541962.9995147199991963.5522931199911963.93483200004131964.21917008001671964.12879999994241963.1399260800081963.1399260800081962.71372208003231962.57054656000561963.23470336004741963.51806944003851963.57444400004281963.48086272003321963.20097343998281963.34439999998521963.585599999991964.22314624003161961.31215615995441962.12465343997841963.19207279997731964.14180608007361965.01952639998291965.55400255998691965.98974800002471967.27400000010651967.82614400009221968.49424640004531969.48960319996921970.78159999989321971.4008000000181971.77519999999371971.83999999998351971.48000000002281971.1631999999531971.24240000000871971.63935424013681973.62050144000771975.03840783991021975.93696928007531976.21407855991741976.2437094401011975.8925481600041975.59359408007821975.33654976006481975.32467840008531977.47966528001031985.55726240001971986.2647999999691986.85160000007341987.42039999994181987.8623312000471988.48599999998851989.82520000000931991.8664000000231992.56147024000191992.48707840000591992.4981721600161992.53840144001171992.7121719999831991.70481264002251991.60800000005681991.5107999999441991.58639999995781991.69080000005241991.22151760007071991.18369599993661991.0797625599781990.86577279991981990.49274943992171982.76399999989481978.35102720002031978.53546672008431978.84780703989741978.93509120002361978.93164816008431977.77408831993781977.14806272006331976.38284240001741974.87858095993741972.76735520003131971.5781414400231970.73071871995761969.31416480002241966.6323999999591966.2963999999781966.0012000000071966.30559999999761966.07080000005391965.91960000002631965.99159999994691963.74240000003121959.49877296002251955.8128854400841954.52885824010741950.39690640014991944.67012608007851934.94842239981221934.5608000000121934.33039999992871934.02080000004531934.7059999999981935.22912608001871936.95200000002021936.7215999999881936.79360000001091937.22559999994341939.67583487986921940.0810897600581940.2617536000291940.1589735999951939.87159999994131938.83120000003561938.19039999993951938.0464000000731938.26854880005111938.74559199990471939.19516288007531939.28497280002011939.1706079999861938.8341312000231936.58629696006531936.21793840004531936.16963359999681936.36450303998121936.5566105601111937.58022944003621937.8048895999661937.72286431996831937.35172736001461936.46905184001751935.72473743993871935.30303199993731935.14700224001541934.10400000009331933.86279999993491933.57839999998781933.23280000004181932.81520000002181929.50319999994261928.7328000000461928.06170751996641927.30281600006861926.81554304007181925.92063920006421923.52810720006481922.61268479992261921.11349183990271918.57076496000221918.12799999998291917.77159999998731917.48800000006121917.41599999998741917.32599999995861916.3413756800041917.26239999996961917.42439999994531917.53240000003071917.51439999997391917.38479999999411914.74880000004121914.38880000002931914.10080000004021913.66058815996961913.46074640002371913.24858399999471913.1266711999871913.1266711999871914.39840000001251914.29760000001121914.16799999998031913.84399999997981913.71441913.6640000000251910.17056191998271909.87319039994281909.449024000021908.87927791995341908.21493279994661906.25535039999181905.36400000002251906.28959999999421906.71799999993571907.60399999998071909.6848000000991910.50796928006871910.7229440000791910.72714159999651910.57733360003611909.98512640005971910.56518240004221910.25279999998521909.43258719995721909.5409760000871909.86741375995731911.5037145600391912.25625280006221912.96854016008341915.74330959998721918.01600000004241917.97874048002931918.20492272000871918.6657472000431919.73996992008121921.55164832009561922.97431936006111924.49693696010331925.69904096008491926.37506384012521926.05812991995551925.90251056013061925.82582047991171925.89750079985241927.6641331200471927.97632512003521927.93330944007061927.39899359995051927.45766783997831927.74021375987171931.7342918399241932.6704243200861933.64881792008921934.4508000000421935.49839999998061935.69279999987341934.52640000000471934.44490592016451934.67458591986381942.88098528012371943.77121536012921944.55220799999281944.95229472003671945.17474880006421945.62905727996261948.02166784002841949.17051999993851950.28543423995121951.17534991993351951.59960127997741953.686919999991954.98598719989511955.8622271999911956.822845439931958.34387999998421959.39991167997621960.17642159999561960.72133632000031962.25974400001161965.04619295999371970.1335343999941971.42601088001041972.8820870400411977.90679999995521978.5800000000461979.4871999999811980.6428000000821981.13959999995311981.36999999998531981.60759999992251981.85239999999521982.5853884800381982.74299935996371982.97497471998211983.35079311998671983.82153487997581985.3557699199571988.42599999999951989.50600000000991990.57160000003071993.1083999999711994.62039999999071995.54920000002451996.02079999995931996.46719999995781997.0684288001041997.61666560005281998.1408486400672002.67480000004072003.5243999999422004.78079999989062013.3476751998912017.62911584010132019.49350399996252020.5102332799972021.24979136004692021.75263936003782022.21732864001072022.70871135989552023.71872880007322024.09313792007772024.45400191998762024.90164511996322025.36369504006922025.95010048016122027.0002492799782029.1699174400412030.77980576005072032.67068415998532032.69082623999362032.2902067199832031.45443359998442031.28854911999572031.06725568002182030.97051679996232032.22812639999852032.6189567999942032.60561920004942028.81990112005062028.57440000002432028.00165440005412027.39879999986442025.7632742399472025.01658384003032024.91679999982722026.04301439997152031.33599999991772032.83720000004132033.22960000002242032.97282030.96615040006712030.8783521600052031.04897600006642031.68630272003682032.17043072003782032.15233279998922031.71495199994232033.7196416000112034.88887199996092035.64876864004622035.96816640004932036.15408479993362036.1703308799952036.09041088000752035.7705465600242034.34913823999022034.09264831996852033.99566719993872034.08369151997862034.36978783992412038.45434687998022040.49772271998542042.41807824000562045.5662560000132046.76167776006562047.19789695987522047.35103232013242047.21480000010482047.02236847992912046.1348892799482045.99733760007052046.02018464004372046.27692512009822046.81471039992682048.35796128006362048.35796128006362049.7243999999472052.2228000000082052.95141599991582053.87138735997042054.41126207999652054.68217360002152054.8737425600462054.97578239994072055.0131561599892054.4066843199032054.4080000000922054.796799999982058.72603263992642058.9100817598592059.3488108798872060.1074575999962061.0760000001542065.9360000000092066.6055999999042067.10960000001252067.41199999996572067.5980806398922067.66570303991882067.71400063994132067.86554624004932068.3244512000392074.13999999984162079.75638240000942079.63320000005842080.47560000008072082.6759999999342089.11639168012742091.8922119999282094.30080000002732095.34412447999882097.00340479991472098.260061440042099.86999775992672101.91214880006652104.749430080062106.9797222399612108.6091456000022109.6918195200072110.37993935998252110.4898560000042110.54113439996952110.189159519952108.65760000003142108.1896000000872107.89800000015842108.0275999999342111.2259999998342111.36640000004262111.2619999999992111.15760000005782110.92719999997462111.04520000000142112.0380518399182113.18896575993452115.7185446399622117.05225983996842117.3149144000012117.4880887998352117.52251200001052117.37233440006282116.7210007998472116.5394700800732116.51089183994332119.28320000001082118.9088000000352118.2993268800842117.9924390399652117.8689881599572117.9659750400832120.69629439997742121.040260000012120.8903471999742120.18275999984052119.4130439999662114.29959999996842112.39086383986072111.77472383990932111.45120000011782111.5347392000942111.4601932799392111.1161660800112109.2313479999662107.87812960001472107.2096239999522106.28376592002452105.0250086400212103.62999999986962102.96228095997282102.44934143986172101.1308342399982100.08500543996572097.65600000012642096.90719999997052096.59264000005032096.31408639994562096.0407441600442096.0053503999412095.92113920005572095.64154880004032095.3767572799472095.0166680000042094.66479104002932094.3170080000432093.71515136001842093.07441759996532092.10887808005152091.3402579200092090.7273075200332090.29314176005352090.23659296003872090.5200742399632091.11407999998662090.86530367998742090.66005760002872090.5066572800292090.48083520003552090.7366540800052091.43890031992032092.39770704005062093.05042879995472093.6040339199862093.9914831999832094.43111232005232094.8553132799272095.35611072007532096.20428800006272098.87005440001762099.89569152003832100.2366086399312100.2928000000532100.5951999999042101.30799999994542102.1395999999952103.0913638400252104.449592000022107.12070848004442108.32117599997262109.19567359989562110.6631737600042113.0488851200412114.41710399996462115.12239999994242116.0752000000742116.67999999997942116.9823999999322117.42879999993062117.7743999999792118.2351999999412118.9551999999652123.69865824001232126.6528000000472128.5464000000242129.0575999999612129.42479999995482129.73439999999162129.6356764800692129.64823760001952129.8242761600152130.05948319999422130.9796352000172130.76080000001782130.43680000001722128.81719999994632124.4935337599642123.73969519997762123.22759999998832122.46170672001562122.186387360012121.74589280003872116.53968944000872116.28352640003232115.99886000006152115.59827071991372114.23489312003682113.9092443200272113.7045785600292113.5337196799852105.76593584008152104.26079167992932103.61680000002932102.95439999996232101.86719999994472100.78000000005482100.1932000000532099.62080000003972096.317571682093.43929727997062093.43929727997062090.8505472000422086.51571759995342085.13963775996852084.59587504002272084.3693817599942084.1331391999632083.7048716799232083.10556384005942082.5851999999882082.13598912000772078.93160576016132078.19508319999252077.75843919980072077.4229393600732077.07536896011242076.8999251197242078.82534912007532079.98792176000822079.991312960012080.24339360018082080.7328755199432083.94078079998832084.5869952000482085.3421873600262085.78244576002772084.19012927977252086.4174323199072087.62568319997542088.17631615994742088.8965465599542090.5300019200032093.098410880032093.3799423999722093.56963072000332093.73321472002142093.9135200000172094.19114832000332094.92525600000732095.20826464000862095.39335935996722095.63032576000342096.0291179199952096.25989375998552096.44897152001752096.6037600000272096.68353167999572096.7421940799742096.63972800004382096.52759999995852096.22160000001442093.7996200000292091.30479999996442088.5548671999872084.87918399994762083.7211619199612082.4724976000472076.3356000000742075.2960438399532074.97986160005082074.88796511999272074.9025497600732074.20040000002252074.3624000000742074.29039999997442073.9880000000472073.84399999997552073.9520000001122075.474800000132075.7699999999222075.95720000011442075.85640000006152075.5720000000132069.98879999994822069.2193719998722068.81205936016072068.5004404800162064.56856063996432062.49827135998482061.55720000000662061.26560000005252061.16119999995772061.08919999998622060.8948000000422060.5366431999422059.9790924800662059.7756636799762059.73921152007962059.9002726398662060.0485494400592060.0701407999852059.6365103999792059.38089600009152058.8351647999112058.1802499198832057.2659929599382057.2248895999712057.12281088003372056.96996352003132056.66737920003422053.10108976010272052.6289999999532052.52240000000352053.39000000008952054.2972000001012055.28907696004672055.82340127997942055.88204240003962055.90599999993632055.90599999993632055.58199999998672054.91196175993352052.9615758400582052.771804000032052.72111456008132052.79845120001852051.4620233599552052.0669155199642052.58459264003842052.9005663999862053.027343999912052.88205952009142051.20048640003232051.3613919999872051.5541964799962052.3017984000972052.53355824009672052.81419839999852052.51709184008452052.3971600000162052.52069471991262053.03622112002772053.00794959995942052.56615760006072051.6015999999282052.07079743995432052.35080000001152052.15640000004172051.8971999999292049.4420000000592046.82831552002272047.37337600000392047.7721910400152048.82080000002132044.6356500799762042.04100095999752041.73247231999222041.5004768000142030.77919999996542029.1425183999862025.93826208000362017.98028799997952012.55799680010142012.0899392000162011.5925344000882006.84639999990552006.09231295990552005.83399999999852003.53659263989952003.37992064000352003.2648768000242003.02367679995722002.34690559993492000.69410815992731993.27599999999621991.61279999994921990.09359999994791987.59519999996311985.10028095999771983.96172400004321982.6331180799971976.76179519995461973.70713599997341973.70713599997341972.13795648000561971.539008639951970.992127999951970.13997920009071969.37676800001871968.4343110399771967.4613491200751964.63165008004581963.74549279997451958.9965076800141958.85020799995641958.71622463996621958.35417119995651958.22512560006321957.95556480005671950.64559999995661949.70960000006881948.84559999999921947.06720000008951946.38054320008451945.32465312011031933.12375551997121925.8162848000011925.5908240000241924.0644447999811923.74474607998651923.32463471992881922.96098431998351922.4618284799611918.8009848000791918.37932544007371917.71049872008331916.0571224000781914.264000000031913.69520000008471912.3200000000781911.00839999995291909.61879999993131908.19679999994041906.43999999997621904.35559999991821903.68239999992971902.83999999990781898.476400000041897.66999999997851896.58999999999381894.24640000000181893.1627999999751889.87459663993351887.2005555200391886.50046927991871885.6728259198861881.17361696007771880.56750751994131880.06054399995221876.1254329599611875.46196880007461874.49868224008881873.5358736000421868.8816524800121867.942714880051866.9006339199571866.13440000002561863.36239999991311862.59920000002381861.7712000000171860.89328383998741860.38710943998221859.80154496002571858.9100352000691854.17110015994541853.56125408008281853.08186800008181850.63468161850.402477280021849.90617008005281846.57999999999291841.96239999999171841.63480000002571841.32879999997981840.88240000003271840.29199999998851838.15493631997951837.75520000001551837.48520000000641836.94843759999731836.78152000002821836.50905568001761836.25497055999861836.0562505600241835.45083632003481834.9087137599421833.73973183996371832.0034246399651830.3901872000221830.11838719998331829.9407999999911829.7031999999771828.51247647996481828.02162943995811827.18200000000521822.89919999999461821.86240000000321820.73920000007641818.98123903999981818.2206944000231817.24929344002071816.27157375995081815.56632512006041814.79310127992741813.3523999999561805.66360000000331804.97599999994871804.32079999996591802.91319999998971801.98079999993931800.98878463997331798.01679999998351795.0143999999821794.47426559999261794.4317798399921794.44971071998981794.46832415998141794.59179840000641794.60417520001721794.57961023998381794.56389376000671794.97499791997531795.19770896002111795.54053119998571799.79683712005821806.16790848001851806.31835824000131806.47225103998311806.60841311999891806.84713055999711807.4672809599721807.87197855999721808.1164000000241808.56280000002241809.30670624002731810.15349248001461811.03621440005461811.59604607994951812.1493804800091812.96287055996781813.62879999998541813.89519999997791814.27679999998651814.6007999999871814.92479999998751815.42880000004521816.14879999996671816.92992383995351817.552519680041818.43926160001461820.42719999996641821.67640000003551822.49725471999251822.61679999999661823.70668607994681824.8527999999821824.7350211200241824.6327292799971824.5842156799851824.7093272000161827.99974719996271830.15599999994861830.3791999999991830.62399999999481830.9623999999591831.34319999997931832.1424000000591833.12880000007541834.40679999996951835.44331743996491836.86346992000151837.37630287996971837.83752192000271838.5771999999431838.87959999997251839.31879999996361840.48495359997791841.08158016001131841.50163679999721841.72573247999341841.8708297599981841.994457921841.8947379199841841.45621760000881840.89195135998851840.9407999999891841.22159999997071842.5607999999661842.72280000001731842.7119999999681842.8946608000081843.67141983996951844.19146287996661844.48639999997751844.9327999999761846.28498687998471846.64093759996541847.07820800000381847.5630545599791848.22594832000251850.14400000000271852.22119999997651853.9911568000041854.29468287996561854.61212800001071855.15320799998181855.65192607996911856.13680000001271856.55439999998161856.99359999999821857.33919999999531857.7784000000121858.40640000002761863.2159999999581864.1232000000211864.84320000004481865.56319999996641865.70837951998211865.47576479999271865.60319999998841865.71839999995341866.43902000003321869.74852447996821870.0209004800281870.5233020800621871.30193456001711877.6109216000951878.14779776007161878.29256672007751878.28300800001421876.13200000008961875.9670745599281876.13268319996061877.2748048000551879.06085119992461880.28599999996641880.4695999999891880.7575999999781880.74079999994551880.73184784003251881.15324799996261881.27023936004281881.30159999997161880.5564000000371880.29719999994951879.91796911997061878.71995392005621877.79410607997061877.9463999999351878.34240000006071878.86079999997971879.58012160004361883.62642240003861885.72453263997731887.9432473599751889.96892992003021892.33881424002861896.56892639996841897.78719663992961899.18239999996241899.34079999997181899.41279999999461899.76439999998321900.80120000010241901.24759999989621901.29120000002791900.85920000009541900.4415999999221899.62079999994781898.49759999991871898.23839999995931898.09439999991351897.23040000004861896.93953536005921896.60041680005341893.35597168001911892.2157417599441891.97942047999621891.94933599992351892.0837398400421893.80287935993531894.0129839999351894.10140000006031893.22439999997321892.8535999999121892.63760000009921892.61960000001681892.71679999989921892.9050623999271892.72637856007151892.25868095998131889.69279999984881889.69279999984881889.548799999881888.42199999998771887.74519999990621887.46799999994071887.87063552000341891.1551603198951894.6596000000031895.21400000003561895.33637151988961894.79695904011491893.51144511984721892.54959999993071891.79760000009511891.85159999990761892.0496000001241892.46000000000871893.55736319991461895.98839136018641897.1919999999241898.2802809600811898.78828047991899.68841151989391899.785528000081899.59480000004171898.9252000000441897.99618399999921897.53351343996221896.94440000014971896.00480000011751896.26050080009991896.22630799993161895.86784447984861894.75572672013821894.58477279986571894.6828569601281894.98980735993091895.6992841600241896.7948000001621896.22240000007241895.45559999993661894.69105983978921894.63615408007441894.82279535987981895.18777919995451898.55200000016431898.91083968002451898.96259328010481898.6399454399491899.89000000008631900.92680000010341901.45599999991831901.46680000001881901.68999999996691901.87639040001341902.32414399997541904.68428959998531905.18746880002731905.66077952000181906.62640000003921906.4932000000431906.37800000000131906.2160000000011905.99157984000341905.98358400002081905.76425088001321906.155628800021907.71501199998351907.41108416007431907.2527999999671905.36545023999631906.25050752007221905.1221871998361904.80425535987141904.51014207993561904.33012480019081904.85922687987931906.70688399990971913.38319999996251913.81520000004841913.8645811198761914.3355878400831914.42010720016291913.87007360008721911.9061137600491911.62341135995231911.70402975996921913.24709056006711916.19688127987231916.36479999994531916.20640000003821915.65919999998731913.46586112000161912.97736127992361913.01082112001561913.28888511986881914.00563935995391915.27662080004051919.95557775998991923.79692832002551926.54808000004751927.2939999998721927.1320000000251926.21399999991291920.29374239994651919.7537062401241919.48965055999861919.34869952000551919.57389247994551920.33313535996671923.67423199993461932.32439039997331933.79718672015931934.35307999987621933.77719999997131934.03960000004241934.41429119995221934.275450719931934.0651891199591932.78680000009581932.39800000007971932.25760000012721932.08187007994961931.92817023990321931.66194160007421932.1034252800351931.79957823987271931.74438159999041932.21800000001031933.59172736004831933.88503807993521934.10329311997951941.07494911994561944.787599999911944.30879999999341945.42840000003161947.91174879987371948.28851904002611949.1671321597781953.70980720007061953.70980720007061955.70355200008251957.63655167979551958.13120000026721956.89746816017921953.66194159998531955.51569039998551956.35353855980131954.02327104011261954.28140287997991954.61245647994521955.00619583991061954.74584960005861955.42869232003071955.92760624000151955.5700816000271955.2644200000251954.75861423998371951.41320000012271945.44709296006771938.60705151989331935.89440000018821927.9784000001161925.38186559986751925.8502745598441926.97199999996471926.64559999976221924.62599999989881924.66105120016121924.66105120016121924.18861887991351926.01759679977181927.5033291199691927.99624080000221929.87788735996361933.67839999999681934.51360000001431933.26930479999921933.21519999996371934.02700096001331934.1054636800051934.19854240001381934.473200000031934.10001680000911932.97174560001141934.43711791994861936.89640000001851938.75508223993851939.69079999993161939.8917519999191939.3857144000081939.84057439989941938.77640000006611937.82774336013091929.40560000007211925.65137312015551920.74880000001261918.09559999998371917.940800000121918.13581840000281918.921743359991918.9730073600171918.45425279999361914.28032383998241910.68555904002281907.7945849598111907.8157734397361909.06356223992361899.4254950401231898.13321023977231895.94770128016421894.97812400012691891.22491807976461887.71941344018361885.9413696000891884.11582688017261881.96820720010121881.15217920004081880.0454556799891875.27560000001221870.88343999995461868.3086582399551866.07932383989741861.39839999993481860.4732000001221860.21637839998521860.6765206400051861.1235999998831862.5513049599551864.27715359994321866.30319232008631868.61628160004881869.01724383996981868.32408543992621867.45303807994561865.60947808005361862.83988864005071860.2600000000951858.17119999994341855.89240000001811854.3338342398971853.0188262399511853.83789599987591854.9309696000311853.18198575999781852.69091280002681851.67879999989581852.81119999997331852.65602687999671849.8492576000221848.14040000012121843.43798719990651840.11280127999931840.0598496000411840.40439151993451842.21948159993781842.67035136001981837.94235776012491832.60416928006021831.07679999995411830.1508646399111830.2139999999151827.9307059199511825.21469056000661823.31067167998411820.16167167998721818.6327027200211817.0018713600591816.19844063999561815.80271999999151816.00056447997081816.2597510400271816.6830016000021816.03164032002131814.57898752009991812.7193830399621812.25394287998261812.5454736000561817.00454880006831817.9579065600251818.48197840002721818.35444479998681817.60319999990861815.5832300800911812.62835200005661809.30668160007831807.2639206399361804.565600000151799.12749999984251793.67525632003341794.32635087994281794.82290176002311795.52183855994921797.08535167999621796.4692000000041795.63026015999141795.63026015999141796.4692000000041796.77519999997321797.8228000000141800.33991871999021802.38220159996221804.0389631999251806.41748607999561809.0735134400531812.90384143999061815.9564000000281814.74158815995381810.73014847995111810.19731519990321809.90637183995021807.00172400010321806.06432000006071800.75544079999441794.64855040003181789.07494336008381783.11421951994021779.90660799996071775.57166079993751772.40221376001231768.51109615996321761.15962400001741756.69268703995481751.56368639994751748.90399999987681744.65586847994361740.82234495992931736.1914400001381733.60490624006391730.07285119991551724.44487040019381719.20993472018841716.6148793599271715.09102528002951709.93259039998181708.22489440004921704.6282350400171701.53597760006821697.36375039996071696.17873920006541694.32370400002291692.536195200061691.06743872003651686.94840000006621682.87908255990831683.72106927993171683.02680000016861682.34861983998231680.53280000002591677.97212688010971675.84793535998531673.84026480010271669.99585599989361667.45073520012871665.25834240012361663.49232543986861662.24619263978841661.97068896008771663.63968688015871665.50878735975361667.10874879995661668.3825088002051668.4571363198661666.32559999998421663.51147871961031661.16782319980761659.24590784026671656.3343999998391654.16589599968691655.7415721599281653.13759103994431648.93781999996741643.4445375999241644.0158412800391642.01999999998751629.07405296033381625.11443200016631621.15584319972191620.98843360015641620.10503424025111621.15879999989171632.01312927989531633.89322240006321632.42853216017421629.59560576015591613.4013030399681610.7326924799311610.26863807977951607.11158911978211604.7950457598221603.04621855985151602.72890239992741602.50964224006271602.29958224009851601.89039999986861602.49761536018151603.06335680005961602.16307456011711600.57319999994271599.0863999999621598.05977215998661597.67692400002581597.0143295999981596.50960000000171595.9599999999751595.43080000000651594.62080000003061590.95484799993871589.74802720003051584.40365472025221586.24539311997021587.09517263998961586.58200959983261586.00450879998241583.30187647992421581.64182816004471581.19731744002661581.35342080012151581.62313856026961578.67227519987521577.14659615986421576.75640000020351577.55560000002741576.91199999977311573.83074751981761568.85623743976661569.01632512000951566.37509504004381565.0972716800511566.36222975990131567.7377356800441573.42579327989761573.8680000002031573.58000000006041573.06159999983451571.9095999998781569.62840000016011567.95079999994481565.52700159985121562.84776640016141560.8795628800041558.3242540799011557.92213695987311557.4344140800941557.2129104000481557.6700000000931557.70826175995031555.48972399975761553.83040000008211554.28413696001171554.07342463995721552.6153838400461549.28503711996561543.1648448000071539.58438895984771537.89301631998661537.98470400007051538.4335462399991538.67701376011361540.38201535992521539.80952144002051539.82539135990331539.82539135990331541.0155686397371542.21506560005791543.01153823987741542.99040000009651540.83760000000671537.25360000013821534.40600000006321531.80485503985261530.81774400020781530.57820095991131534.90428799974441535.4245759999841533.476880959991526.97998176001581513.83900160012721511.503752000151507.81280000030071503.68691199985231500.7667392000351500.0796441601781503.34363008010971505.3641363202181502.09874623993731497.60296384003481494.5606163203111492.01602911972851489.99940704016011489.05088384023091489.0428313599831489.15875135989521489.23416832033741488.21930240000351490.9591036796981490.50710432028841488.2977920000681485.8702159999491482.39517055990771482.44359200010851482.4374415999171482.3591214400231478.418402560141473.38278399991241473.36797695996821473.28507616005141473.2247847999031472.6869865599891472.6327999999031472.65440000002951472.2080000000311470.58723776008971469.55351040004981466.58880000002771464.0700924799921464.04985871980971461.13004607988021458.55781680008771456.55318944000491454.07920000011521452.08684735993421447.50559999999521441.83437312014081438.78318959988751437.08079999985421434.23089472005381431.50991680003341428.71884192000521426.36065568000731424.68024480010131422.3528009601241421.13485471994251419.51619904001841418.370186560011415.12260608001431407.76102304006871390.2132147200771390.21999999992981389.5288000000891388.56640000002971388.2767361598991388.03649663995771388.13432879993731385.95868480005221383.26320208009721380.65135312005371378.01983808010551373.99991680011751371.86471935995661371.12385536004351370.69200224009111371.35039711991071372.14321792002081373.43120000008341375.20960000004421377.49199999990921378.45275840002891379.31019456009591379.76799999994431378.95380879994971378.60705343996371378.41062783993241377.88054080002121376.95596288007911370.14840000001551366.79280000006971365.6074553600881365.9022929599011366.75289600001821367.55648320006941367.01536575990281366.40434943989341364.8166272000281360.21574703991451357.7575091199461356.99884544005841356.20206463999971356.15691056002451355.90829824001321355.45798559995821351.97167200011021350.74516095996961349.70993983998641345.44293696001321343.49156799990351345.4569907199051343.69300479993151341.3168000001171339.71839999985511335.31199999992991331.92180095990781330.889361280041315.99238400005151313.41200000001461310.55799999992541310.71371600010571308.17958911994581307.87654639992931307.13657472000431301.8602303999341301.23082496007171301.23395839985871301.75040000011181303.78959999991841307.08974592007081306.619454400061305.13928128006981300.26842431990121295.52106400003911292.345151200091289.82680000009211289.3336000000331289.56200000013041284.53679999998741282.24951680005921281.0428851202031275.33998639993231276.12054847990341278.063199999911278.27440000017331276.0362799998281272.7014835198921271.95158335992161263.21796735995241261.35464960005581260.18428288000931259.58316352008731259.1155379199821257.0198092799551257.29119999997871257.08959999997611258.3640031999861258.32845760000831259.08108656001421259.24914319997651258.69589088004361256.60199999983321253.31880000003931249.36599999988971245.38409664004781244.02251775991771241.49919999994931238.2971443199361235.3623999999051232.2375999999671230.59310975999191228.56102207996541226.0919014401531225.20083071988261227.75399999986121215.4415657601911215.31519999995021214.51599999997281212.69439999989071206.09307663995471201.97740816011881198.89283839990551196.3676755200581193.81588767999141193.1869446400511192.42920000002191191.1727999999711189.78320000005171187.56919999999561187.0075999999811186.75666735999271189.19563887999861189.9300144000121190.6168825600651190.6168825600651191.32138383994671194.1444000000021193.2668000000081192.21199999998541190.59079999998311189.2983999999721186.87714783999381186.13832128000171184.83280000000171184.20160000000261184.24840000001221184.55799999999821184.7127999999911184.83519999998881184.94571327999531185.01185.01185.01183.81795455999281185.20354640001761186.67717184000841188.10359999999171187.13219199997371186.49079999998121184.3611999999811184.08476847999441184.09287680000331183.87456320001571186.23408896000071186.23408896000071195.24359023997341195.62256512000341195.0104153599891188.57263999999991187.13718784001641186.91055360001721187.78564352002791188.93170847992271191.0746240000811195.2244903999381190.01196.92599680009381198.00079999995141198.00079999995141197.46079999998461196.286898240031193.44823728000961193.36898688000881193.10826911998631192.39679999999521192.34720000000631192.63519999999541197.77450000002981198.3360928000121198.92740575999781200.61139487997321201.03304127998351200.92979680001871198.7509033600321198.73342463994481198.73342463994481196.40119999997931197.58292159997591198.11040000001531198.76559999999811203.46777920005181206.28900415999621212.88479999993271216.19170303996571219.26386879998221218.07280000000171221.492495200051224.23450496002721231.3119999999831233.66392512004651235.6432000000841237.59040000010961246.66574719993831248.153683200011248.68149216002941246.893600000021247.28759999998581254.0459999999961253.66800000000381252.85800000002811252.5063999999821252.1403519999911251.97439999999281251.16799999998241250.02320000000781247.42200000001641247.17000000003871247.17000000003871245.4145375999961243.746875520031242.254400000021242.04920000002651242.29749408004181242.60759999998481242.29440000000841241.81692095997351241.72560000002931240.92381279994861241.59538559995941241.26960000003121238.05840000002081238.1016000000141238.38400000004281240.51519999998251241.05305600002181241.43239999997061242.0623999999661242.68879999997031245.47885184002441245.90634432007691245.42909248002021244.12263232003241247.45055743999931247.0413107199691246.32306240000961245.56439999999981245.1544000000141244.014192960011244.1628000000061244.38959999999591244.68120000000161245.12761245.57219280000341245.89631231999721245.94879999998351245.02199999999081252.1262776000561256.97273728035611261.30638160011971271.6042294398551276.81838464012341271.02808223974221270.65312639977511263.39109039984421263.4076518400971263.4076518400971269.69719999988141268.24298367983651266.3595999998561266.61562623997681263.2335692799831270.0359999998581265.64885759983081279.2944000000421285.76093600002791273.48173920017171267.92095616005621264.6530239998231267.8429412801161262.94630847996361263.58228480006191262.36614144008721259.4935257601411261.25681440014891261.29800000002071260.52136303993961260.7763999999791260.30334336001441259.770441259.45666400007831256.89919999995781257.59978064004031257.04679999999441256.6807999999911255.90680000002681253.58479999998051252.97960000001331252.23079999995981251.91101696005561251.71894400008571250.72182463999681249.78401599995141248.74808384000361248.23192000006021247.71339616004551247.01656864002421246.60480000000171247.524799999931248.49680000005951248.70560000001841253.8124144000771262.17492224010181263.6414473599971262.53681663999991262.53681663999991266.29569663999721268.83296000004041268.92759999997181268.80519999997391270.51794240006851272.34059520000371272.72395744007831272.51399999996241269.88372992002751269.86861279993291270.33539584004051274.8775999999981278.91559999997981279.57244031997951280.26818847998581281.81907200000861280.1145471999881280.26583808002211282.16160000001131284.01311839998931284.01284.01284.04239999999281287.85358911997541288.57519999998391285.05800000001341282.58239999998611279.95512320001241278.69289536003361279.1063623999951280.08440000009681281.6557491199491302.3696000000321302.79501521300.08421312006661296.8040000000111292.97137119996611304.79437983997881306.57849919994371307.31038496008051307.01049215984681313.57113375996281328.36420607993981333.5990323200131333.8279065599051339.4616600000371341.11187840004691343.62807536000631337.35400000000531335.94417920000341335.94417920000341338.29199999995581345.11983695999041344.97791008005581344.75679999998061346.36441200001331351.1407743999611353.88649039996471355.07350239999531355.70559999999071358.45600000000421360.61280000004541363.21607039997751366.29441791999971368.61160000000381367.17858031996961373.88875712002551372.41360000000031373.11520079999241371.95458623999781370.98426400004971368.37926720001341370.80404479999131367.18288448000481368.51970719999941369.22199999999131369.35519999998771369.58559999999421369.81959999999161370.0280000000251370.33039999997781371.53265472000771371.53265472000771370.33039999997781370.0280000000251369.81959999999161369.58559999999421369.35519999998771369.22199999999131369.2111999999931369.45097599998831369.7918412799881370.30604223998631370.49165536001921372.2931999999761373.03120000000541373.89401519997111375.15545760003831375.56763168001821375.69427968000331375.86056000000741374.44559999997551374.287232319991371.11600703999921367.77844735995061364.47976351998111366.08816384004441366.11160000000381365.55500800000661365.35960000000861367.06078016000951365.30413439998781367.64840000000641374.92560000004121379.24507008001551381.87075599997771385.38711840001431390.99785440000531393.59159999995181394.22683775999551394.785983999961396.10986239999781397.33919999995441398.840399999951399.98159999996231399.38400000003871399.44159999999561399.81599999997121401.82768960004611402.41641920002581402.41641920002581403.16931456008641406.91280480013571409.42299263986891410.581471999981410.92730240003561410.98560512000771412.02081168000771413.19284927998891413.58422015996851414.53335296003731416.34928576003591418.10113504000511419.69244736003271423.54289407996381428.5092070399991429.21599999996621430.59839999995481431.8023879999941432.83222847996421433.9918982399771435.1189804800231436.22534975996251437.6710993600091439.5712000000931441.1952000001441444.1831999999671447.16054687999281450.43798400010131452.88422768013221453.56520000009141454.28519999985951455.61054575983641455.82500960000581455.87740832011171462.0096000000181465.11843904004921468.98963280006551471.33505151997721474.49041408001771474.51552191998531473.6771999999591470.34980207999321469.1704000000891468.92392480007721472.22680000008721476.77842239998241483.0398720001141491.07678656008261493.2488000000271495.12026400012161498.6099999998831501.1247038400371507.15119999994361506.69999999996681517.18320000002631516.6452895998731516.67619775993531519.486784000041525.7374463999811528.75077151995331531.10497535990681536.86860480013431546.12319999991451547.11007040004551569.0960294402361574.53024192017441574.70333200004941580.83468576014251588.04937279999851593.55066240002431596.31389007994361597.96679120002071599.508113920041600.5111999999741601.04759999995081602.63520000000971606.41520000018641611.68472096004231614.39735264000981617.07641072008031619.82463263997781618.47493072011031626.49292672006411630.11377791995321632.8679999999831632.88886816014751634.01873919984251637.6499152000031639.94115679999731644.16167519996591646.1867385598921646.22756160004561644.7768191999141643.89517184004561646.48926559978031655.15651327992081657.69077504007441657.9469279999711658.17273728016271659.93212927983881661.43300192010381660.9530148799151662.36599759984121665.17502144019251673.684175359961676.04319999985821676.7703999999561676.15785360031651682.16067839998071686.44679680002671688.34560000009331692.41108287999761694.98997024007991696.37829696000381696.37829696000381699.62119999979971701.40986479994081702.99928736005381700.97748271987581699.62362623993981699.67983040006541700.56708992003041700.25801120015061694.22200000003111691.79937440010081687.98590511999991682.7842799999861683.82679999997251686.66164208004081690.87873920013931699.84912639983921702.59610528000351703.76849999987081704.64290975990531710.205215359851711.04151552008651711.27691232001731714.603199999831718.76316927983181721.64708608011571723.91039999992561728.23519999999831728.82559999999131729.31160000001771730.91773119998921733.78035328002441735.40421120011821740.58156672005731745.85633664005541749.62888815994981752.47273216007671751.77159999986861756.63782528017711762.1107000001181763.840000000091764.3822943998951767.53552288011251775.58319999983221778.5603999998871780.6916000000571783.7488000000351786.58930079993341787.70172671996421788.83695360001431789.1631222400491788.95048319992821791.70457408017481794.40420703985931797.58907519997621799.4350673599871802.3600000000161802.7200000000281803.41711279992111808.7082214400541818.02080000015171826.46808064009361846.85239999971171835.9368000001761809.41226240027961830.3084380798581848.01261840000171855.31745056005931859.9515839999551852.58372592007251822.40857856018971816.81300864007061805.530424800111805.72206432016471805.72206432016471811.81558399998541816.69359999978091831.2005350400911838.20648895998991841.81077279985221875.39535999987381881.68052800000151883.9019628800481870.38915680006581870.46599999983531870.00599040003931861.57079999993521873.26161920001621891.45855392003951887.89627295995931858.1000214400461849.23249280000231841.74899584002361835.31789904011841879.03894272007571896.50668623999221915.67976448002791915.67976448002791917.77597519990921929.57521152004961969.80620416000941982.7099788800041977.27776064001451934.53773184002011918.23296511997431920.12055999997961927.91107199998421937.4581887999591943.9815553600361939.90536511998131926.08976320007011920.12894400005531919.5945312000781918.75870240006341915.73806559993661908.4030319999741906.5904204800181898.31760672013871886.46859808010531880.04900800005611874.01342896010851858.61199999999281847.8504000000411844.97760000002951829.84870399992631819.90550559998221823.47812831995661814.23890559993131802.65728640000181785.2150809601381755.68080000026841747.03000000021961716.59821760001861707.93590271999351689.26431551993481686.07171040004851684.17011072002081678.74951008006041675.67192480003311669.78558143987151663.89760000007751657.93150976001651658.51407728006981658.51407728006981657.40847552020841638.6871577600661634.5408640000431639.9934004802021638.50976800019271612.93437119993591604.4657920001311583.52455679965741574.0671999998931567.74320272000891571.59839999986981566.77119999996881541.94637487980911540.25818303982491534.86783904001031529.29978095977821518.2260291201041516.33640000014821516.89431903980931511.72167679995641503.9149439999791520.58424112011181491.279351039851484.29634367977451485.69459840003951476.82847103984451470.82383359980991465.72020751998981461.04879999998731462.4006691201071460.51725280013151447.89759999981881437.91359999990911434.76654159986881434.8690355198661431.42688000017571422.65825760030661412.21740927990071404.8914560000781397.9700159997691406.9039416001161398.49284960020531386.05553887991321368.80971760005261358.74461920005641357.06959999989571357.36535327991441350.37430847989871349.76901375994251353.20385248010671344.52626463998881327.8520294400081317.68773279991461315.17512399994121300.31231103994261291.82520000009841289.09919999987141298.38837984010141288.31114464008721285.18128320001261279.76720639994321267.06700800001771267.64502192001331268.0292000000351265.72271744009191264.97792255989521266.08933856007981265.63551103992271264.62854495992271266.40812927991621268.75990543999391267.91552415980551266.47314144016921263.90567424006991262.11758736010321262.77653856014351262.77653856014351263.8016000001591265.61762944004451263.95349247990541262.68693503979261262.9179073602051260.91201615981251259.42683040001881259.72040320015181257.32272256012881254.13686944018451251.74547904001661250.9796265599581250.48228223993171248.70560000001841248.49680000005951247.524799999931246.60480000000171247.01656864002421247.71339616004551248.23192000006021248.74808384000361249.78401599995141250.72182463999681251.71894400008571251.91101696005561252.23079999995981252.97960000001331253.58479999998051255.90680000002681256.6807999999911257.04679999999441257.59978064004031256.89919999995781259.45666400007831259.770441260.30334336001441260.7763999999791260.52136303993961261.29800000002071261.25681440014891259.4935257601411262.36614144008721263.02930560001071263.02930560001071263.58228480006191262.94630847996361267.8429412801161264.6530239998231267.92095616005621273.48173920017171285.76093600002791279.2944000000421265.64885759983081270.0359999998581263.2335692799831266.61562623997681266.3595999998561268.24298367983651269.69719999988141263.39109039984421270.65312639977511271.02808223974221276.81838464012341271.6042294398551261.30638160011971256.97273728035611252.1262776000561245.02199999999081245.94879999998351245.98787055999671245.88899728000521245.82469695998821245.92955199999441246.63628096001841247.54533648000191245.6683999999961245.17467999998081245.19999999999141244.9407398400112 \ No newline at end of file diff --git a/docs/release/fixtures/moonridge-100.gpx b/docs/release/fixtures/moonridge-100.gpx new file mode 100644 index 0000000..58de3d1 --- /dev/null +++ b/docs/release/fixtures/moonridge-100.gpx @@ -0,0 +1,2 @@ + +FICTIONAL Moonridge 100 — not for navigationMoonridge 100450.0450.1450.3450.8451.3452.1453.0454.1455.3456.8458.3460.1462.0464.0466.2468.6471.1473.8476.6479.6482.7485.9489.3492.9496.5500.3504.2508.3512.4516.7521.1525.6530.2535.0539.8544.7549.7554.8560.0565.3570.6576.0581.5587.0592.6598.3604.0609.7615.5621.3627.1633.0638.9644.8650.8656.7662.6668.6674.5680.4686.3692.2698.1703.9709.7715.4721.1726.8732.4737.9743.4748.8754.2759.4764.6769.7774.7779.7784.5789.2793.9798.4802.8807.1811.3815.3819.3823.1826.7830.3833.7837.0840.1843.1845.9848.6851.1853.5855.7857.8859.7861.5863.1864.5865.8866.9867.8868.6869.2869.6869.9870.0869.9869.7869.3868.7868.0867.1866.0864.8863.4861.8860.1858.2856.2854.0851.7849.2846.5843.7840.8837.7834.4831.0827.5823.9820.1816.2812.2808.0803.8799.4794.9790.3785.6780.8775.8770.8765.8760.6755.3750.0744.6739.1733.6728.0722.4716.7710.9705.2699.4693.5687.6681.7675.8669.9664.0658.0652.1646.2640.2634.3628.5622.6616.8611.0605.2599.5593.9588.2582.7577.2571.8566.4561.2556.0550.8545.8540.9536.0531.3526.6522.1517.7513.4509.2505.1501.2497.4493.7490.1486.7483.4480.3477.3474.4471.7469.2466.8464.5462.4460.5458.7457.1455.6454.4453.2452.3451.5450.9450.4450.1450.0450.1450.3450.6451.2451.9452.8453.8455.1456.4458.0459.7461.5463.6465.7468.1470.6473.2476.0478.9482.0485.2488.6492.1495.7499.5503.4507.4511.5515.8520.1524.6529.2533.9538.7543.6548.6553.7558.8564.1569.4574.8580.2585.8591.4597.0602.7608.4614.2620.0625.8631.7637.6643.5649.4655.4661.3667.3673.2679.1685.0690.9696.8702.6708.4714.1719.9725.5731.1736.7742.2747.6753.0758.3763.5768.6773.6778.6783.4788.2792.8797.4801.8806.1810.3814.4818.4822.2825.9829.5832.9836.2839.4842.4845.3848.0850.6853.0855.3857.4859.3861.1862.7864.2865.5866.6867.6868.4869.1869.5869.9870.0870.0869.8869.4868.9868.2867.3866.3865.1863.7862.2860.5858.7856.7854.5852.2849.7847.1844.3841.4838.4835.2831.8828.3824.7821.0817.1813.1809.0804.7800.4795.9791.3786.6781.8776.9772.0766.9761.7756.5751.2745.8740.4734.8729.3723.6718.0712.2706.5700.6694.8688.9683.0677.1671.2665.3659.3653.4647.5641.6635.6629.8623.9618.1612.3606.5600.8595.1589.5583.9578.4573.0567.6562.3557.1552.0546.9542.0537.1532.3527.7523.1518.7514.3510.1506.0502.0498.2494.5490.9487.4484.1480.9477.9475.0472.3469.7467.3465.0462.9460.9459.1457.4456.0454.6453.5452.5451.7451.0450.5450.2450.0450.0450.2450.5451.1451.7452.6453.6454.8456.1457.6459.3461.1463.1465.2467.5470.0472.6475.3478.2481.3484.5487.8491.3494.9498.6502.5506.5510.6514.8519.2523.6528.2532.9537.6542.5547.5552.5557.7562.9568.2573.6579.0584.5590.1595.7601.4607.1612.9618.7624.5630.4636.3642.2648.1654.1660.0665.9671.9677.8683.7689.6695.5701.3707.1712.9718.6724.3729.9735.5741.0746.4751.8757.1762.3767.5772.5777.5782.4787.1791.8796.4800.8805.2809.4813.5817.5821.4825.1828.7832.2835.5838.7841.8844.7847.4850.0852.5854.8856.9858.9860.7862.4863.9865.2866.4867.4868.3868.9869.5869.8870.0870.0869.8869.5869.0868.3867.5866.5865.4864.0862.6860.9859.1857.1855.0852.7850.3847.7845.0842.1839.1835.9832.6829.1825.5821.8818.0814.0809.9805.7801.3796.9792.3787.7782.9778.0773.1768.0762.9757.7752.4747.0741.6736.1730.5724.9719.2713.5707.7701.9696.1690.2684.4678.4672.5666.6660.7654.7648.8642.9637.0631.1625.2619.4613.5607.8602.0596.4590.7585.2579.6574.2568.8563.5558.3553.1548.0543.1538.2533.4528.7524.1519.6515.3511.0506.9502.9499.0495.3491.7488.2484.8481.6478.6475.7472.9470.3467.8465.5463.3461.3459.5457.8456.3454.9453.7452.7451.8451.1450.6450.2450.0450.0450.1450.5450.9451.6452.4453.4454.5455.8457.3458.9460.7462.6464.7467.0469.4472.0474.7477.6480.6483.8487.1490.5494.1497.8501.6505.6509.7513.9518.2522.6527.2531.8536.6541.4546.4551.4556.5561.7567.0572.4577.8583.3588.9594.5600.1605.9611.6617.4623.2629.1635.0640.9646.8652.7658.7664.6670.6676.5682.4688.3694.2700.0705.8711.6717.3723.0728.6734.2739.8745.2750.6755.9761.2766.3771.4776.4781.3786.1790.8795.4799.9804.2808.5812.6816.6820.5824.3827.9831.4834.8838.0841.1844.0846.8849.4851.9854.3856.4858.5860.3862.0863.6864.9866.2867.2868.1868.8869.4869.7869.9870.0869.9869.6869.1868.5867.7866.8865.6864.4862.9861.3859.5857.6855.5853.2850.8848.3845.6842.7839.7836.6833.3829.9826.3822.6818.8814.9810.8806.6802.3797.9793.4788.7784.0779.1774.2769.2764.0758.8753.6748.2742.8737.3731.8726.1720.5714.8709.0703.2697.4691.5685.7679.8673.8667.9662.0656.0650.1644.2638.3632.4626.5620.6614.8609.1603.3597.6592.0586.4580.9575.4570.0564.7559.4554.2549.2544.2539.2534.4529.7525.1520.6516.2512.0507.8503.8499.9496.1492.5489.0485.6482.3479.2476.3473.5470.8468.3466.0463.8461.8459.9458.2456.6455.2454.0452.9452.0451.3450.7450.3450.1450.0450.1450.4450.8451.4452.2453.1454.2455.5456.9458.5460.3462.2464.3466.5468.9471.4474.1476.9479.9483.0486.3489.7493.3496.9500.7504.7508.7512.9517.2521.6526.1530.8535.5540.3545.3550.3555.4560.6565.8571.2576.6582.1587.6593.2598.9604.6610.3616.1621.9627.8633.7639.6645.5651.4657.4663.3669.2675.2681.1687.0692.9698.7704.5710.3716.0721.7727.4733.0738.5744.0749.4754.7760.0765.2770.3775.3780.2785.0789.8794.4798.9803.3807.6811.7815.8819.7823.5827.1830.7834.1837.3840.4843.4846.2848.9851.4853.8856.0858.0859.9861.7863.2864.7865.9867.0867.9868.7869.2869.7869.9870.0869.9869.7869.2868.7867.9867.0865.9864.7863.2861.7859.9858.0856.0853.8851.4848.9846.2843.4840.4837.3834.1830.7827.1823.5819.7815.8811.7807.6803.3798.9794.4789.8785.0780.2775.3770.3765.2760.0754.7749.4744.0738.5733.0727.4721.7716.0710.3704.5698.7692.9687.0681.1675.2669.2663.3657.4651.4645.5639.6633.7627.8621.9616.1610.3604.6598.9593.2587.6582.1576.6571.2565.8560.6555.4550.3545.3540.3535.5530.8526.1521.6517.2512.9508.7504.7500.7496.9493.3489.7486.3483.0479.9476.9474.1471.4468.9466.5464.3462.2460.3458.5456.9455.5454.2453.1452.2451.4450.8450.4450.1450.0450.1450.3450.7451.3452.0452.9454.0455.2456.6458.2459.9461.8463.8466.0468.3470.8473.5476.3479.2482.3485.6489.0492.5496.1499.9503.8507.8512.0516.2520.6525.1529.7534.4539.2544.2549.2554.2559.4564.7570.0575.4580.9586.4592.0597.6603.3609.1614.8620.6626.5632.4638.3644.2650.1656.0662.0667.9673.8679.8685.7691.5697.4703.2709.0714.8720.5726.1731.8737.3742.8748.2753.6758.8764.0769.2774.2779.1784.0788.7793.4797.9802.3806.6810.8814.9818.8822.6826.3829.9833.3836.6839.7842.7845.6848.3850.8853.2855.5857.6859.5861.3862.9864.4865.6866.8867.7868.5869.1869.6869.9870.0869.9869.7869.4868.8868.1867.2866.2864.9863.6862.0860.3858.5856.4854.3851.9849.4846.8844.0841.1838.0834.8831.4827.9824.3820.5816.6812.6808.5804.2799.9795.4790.8786.1781.3776.4771.4766.3761.2755.9750.6745.2739.8734.2728.6723.0717.3711.6705.8700.0694.2688.3682.4676.5670.6664.6658.7652.7646.8640.9635.0629.1623.2617.4611.6605.9600.1594.5588.9583.3577.8572.4567.0561.7556.5551.4546.4541.4536.6531.8527.2522.6518.2513.9509.7505.6501.6497.8494.1490.5487.1483.8480.6477.6474.7472.0469.4467.0464.7462.6460.7458.9457.3455.8454.5453.4452.4451.6450.9450.5450.1450.0450.0450.2450.6451.1451.8452.7453.7454.9456.3457.8459.5461.3463.3465.5467.8470.3472.9475.7478.6481.6484.8488.2491.7495.3499.0502.9506.9511.0515.3519.6524.1528.7533.4538.2543.1548.0553.1558.3563.5568.8574.2579.6585.2590.7596.4602.0607.8613.5619.4625.2631.1637.0642.9648.8654.7660.7666.6672.5678.4684.4690.2696.1701.9707.7713.5719.2724.9730.5736.1741.6747.0752.4757.7762.9768.0773.1778.0782.9787.7792.3796.9801.3805.7809.9814.0818.0821.8825.5829.1832.6835.9839.1842.1845.0847.7850.3852.7855.0857.1859.1860.9862.6864.0865.4866.5867.5868.3869.0869.5869.8870.0870.0869.8869.5868.9868.3867.4866.4865.2863.9862.4860.7858.9856.9854.8852.5850.0847.4844.7841.8838.7835.5832.2828.7825.1821.4817.5813.5809.4805.2800.8796.4791.8787.1782.4777.5772.5767.5762.3757.1751.8746.4741.0735.5729.9724.3718.6712.9707.1701.3695.5689.6683.7677.8671.9665.9660.0654.1648.1642.2636.3630.4624.5618.7612.9607.1601.4595.7590.1584.5579.0573.6568.2562.9557.7552.5547.5542.5537.6532.9528.2523.6519.2514.8510.6506.5502.5498.6494.9491.3487.8484.5481.3478.2475.3472.6470.0467.5465.2463.1461.1459.3457.6456.1454.8453.6452.6451.7451.1450.5450.2450.0450.0450.2450.5451.0451.7452.5453.5454.6456.0457.4459.1460.9462.9465.0467.3469.7472.3475.0477.9480.9484.1487.4490.9494.5498.2502.0506.0510.1514.3518.7523.1527.7532.3537.1542.0546.9552.0557.1562.3567.6573.0578.4583.9589.5595.1600.8606.5612.3618.1623.9629.8635.6641.6647.5653.4659.3665.3671.2677.1683.0688.9694.8700.6706.5712.2718.0723.6729.3734.8740.4745.8751.2756.5761.7766.9772.0776.9781.8786.6791.3795.9800.4804.7809.0813.1817.1821.0824.7828.3831.8835.2838.4841.4844.3847.1849.7852.2854.5856.7858.7860.5862.2863.7865.1866.3867.3868.2868.9869.4869.8870.0870.0869.9869.5869.1868.4867.6866.6865.5864.2862.7861.1859.3857.4855.3853.0850.6848.0845.3842.4839.4836.2832.9829.5825.9822.2818.4814.4810.3806.1801.8797.4792.8788.2783.4778.6773.6768.6763.5758.3753.0747.6742.2736.7731.1725.5719.9714.1708.4702.6696.8690.9685.0679.1673.2667.3661.3655.4649.4643.5637.6631.7625.8620.0614.2608.4602.7597.0591.4585.8580.2574.8569.4564.1558.8553.7548.6543.6538.7533.9529.2524.6520.1515.8511.5507.4503.4499.5495.7492.1488.6485.2482.0478.9476.0473.2470.6468.1465.7463.6461.5459.7458.0456.4455.1453.8452.8451.9451.2450.6450.3450.1450.0450.1450.4450.9451.5452.3453.2454.4455.6457.1458.7460.5462.4464.5466.8469.2471.7474.4477.3480.3483.4486.7490.1493.7497.4501.2505.1509.2513.4517.7522.1526.6531.3536.0540.9545.8550.8556.0561.2566.4571.8577.2582.7588.2593.9599.5605.2611.0616.8622.6628.5634.3640.2646.2652.1658.0664.0669.9675.8681.7687.6693.5699.4705.2710.9716.7722.4728.0733.6739.1744.6750.0755.3760.6765.8770.8775.8780.8785.6790.3794.9799.4803.8808.0812.2816.2820.1823.9827.5831.0834.4837.7840.8843.7846.5849.2851.7854.0856.2858.2860.1861.8863.4864.8866.0867.1868.0868.7869.3869.7869.9870.0869.9869.6869.2868.6867.8866.9865.8864.5863.1861.5859.7857.8855.7853.5851.1848.6845.9843.1840.1837.0833.7830.3826.7823.1819.3815.3811.3807.1802.8798.4793.9789.2784.5779.7774.7769.7764.6759.4754.2748.8743.4737.9732.4726.8721.1715.4709.7703.9698.1692.2686.3680.4674.5668.6662.6656.7650.8644.8638.9633.0627.1621.3615.5609.7604.0598.3592.6587.0581.5576.0570.6565.3560.0554.8549.7544.7539.8535.0530.2525.6521.1516.7512.4508.3504.2500.3496.5492.9489.3485.9482.7479.6476.6473.8471.1468.6466.2464.0462.0460.1458.3456.8455.3454.1453.0452.1451.3450.8450.3450.1450.0 \ No newline at end of file diff --git a/docs/release/fixtures/moonridge-100.json b/docs/release/fixtures/moonridge-100.json new file mode 100644 index 0000000..b934995 --- /dev/null +++ b/docs/release/fixtures/moonridge-100.json @@ -0,0 +1,937 @@ +{ + "@ultraedge/events": [ + { + "id": "fixture-moonridge-100", + "mover_id": "local", + "name": "Moonridge 100 \u2014 Overnight Rehearsal", + "description": "FICTIONAL release-test race. Overnight crew plan; course is synthetic and must never be used for navigation.", + "event_date": "2026-09-19", + "event_time": "05:00", + "location": "Fictional Moonridge Range", + "start_location": "Cedar Base", + "finish_location": "Dawn Meadow", + "total_distance": 100, + "distance_unit": "miles", + "total_elevation_gain": 12400, + "total_elevation_loss": 12400, + "elevation_unit": "feet", + "cutoff_time": "32:00", + "target_time": "28:00", + "status": "ready", + "mover_weight_snapshot": null, + "total_gear_weight": null, + "total_nutrition_weight": null, + "total_hydration_weight": null, + "race_website": null, + "course_map_url": null, + "gpx_file_url": "__FIXTURE_GPX_URI__", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + } + ], + "ultraedge_checkpoints": { + "fixture-moonridge-100": [ + { + "id": "fixture-cp-00", + "event_id": "fixture-moonridge-100", + "name": "Cedar Base", + "checkpoint_type": "start", + "order_index": 0, + "distance_from_start": 0, + "elevation": 1476, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.0, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 05:00", + "estimated_duration": null, + "has_crew_access": true, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-01", + "event_id": "fixture-moonridge-100", + "name": "Fern Creek", + "checkpoint_type": "aid_station", + "order_index": 1, + "distance_from_start": 8, + "elevation": 2184, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.1157852, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 07:00", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-02", + "event_id": "fixture-moonridge-100", + "name": "Granite Pass", + "checkpoint_type": "aid_station", + "order_index": 2, + "distance_from_start": 17, + "elevation": 2302, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.24604355, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 09:30", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-03", + "event_id": "fixture-moonridge-100", + "name": "North Fork", + "checkpoint_type": "aid_station", + "order_index": 3, + "distance_from_start": 26, + "elevation": 1495, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.3763019, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 12:00", + "estimated_duration": null, + "has_crew_access": true, + "has_drop_bag": true, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-04", + "event_id": "fixture-moonridge-100", + "name": "Lake Lookout", + "checkpoint_type": "aid_station", + "order_index": 4, + "distance_from_start": 36, + "elevation": 2048, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.5210334, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 15:00", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-05", + "event_id": "fixture-moonridge-100", + "name": "Raven Saddle", + "checkpoint_type": "aid_station", + "order_index": 5, + "distance_from_start": 46, + "elevation": 2395, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.6657649, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 18:00", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-06", + "event_id": "fixture-moonridge-100", + "name": "Moonrise Camp", + "checkpoint_type": "aid_station", + "order_index": 6, + "distance_from_start": 55, + "elevation": 1554, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.79602325, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sat 21:00", + "estimated_duration": null, + "has_crew_access": true, + "has_drop_bag": true, + "has_pacer_pickup": true, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Headlamps on before leaving. Confirm spare batteries and warm layer.\nPacer handoff; no reliable cell coverage.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-07", + "event_id": "fixture-moonridge-100", + "name": "High Ridge", + "checkpoint_type": "aid_station", + "order_index": 7, + "distance_from_start": 65, + "elevation": 1907, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 48.940754749999996, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sun 00:30", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-08", + "event_id": "fixture-moonridge-100", + "name": "Silver Creek", + "checkpoint_type": "aid_station", + "order_index": 8, + "distance_from_start": 74, + "elevation": 2454, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 49.0710131, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sun 03:30", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-09", + "event_id": "fixture-moonridge-100", + "name": "Owl Hollow", + "checkpoint_type": "aid_station", + "order_index": 9, + "distance_from_start": 83, + "elevation": 1645, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 49.20127145, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sun 06:00", + "estimated_duration": null, + "has_crew_access": true, + "has_drop_bag": true, + "has_pacer_pickup": true, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-10", + "event_id": "fixture-moonridge-100", + "name": "Last Climb", + "checkpoint_type": "aid_station", + "order_index": 10, + "distance_from_start": 92, + "elevation": 1771, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 49.3315298, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": null, + "estimated_arrival": "Sun 08:00", + "estimated_duration": null, + "has_crew_access": false, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": false, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-cp-11", + "event_id": "fixture-moonridge-100", + "name": "Dawn Meadow", + "checkpoint_type": "finish", + "order_index": 11, + "distance_from_start": 100, + "elevation": 2475, + "location_description": "Fictional location. Crew parking only where access is marked.", + "latitude": 49.447315, + "longitude": -123, + "cutoff_time": null, + "cutoff_duration": "32:00", + "estimated_arrival": "Sun 09:00", + "estimated_duration": null, + "has_crew_access": true, + "has_drop_bag": false, + "has_pacer_pickup": false, + "has_pacer_dropoff": true, + "aid_supplies": [ + "Water", + "Broth", + "Potatoes" + ], + "notes": "Check bottle labels & personal packing list; no live tracking assumed.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + } + ] + }, + "@ultraedge/crew": [ + { + "id": "fixture-crew-0", + "name": "Alex River", + "phone": "+1 202-555-0110", + "email": "crew0@example.com", + "notes": "Fictional contact for simulator only.", + "avatar_url": null, + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-crew-1", + "name": "Sam Pine", + "phone": "+1 202-555-0111", + "email": "crew1@example.com", + "notes": "Fictional contact for simulator only.", + "avatar_url": null, + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-crew-2", + "name": "Jordan Vale", + "phone": "+1 202-555-0112", + "email": "crew2@example.com", + "notes": "Fictional contact for simulator only.", + "avatar_url": null, + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + } + ], + "@ultraedge/event-crew": [ + { + "eventId": "fixture-moonridge-100", + "crewMemberId": "fixture-crew-0", + "roles": [ + "crew_chief", + "driver" + ], + "customRole": null, + "notes": "Carry printed plan; coordinate overnight driver rest." + }, + { + "eventId": "fixture-moonridge-100", + "crewMemberId": "fixture-crew-1", + "roles": [ + "pacer" + ], + "customRole": null, + "notes": "Pace Moonrise Camp to Owl Hollow; bring independent lighting." + }, + { + "eventId": "fixture-moonridge-100", + "crewMemberId": "fixture-crew-2", + "roles": [ + "pacer", + "other" + ], + "customRole": "Finish logistics", + "notes": "Pace Owl Hollow to finish; verify handoff at 06:00 Sunday." + } + ], + "@ultraedge/gear-items": [ + { + "id": "fixture-gear-00", + "name": "Trail shoes", + "category": "footwear", + "weight": 600, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-01", + "name": "Spare socks", + "category": "clothing", + "weight": 60, + "weightUnit": "g", + "quantity": 2, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-02", + "name": "Rain shell", + "category": "clothing", + "weight": 220, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-03", + "name": "Warm midlayer", + "category": "clothing", + "weight": 260, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-04", + "name": "Gloves", + "category": "clothing", + "weight": 50, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-05", + "name": "Beanie", + "category": "clothing", + "weight": 45, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-06", + "name": "Running vest", + "category": "pack", + "weight": 300, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-07", + "name": "Soft flask", + "category": "hydration", + "weight": 45, + "weightUnit": "g", + "quantity": 2, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-08", + "name": "Reserve bottle", + "category": "hydration", + "weight": 80, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-09", + "name": "Primary headlamp", + "category": "lighting", + "weight": 100, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-10", + "name": "Backup headlamp", + "category": "lighting", + "weight": 90, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-11", + "name": "Spare batteries", + "category": "lighting", + "weight": 80, + "weightUnit": "g", + "quantity": 2, + "notes": "Replace batteries at Moonrise Camp .", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-12", + "name": "Paper course notes", + "category": "navigation", + "weight": 20, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-13", + "name": "Compass", + "category": "navigation", + "weight": 30, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-14", + "name": "Emergency blanket", + "category": "safety", + "weight": 60, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-15", + "name": "Whistle", + "category": "safety", + "weight": 10, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-16", + "name": "Foot care pouch", + "category": "safety", + "weight": 90, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-17", + "name": "Poles", + "category": "poles", + "weight": 360, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-18", + "name": "Personal snack pack", + "category": "nutrition", + "weight": 250, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-gear-19", + "name": "Cup", + "category": "other", + "weight": 25, + "weightUnit": "g", + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs.", + "isActive": true, + "retired": false, + "createdAt": "2026-09-05T12:00:00Z", + "updatedAt": "2026-09-05T12:00:00Z" + } + ], + "@ultraedge/event-gear": [ + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-00", + "isWorn": true, + "isCarried": false, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-01", + "isWorn": false, + "isCarried": false, + "quantity": 2, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-02", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-03", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-04", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-05", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-06", + "isWorn": true, + "isCarried": false, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-07", + "isWorn": false, + "isCarried": true, + "quantity": 2, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-08", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-09", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-10", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-11", + "isWorn": false, + "isCarried": true, + "quantity": 2, + "notes": "Replace batteries at Moonrise Camp ." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-12", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-13", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-14", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-15", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-16", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-17", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-18", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + }, + { + "eventId": "fixture-moonridge-100", + "gearItemId": "fixture-gear-19", + "isWorn": false, + "isCarried": true, + "quantity": 1, + "notes": "Fictional packing item; adjust quantities to personal race needs." + } + ], + "@ultraedge/dropbags": [ + { + "id": "fixture-bag-0", + "name": "North Fork \u2014 Blue", + "eventId": "fixture-moonridge-100", + "checkpointId": "fixture-cp-03", + "items": [ + { + "id": "fixture-bagitem-0-1", + "type": "gear", + "refId": "fixture-gear-01", + "name": "Spare socks", + "quantity": 1, + "notes": "Pack in labeled waterproof pouch." + }, + { + "id": "fixture-bagitem-0-18", + "type": "gear", + "refId": "fixture-gear-18", + "name": "Personal snack pack", + "quantity": 1, + "notes": "Pack in labeled waterproof pouch." + }, + { + "id": "fixture-bagitem-0-7", + "type": "gear", + "refId": "fixture-gear-07", + "name": "Soft flask", + "quantity": 1, + "notes": "Pack in labeled waterproof pouch." + } + ], + "notes": "Runner bib 100; collect after race. Keep dry.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-bag-1", + "name": "Moonrise \u2014 Overnight", + "eventId": "fixture-moonridge-100", + "checkpointId": "fixture-cp-06", + "items": [ + { + "id": "fixture-bagitem-1-3", + "type": "gear", + "refId": "fixture-gear-03", + "name": "Warm midlayer", + "quantity": 1, + "notes": "Pack in labeled waterproof pouch." + }, + { + "id": "fixture-bagitem-1-10", + "type": "gear", + "refId": "fixture-gear-10", + "name": "Backup headlamp", + "quantity": 1, + "notes": "Pack in labeled waterproof pouch." + }, + { + "id": "fixture-bagitem-1-11", + "type": "gear", + "refId": "fixture-gear-11", + "name": "Spare batteries", + "quantity": 2, + "notes": "Pack in labeled waterproof pouch." + } + ], + "notes": "Runner bib 100; collect after race. Keep dry.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + }, + { + "id": "fixture-bag-2", + "name": "Owl Hollow \u2014 Empty Reserve", + "eventId": "fixture-moonridge-100", + "checkpointId": "fixture-cp-09", + "items": [], + "notes": "Reserved for final packing check.", + "created_at": "2026-09-05T12:00:00Z", + "updated_at": "2026-09-05T12:00:00Z" + } + ] +} diff --git a/docs/release/fixtures/seed-simulator.py b/docs/release/fixtures/seed-simulator.py new file mode 100644 index 0000000..8016766 --- /dev/null +++ b/docs/release/fixtures/seed-simulator.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Seed fictional local data in an explicitly supplied, stopped iOS Simulator app.""" +import argparse +import datetime +import hashlib +import json +from pathlib import Path +import plistlib +import re +import shutil + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--container', required=True, type=Path, help='Exact CoreSimulator application DATA container; app must be terminated first') +args = parser.parse_args() +container = args.container.expanduser().resolve() +if not re.search(r'/CoreSimulator/Devices/[A-Fa-f0-9-]+/data/Containers/Data/Application/[A-Fa-f0-9-]+$', str(container)): + parser.error('Refusing non-Simulator application data container') +metadata = container / '.com.apple.mobile_container_manager.metadata.plist' +if not metadata.exists(): + parser.error('Container metadata missing') +with metadata.open('rb') as f: + if plistlib.load(f).get('MCMMetadataIdentifier') != 'com.beaux.ultraedge': + parser.error('Container does not belong to com.beaux.ultraedge') +storage = container / 'Library/Application Support/com.beaux.ultraedge/RCTAsyncLocalStorage_V1' +manifest_path = storage / 'manifest.json' +manifest = json.loads(manifest_path.read_text()) if manifest_path.exists() else {} +fixtures = Path(__file__).resolve().parent +values = json.loads((fixtures / 'moonridge-100.json').read_text()) +gpx = container / 'Documents/gpx/fixture-moonridge-100.gpx' +values['@ultraedge/events'][0]['gpx_file_url'] = gpx.as_uri() +# Decode before any write. Merge fixture records, retaining unrelated local plans. +for key, incoming in list(values.items()): + raw = manifest.get(key) + if raw is None and key in manifest: + raw = (storage / hashlib.md5(key.encode()).hexdigest()).read_text() + previous = json.loads(raw) if raw is not None else ({} if isinstance(incoming, dict) else []) + if isinstance(incoming, dict): + values[key] = {**previous, **incoming} + else: + def identity(row): + return row.get('id') or (row.get('eventId'), row.get('crewMemberId') or row.get('gearItemId')) + ids = {identity(row) for row in incoming} + values[key] = [row for row in previous if identity(row) not in ids] + incoming +backup = container / ('fixture-backup-' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f')) +backup.mkdir() +if storage.exists(): + shutil.copytree(storage, backup / 'RCTAsyncLocalStorage_V1') +if gpx.exists(): + shutil.copy2(gpx, backup / gpx.name) +storage.mkdir(parents=True, exist_ok=True) +gpx.parent.mkdir(parents=True, exist_ok=True) +shutil.copy2(fixtures / 'moonridge-100.gpx', gpx) +# Native AsyncStorage uses strings <= 1024 UTF-16 code units inline; larger +# values are UTF-8 files named MD5(key) and a null manifest value. +for key, value in values.items(): + raw = json.dumps(value, ensure_ascii=False, separators=(',', ':')) + if len(raw.encode('utf-16-le')) // 2 <= 1024: + manifest[key] = raw + else: + (storage / hashlib.md5(key.encode()).hexdigest()).write_text(raw) + manifest[key] = None +pending = storage / 'manifest.fixture.tmp' +pending.write_text(json.dumps(manifest, ensure_ascii=False)) +pending.replace(manifest_path) +print(f'Seeded fictional Moonridge 100. Backup: {backup}') diff --git a/docs/release/maestro/README.md b/docs/release/maestro/README.md new file mode 100644 index 0000000..9c0a2c3 --- /dev/null +++ b/docs/release/maestro/README.md @@ -0,0 +1,16 @@ +# Local native smoke flows + +Run with `maestro --device test ` on a disposable UltraEdge simulator. These flows never upload to Maestro Cloud. Never run the deletion flow against real race data. + +- `onboarding.yaml`: requires introduction visible; verifies all slides, final action and relaunch. +- `fixture-export.yaml`: requires seeded Moonridge fixture and completed onboarding, starting on Home; verifies calendar date and native PDF share sheet. It leaves the share sheet open. +- `delete-fictional-fixture.yaml`: requires Moonridge fixture plus a separately created event named `Native smoke 50K`. Deletes the fictional fixture and verifies the unrelated event remains after restart. The delete icon coordinate is for the tested iPhone 17 Pro layout. +- `privacy-support.yaml`: opens local policy content without opening the external support link. + +Use `fixtures/README.md` to prepare fictional data. Seeding bypasses creation forms. Run the full TestFlight/device matrix separately and record exact candidate identity. Assertions use native accessibility labels; earlier attempts using just “Events” or “Export Race Plan” failed because tab/icon labels contain additional text, then passed with corrected selectors. + +## Athlete workflow regression + +`workflow-all.yaml` requires a freshly installed app on a **disposable iPhone simulator**. It creates fictional QA Alpine 100 / QA Forest 50 races through the UI, a shared gear item and crew member, a drop-bag checkpoint, a packed vest, and a saved template reused in another race. It asserts relaunch persistence and opens native PDF sharing. No data is seeded and nothing is sent externally. + +Run `maestro --device test docs/release/maestro/workflow-all.yaml`, then `python3 docs/release/scripts/verify-native-workflow.py `. The latter only reads the app's simulator storage, validating independent packing quantities, crew roles, checkpoint scope, and bag item IDs. Do not point this flow at personal race data. Screenshots are written to the working directory. diff --git a/docs/release/maestro/delete-fictional-fixture.yaml b/docs/release/maestro/delete-fictional-fixture.yaml new file mode 100644 index 0000000..8687212 --- /dev/null +++ b/docs/release/maestro/delete-fictional-fixture.yaml @@ -0,0 +1,17 @@ +appId: com.beaux.ultraedge +--- +- stopApp +- launchApp +- tapOn: "Events, tab, 2 of 5" +- tapOn: ".*Moonridge.*" +- tapOn: + point: "90%,11%" +- assertVisible: "Delete Event" +- tapOn: "Delete" +- assertVisible: ".*Native smoke 50K.*" +- assertNotVisible: ".*Moonridge.*" +- stopApp +- launchApp +- tapOn: "Events, tab, 2 of 5" +- assertVisible: ".*Native smoke 50K.*" +- assertNotVisible: ".*Moonridge.*" diff --git a/docs/release/maestro/fixture-export.yaml b/docs/release/maestro/fixture-export.yaml new file mode 100644 index 0000000..f204213 --- /dev/null +++ b/docs/release/maestro/fixture-export.yaml @@ -0,0 +1,16 @@ +appId: com.beaux.ultraedge +--- +- tapOn: "Events, tab, 2 of 5" +- tapOn: ".*Moonridge.*" +- assertVisible: ".*Saturday, September 19, 2026.*" +- takeScreenshot: corrected-race-date +- scrollUntilVisible: + element: ".*Export Race Plan.*" + direction: DOWN + speed: 30 + timeout: 40000 +- tapOn: ".*Export Race Plan.*" +- extendedWaitUntil: + visible: "Copy" + timeout: 30000 +- takeScreenshot: final-pdf-share diff --git a/docs/release/maestro/onboarding.yaml b/docs/release/maestro/onboarding.yaml new file mode 100644 index 0000000..9b50cab --- /dev/null +++ b/docs/release/maestro/onboarding.yaml @@ -0,0 +1,16 @@ +appId: com.beaux.ultraedge +--- +- assertVisible: "Plan your race" +- tapOn: "Next" +- assertVisible: "Dial in your gear" +- tapOn: "Next" +- assertVisible: "Rally your crew" +- tapOn: "Next" +- assertVisible: "Share the plan" +- tapOn: "Start planning" +- assertVisible: "Events, tab, 2 of 5" +- stopApp +- launchApp +- assertVisible: "Events, tab, 2 of 5" +- assertNotVisible: "Plan your race" +- takeScreenshot: onboarding-completed diff --git a/docs/release/maestro/privacy-support.yaml b/docs/release/maestro/privacy-support.yaml new file mode 100644 index 0000000..35a4d59 --- /dev/null +++ b/docs/release/maestro/privacy-support.yaml @@ -0,0 +1,11 @@ +appId: com.beaux.ultraedge +--- +- stopApp +- launchApp +- tapOn: "Profile, tab, 5 of 5" +- scrollUntilVisible: + element: "Privacy and support" + direction: DOWN +- tapOn: "Privacy and support" +- assertVisible: "Local plans and optional team sharing" +- takeScreenshot: privacy-support diff --git a/docs/release/maestro/workflow-all.yaml b/docs/release/maestro/workflow-all.yaml new file mode 100644 index 0000000..b20828d --- /dev/null +++ b/docs/release/maestro/workflow-all.yaml @@ -0,0 +1,9 @@ +appId: com.beaux.ultraedge +--- +# Requires freshly installed app on a disposable simulator. Does not seed data. +- launchApp +- runFlow: onboarding.yaml +- runFlow: workflow-create.yaml +- runFlow: workflow-pack.yaml +- runFlow: workflow-reuse.yaml +- runFlow: workflow-create-from-race.yaml diff --git a/docs/release/maestro/workflow-create-from-race.yaml b/docs/release/maestro/workflow-create-from-race.yaml new file mode 100644 index 0000000..709c84c --- /dev/null +++ b/docs/release/maestro/workflow-create-from-race.yaml @@ -0,0 +1,36 @@ +appId: com.beaux.ultraedge +--- +- stopApp +- launchApp +- tapOn: "Events, tab, 2 of 5" +- tapOn: ".*QA Forest 50.*" +- scrollUntilVisible: + element: "Add race crew" + direction: DOWN +- tapOn: "Add race crew" +- tapOn: "Create New" +- tapOn: "e.g., John Smith" +- inputText: "QA Morgan" +- tapOn: "Save" +- tapOn: "Assign to race" +- assertVisible: "QA Morgan" +- tapOn: "Save" +- scrollUntilVisible: + element: ".*QA Morgan.*" + direction: DOWN +- assertVisible: ".*QA Morgan.*" +- scrollUntilVisible: + element: "Add race gear" + direction: UP +- tapOn: "Add race gear" +- tapOn: "Create New" +- tapOn: "e.g., Salomon Sense Ride 5" +- inputText: "QA Headlamp" +- tapOn: "Save" +- tapOn: "Assign to race" +- tapOn: "Add \\(1\\)" +- scrollUntilVisible: + element: "Packed QA Headlamp" + direction: DOWN +- assertVisible: "Packed QA Headlamp" +- takeScreenshot: workflow-created-and-assigned diff --git a/docs/release/maestro/workflow-create.yaml b/docs/release/maestro/workflow-create.yaml new file mode 100644 index 0000000..4bccaa5 --- /dev/null +++ b/docs/release/maestro/workflow-create.yaml @@ -0,0 +1,44 @@ +appId: com.beaux.ultraedge +--- +- launchApp +- tapOn: "Events, tab, 2 of 5" +- tapOn: "Create event" +- tapOn: "e.g., Western States 100" +- inputText: "QA Alpine 100" +- tapOn: "DESCRIPTION" +- scrollUntilVisible: + element: "Create Event" + direction: DOWN +- tapOn: "Create Event" +- assertVisible: "QA Alpine 100" +- tapOn: "Back" +- tapOn: "Create event" +- tapOn: "e.g., Western States 100" +- inputText: "QA Forest 50" +- tapOn: "DESCRIPTION" +- scrollUntilVisible: + element: "Create Event" + direction: DOWN +- tapOn: "Create Event" +- assertVisible: "QA Forest 50" +- tapOn: "Back" +- tapOn: "Gear, tab, 3 of 5" +- tapOn: ".*Add gear" +- tapOn: "e.g., Salomon Sense Ride 5" +- inputText: "QA Soft Flask" +- tapOn: "Save" +- tapOn: "OK" +- assertVisible: ".*QA Soft Flask.*" +- tapOn: "Crew, tab, 4 of 5" +- tapOn: ".*Add crew member" +- tapOn: "e.g., John Smith" +- inputText: "QA Alex" +- tapOn: "Save" +- tapOn: "OK" +- assertVisible: ".*QA Alex.*" +- stopApp +- launchApp +- tapOn: "Events, tab, 2 of 5" +- assertVisible: ".*QA Alpine 100.*" +- assertVisible: ".*QA Forest 50.*" +- takeScreenshot: workflow-created-races diff --git a/docs/release/maestro/workflow-pack.yaml b/docs/release/maestro/workflow-pack.yaml new file mode 100644 index 0000000..48cda06 --- /dev/null +++ b/docs/release/maestro/workflow-pack.yaml @@ -0,0 +1,73 @@ +appId: com.beaux.ultraedge +--- +- launchApp +- tapOn: "Events, tab, 2 of 5" +- tapOn: ".*QA Alpine 100.*" +- scrollUntilVisible: + element: "Add race checkpoint" + direction: DOWN +- tapOn: "Add race checkpoint" +- tapOn: "e.g., Foresthill Aid Station" +- inputText: "QA Summit Aid" +- tapOn: 'CHECKPOINT NAME \*' +- scrollUntilVisible: + element: ".*Drop Bag.*" + direction: DOWN +- tapOn: ".*Drop Bag.*" +- scrollUntilVisible: + element: "Save Checkpoint" + direction: DOWN +- tapOn: "Save Checkpoint" +- scrollUntilVisible: + element: "Add race gear" + direction: DOWN +- tapOn: "Add race gear" +- tapOn: "Select Existing" +- tapOn: ".*QA Soft Flask.*" +- tapOn: "Add \\(1\\)" +- scrollUntilVisible: + element: "Packed QA Soft Flask" + direction: DOWN +- tapOn: "Increase QA Soft Flask quantity" +- tapOn: "Packed QA Soft Flask" +- assertVisible: ".*Packed.*" +- takeScreenshot: workflow-vest-packed +- scrollUntilVisible: + element: "Add race crew" + direction: DOWN +- tapOn: "Add race crew" +- tapOn: "Select Existing" +- tapOn: "QA Alex" +- tapOn: ".*Pacer.*" +- tapOn: "Save" +- scrollUntilVisible: + element: "Add race drop bag" + direction: DOWN +- tapOn: "Add race drop bag" +- tapOn: "e.g., Mile 50 Bag, Night Gear" +- inputText: "QA Night Bag" +- tapOn: 'Name \*' +- scrollUntilVisible: + element: '.*Select Checkpoint.*' + direction: DOWN +- tapOn: '.*Select Checkpoint.*' +- tapOn: '.*QA Summit Aid.*' +- scrollUntilVisible: + element: ".*Add Gear.*" + direction: DOWN +- tapOn: ".*Add Gear.*" +- tapOn: "Add QA Soft Flask to drop bag" +- assertNotVisible: "Add QA Soft Flask to drop bag" +- assertVisible: ".*QA Soft Flask.*" +- tapOn: "Create Drop Bag" +- assertNotVisible: "New Drop Bag" +- scrollUntilVisible: + element: ".*QA Night Bag.*" + direction: DOWN +- tapOn: ".*QA Night Bag.*" +- scrollUntilVisible: + element: "Save as template" + direction: DOWN +- tapOn: "Save as template" +- tapOn: "OK" +- takeScreenshot: workflow-saved-template diff --git a/docs/release/maestro/workflow-reuse.yaml b/docs/release/maestro/workflow-reuse.yaml new file mode 100644 index 0000000..7788733 --- /dev/null +++ b/docs/release/maestro/workflow-reuse.yaml @@ -0,0 +1,59 @@ +appId: com.beaux.ultraedge +--- +- stopApp +- launchApp +- tapOn: "Events, tab, 2 of 5" +- tapOn: ".*QA Forest 50.*" +- scrollUntilVisible: + element: "Add race gear" + direction: DOWN +- tapOn: "Add race gear" +- tapOn: "Select Existing" +- tapOn: ".*QA Soft Flask.*" +- tapOn: "Add \\(1\\)" +- scrollUntilVisible: + element: "Packed QA Soft Flask" + direction: DOWN +- assertVisible: ".*To pack.*" +- takeScreenshot: workflow-second-race-unpacked +- scrollUntilVisible: + element: "Add race crew" + direction: DOWN +- tapOn: "Add race crew" +- tapOn: "Select Existing" +- tapOn: "QA Alex" +- tapOn: ".*Driver.*" +- tapOn: "Save" +- scrollUntilVisible: + element: "Add race drop bag" + direction: DOWN +- tapOn: "Add race drop bag" +- tapOn: "Choose template" +- tapOn: "Use QA Night Bag template" +- tapOn: "Create Drop Bag" +- assertNotVisible: "New Drop Bag" +- scrollUntilVisible: + element: ".*QA Night Bag.*" + direction: DOWN +- tapOn: ".*QA Night Bag.*" +- assertVisible: ".*QA Forest 50.*" +- scrollUntilVisible: + element: ".*QA Soft Flask.*" + direction: DOWN +- takeScreenshot: workflow-reused-bag +- stopApp +- launchApp +- tapOn: "Events, tab, 2 of 5" +- tapOn: ".*QA Alpine 100.*" +- scrollUntilVisible: + element: "Packed QA Soft Flask" + direction: DOWN +- assertVisible: ".*Packed.*" +- scrollUntilVisible: + element: ".*Export Race Plan.*" + direction: UP +- tapOn: ".*Export Race Plan.*" +- extendedWaitUntil: + visible: "Copy" + timeout: 30000 +- takeScreenshot: workflow-pdf-share diff --git a/docs/release/scripts/verify-native-workflow.py b/docs/release/scripts/verify-native-workflow.py new file mode 100644 index 0000000..b94fca2 --- /dev/null +++ b/docs/release/scripts/verify-native-workflow.py @@ -0,0 +1,47 @@ +"""Read-only assertions for the fictional Maestro workflow on an explicit simulator.""" +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +udid = sys.argv[1] +container = Path(subprocess.check_output(['xcrun', 'simctl', 'get_app_container', udid, 'com.beaux.ultraedge', 'data'], text=True).strip()) +root = container / 'Library/Application Support/com.beaux.ultraedge/RCTAsyncLocalStorage_V1' +manifest = json.loads((root / 'manifest.json').read_text()) +def read(key): + value = manifest[key] + if value is None: + value = (root / hashlib.md5(key.encode()).hexdigest()).read_text() + return json.loads(value) + +races = {r['name']: r['id'] for r in read('@ultraedge/events')} +a, b = races['QA Alpine 100'], races['QA Forest 50'] +gear = read('@ultraedge/gear-items') +flask = next(g['id'] for g in gear if g['name'] == 'QA Soft Flask') +allocations = read('@ultraedge/event-gear') +first = next(g for g in allocations if g['eventId'] == a and g['gearItemId'] == flask) +second = next(g for g in allocations if g['eventId'] == b and g['gearItemId'] == flask) +assert first['quantity'] == 2 and first['isPacked'] and first['isCarried'], first +assert second['quantity'] == 1 and not second.get('isPacked') and second['isCarried'], second +members = read('@ultraedge/crew') +alex = next(m['id'] for m in members if m['name'] == 'QA Alex') +assignments = read('@ultraedge/event-crew') +assert next(c for c in assignments if c['eventId'] == a and c['crewMemberId'] == alex)['roles'] == ['pacer'] +assert next(c for c in assignments if c['eventId'] == b and c['crewMemberId'] == alex)['roles'] == ['driver'] +bags = [bag for bag in read('@ultraedge/dropbags') if bag['name'] == 'QA Night Bag'] +assert len(bags) == 2 and {bag['eventId'] for bag in bags} == {a, b} +template = next(t for t in read('@ultraedge/dropbag-templates') if t['name'] == 'QA Night Bag') +assert 'eventId' not in template and 'checkpointId' not in template +checkpoint = next(c for c in read('ultraedge_checkpoints')[a] if c['name'] == 'QA Summit Aid') +assert checkpoint['checkpoint_type'] == 'drop_bag' or checkpoint['has_drop_bag'] +assert next(bag for bag in bags if bag['eventId'] == a)['checkpointId'] == checkpoint['id'] +for bag in bags: + assert (bag['checkpointId'] is None) == (bag['eventId'] == b) + assert bag['items'][0]['refId'] == flask and bag['items'][0]['quantity'] == 1 +assert len({bag['items'][0]['id'] for bag in bags} | {template['items'][0]['id']}) == 3 +morgan = next(m['id'] for m in members if m['name'] == 'QA Morgan') +assert any(c['eventId'] == b and c['crewMemberId'] == morgan for c in assignments) +lamp = next(g['id'] for g in gear if g['name'] == 'QA Headlamp') +assert any(g['eventId'] == b and g['gearItemId'] == lamp for g in allocations) +print('PASS: two UI-created races; shared gear with isolated quantity/packing; shared crew with independent pacer/driver roles; durable template and independent bag item IDs.') diff --git a/docs/release/site/index.html b/docs/release/site/index.html new file mode 100644 index 0000000..3de8b99 --- /dev/null +++ b/docs/release/site/index.html @@ -0,0 +1,23 @@ + + +UltraEdge support + + +
+

UltraEdge support

+

UltraEdge is a free race preparation app for ultramarathon runners and crews. Build an event plan, organize checkpoints, gear and drop bags, assign crew roles, import a GPX route, and share a PDF race plan.

+

Where are my plans saved?

+

Editable plans are saved on this device. Optional team rooms share race timing and logistics across phones. Full editable plans do not have automatic cloud backups or device transfer. A shared PDF is a readable copy; it cannot restore your plan into the app.

+

Before travel, export a PDF and save it somewhere you can access offline. Check that the PDF contains the information your crew needs. Keep your original GPX file separately.

+

What works offline?

+

Previously saved plans and locally imported GPX routes can be read without an internet connection. Map backgrounds may need a connection. Check your route and PDF in airplane mode before race day. UltraEdge is a planning tool and does not provide live tracking, crew notifications, or turn-by-turn navigation.

+

A change did not save

+

Keep the app installed. Read the error message, return to the list, and refresh before trying again. Check that your device has available storage. If a problem persists, record the steps that led to it and report it below. Deleting the app to troubleshoot can remove your plans.

+

Remove my data

+

You can delete individual events, crew members, gear and drop bags in the app. To remove the app’s local data from your iPhone, choose Delete App in iOS, rather than Offload App. There is currently no single in-app action to clear all data. Copies you exported or shared, and any device backups, must be managed separately.

+

Report a problem or suggest an improvement

+

Use the UltraEdge GitHub issue tracker. Include your app version, iPhone/iPad model, iOS version, what you expected, and what happened. GitHub may require an account to post.

+

Issues are public. Do not attach private routes, crew phone numbers, personal health information, or screenshots containing them. For a sensitive concern, first request a private contact method without including the sensitive details.

+
+ + diff --git a/docs/release/site/privacy.html b/docs/release/site/privacy.html new file mode 100644 index 0000000..d0e01eb --- /dev/null +++ b/docs/release/site/privacy.html @@ -0,0 +1,29 @@ + + +UltraEdge privacy policy + + +
+

UltraEdge privacy policy

+

Updated September 6, 2026. This policy describes UltraEdge 1.0’s free local planner.

+

Information stored on your device

+

UltraEdge stores the information you enter on your device: event plans and locations, checkpoints, gear, drop bags, crew names and contact details, role assignments, notes, profile information, weight entries, preferences, and imported GPX route files. These records support planning, display and PDF export.

+

Local planning records are not uploaded automatically. There are no subscriptions, advertising, or app-operated analytics and crash reporting enabled.

+

Optional live team races

+

Creating or joining a team room connects to Supabase in Canada and creates a guest identity, with a session saved securely on your device. Shared race timing, checkpoint names, stops, crew display names, vehicle/cargo labels and checkpoint observations are stored on the server and cached on joined phones. Crew contact details, personal notes and route files are excluded. Authentication also processes connection metadata such as IP addresses for abuse prevention.

+

Only joined members can retrieve the plan or submit observations. The owner can revoke membership or stop sharing and delete the room. Use “Delete my collaboration data” in the live team panel to remove your guest identity, owned rooms, memberships and reports. Deleting the app or a local race does not delete the server room. Previously saved copies held by others remain with them. Guest access has no email/password recovery; new phones need a new crew invitation. Pending reports remain on the device until synced.

+

Maps, files and other services

+

Displaying a map background uses the device’s map service and may make network requests for the area being viewed. This service operates under its provider’s privacy practices. On iOS, see Apple’s privacy information. UltraEdge does not provide live location tracking.

+

When you select a GPX file or an image, the system file or photo picker supplies the item you choose. A file stored with a cloud provider may need to download through that provider. Opening a race website or another external link uses the destination service’s privacy practices.

+

Sharing a race plan

+

PDF exports may include crew contact information, route details and notes from your plan. You choose whether and where to share them using the system share sheet. Review the PDF before sharing it. The app or person receiving a copy controls that copy; deleting the original plan does not remove shared copies.

+

Retention and deletion

+

Saved information remains locally until you remove it or delete the app. You can delete individual planning records in the app. There is currently no single in-app action to erase all local information. On iOS, use Delete App, rather than Offload App, to remove the app and its local data.

+

Exported files and copies held by other people or services must be deleted separately. Your operating system may include app data in device backups according to your settings; manage those backups through the relevant device or backup service. UltraEdge does not provide an editable-plan backup or restore service, and a PDF cannot restore an editable plan.

+

Support and privacy questions

+

The UltraEdge GitHub issue tracker is the development support contact. Information you post there is public and is handled under GitHub’s privacy statement. Do not post private routes, contact details or health information. For a sensitive concern, request a private contact method before sharing details.

+

Changes

+

This page will be updated when the app’s data practices change. Its date and version identify the practices it describes.

+
+ + diff --git a/docs/release/site/style.css b/docs/release/site/style.css new file mode 100644 index 0000000..d8c6dce --- /dev/null +++ b/docs/release/site/style.css @@ -0,0 +1,9 @@ +:root { color-scheme: light dark; font-family: system-ui, sans-serif; line-height: 1.65; } +body { max-width: 46rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; } +nav { display: flex; gap: 1.5rem; flex-wrap: wrap; } +h1, h2 { line-height: 1.2; } +h1 { margin-top: 2rem; } +h2 { margin-top: 2rem; font-size: 1.25rem; } +a { color: light-dark(#205b41, #98d9b2); text-underline-offset: .15em; } +a:focus-visible { outline: 3px solid currentColor; outline-offset: 4px; } +footer { margin-top: 3rem; border-top: 1px solid #888; padding-top: 1rem; } diff --git a/ios/UltraEdge.xcodeproj/project.pbxproj b/ios/UltraEdge.xcodeproj/project.pbxproj index bd190e8..fe22c16 100644 --- a/ios/UltraEdge.xcodeproj/project.pbxproj +++ b/ios/UltraEdge.xcodeproj/project.pbxproj @@ -397,7 +397,7 @@ CODE_SIGN_ENTITLEMENTS = UltraEdge/UltraEdge.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = T348Z33H7K; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -437,7 +437,7 @@ CODE_SIGN_ENTITLEMENTS = UltraEdge/UltraEdge.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = T348Z33H7K; INFOPLIST_FILE = UltraEdge/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; diff --git a/ios/UltraEdge/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/ios/UltraEdge/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png index d06d91d..f1ef53b 100644 Binary files a/ios/UltraEdge/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png and b/ios/UltraEdge/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/ios/UltraEdge/Images.xcassets/SplashScreenBackground.colorset/Contents.json b/ios/UltraEdge/Images.xcassets/SplashScreenBackground.colorset/Contents.json index 15f02ab..6f8179d 100644 --- a/ios/UltraEdge/Images.xcassets/SplashScreenBackground.colorset/Contents.json +++ b/ios/UltraEdge/Images.xcassets/SplashScreenBackground.colorset/Contents.json @@ -3,10 +3,10 @@ { "color": { "components": { - "alpha": "1.000", - "blue": "1.00000000000000", - "green": "1.00000000000000", - "red": "1.00000000000000" + "red": "0.090196", + "green": "0.109804", + "blue": "0.125490", + "alpha": "1.000" }, "color-space": "srgb" }, @@ -17,4 +17,4 @@ "version": 1, "author": "expo" } -} \ No newline at end of file +} diff --git a/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image.png b/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image.png index ccc9e7d..f1ef53b 100644 Binary files a/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image.png and b/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image.png differ diff --git a/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@2x.png b/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@2x.png index ccc9e7d..f1ef53b 100644 Binary files a/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@2x.png and b/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@2x.png differ diff --git a/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@3x.png b/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@3x.png index ccc9e7d..f1ef53b 100644 Binary files a/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@3x.png and b/ios/UltraEdge/Images.xcassets/SplashScreenLogo.imageset/image@3x.png differ diff --git a/ios/UltraEdge/Info.plist b/ios/UltraEdge/Info.plist index a28bfb0..2838963 100644 --- a/ios/UltraEdge/Info.plist +++ b/ios/UltraEdge/Info.plist @@ -28,11 +28,14 @@ CFBundleURLSchemes com.beaux.ultraedge + ultraedge CFBundleVersion - 2 + 8 + ITSAppUsesNonExemptEncryption + LSMinimumSystemVersion 12.0 LSRequiresIPhoneOS @@ -44,12 +47,6 @@ NSAllowsLocalNetworking - NSCameraUsageDescription - Allow $(PRODUCT_NAME) to access your camera - NSMicrophoneUsageDescription - Allow $(PRODUCT_NAME) to access your microphone - NSPhotoLibraryUsageDescription - Access required to allow users to update their profile picture RCTNewArchEnabled UILaunchStoryboardName diff --git a/ios/UltraEdge/SplashScreen.storyboard b/ios/UltraEdge/SplashScreen.storyboard index 8a6fcd4..0b61f70 100644 --- a/ios/UltraEdge/SplashScreen.storyboard +++ b/ios/UltraEdge/SplashScreen.storyboard @@ -22,10 +22,10 @@ - - - - + + + + @@ -38,7 +38,7 @@ - + \ No newline at end of file diff --git a/jest.setup.js b/jest.setup.js index 0258329..d5a87a2 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -14,6 +14,11 @@ jest.mock('expo-font', () => ({ loadAsync: jest.fn(), })); +// Mock AsyncStorage (native module unavailable in jest) +jest.mock('@react-native-async-storage/async-storage', () => + require('@react-native-async-storage/async-storage/jest/async-storage-mock') +); + jest.mock('expo-haptics', () => ({ impactAsync: jest.fn(), notificationAsync: jest.fn(), diff --git a/metro.config.js b/metro.config.js index 5c1db73..d9d67ba 100644 --- a/metro.config.js +++ b/metro.config.js @@ -9,6 +9,9 @@ const { getDefaultConfig } = require("expo/metro-config"); // /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); +// Release bundling must not wait on a machine-wide Watchman daemon. +if (process.env.ULTRAEDGE_DISABLE_WATCHMAN === "1") config.resolver.useWatchman = false; + // Polyfills and stubs for Node.js modules const emptyModule = require.resolve('./src/lib/empty-module.js'); config.resolver.extraNodeModules = { diff --git a/scripts/test-live-collaboration.cjs b/scripts/test-live-collaboration.cjs new file mode 100644 index 0000000..bdf0e8c --- /dev/null +++ b/scripts/test-live-collaboration.cjs @@ -0,0 +1,34 @@ +/* Uses disposable guest identities and a synthetic room; no service-role key. */ +const {createClient}=require('@supabase/supabase-js');const fs=require('fs');const assert=require('assert/strict');const {randomUUID}=require('crypto'); +for(const line of fs.readFileSync('.env','utf8').split('\n')){const m=/^([A-Z_]+)=(.*)$/.exec(line);if(m&&!process.env[m[1]])process.env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');} +const clients=[];let passed=0; +async function call(c,action,args={},fail=false){const r=await c.rpc('ue_live',{action,args});if(fail){assert(r.error,`Expected ${action} denied`);return;}if(r.error)throw new Error(`${action}: ${r.error.message}`);return r.data;} +function pass(name){passed++;console.log('PASS',name);} +(async()=>{ + try{ + for(let i=0;i<3;i++){const c=createClient(process.env.EXPO_PUBLIC_SUPABASE_URL,process.env.EXPO_PUBLIC_SUPABASE_KEY||process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY,{auth:{persistSession:false,autoRefreshToken:false}});const {error}=await c.auth.signInAnonymously();if(error)throw error;clients.push(c);} + const [owner,crew,outsider]=clients; + const start=Date.now()-12*3600000;const iso=m=>new Date(start+m*60000).toISOString(); + const snapshot={event:{id:'qa-'+randomUUID(),name:'Disposable live collaboration QA',total_distance:100,target_time:'10:00',distance_unit:'km'},checkpoints:[{id:'half',name:'Half',order_index:0,distance_from_start:50},{id:'finish',name:'Finish',order_index:1,distance_from_start:100}],operations:{id:'qa',variation:10,startAt:iso(0),stops:{half:30},duties:[],vehicles:[],cargo:[],reports:[]},startAt:iso(0),logistics:[]}; + await call(owner,'create',{localId:'bad-'+randomUUID(),snapshot:{...snapshot,event:{...snapshot.event,name:{bad:true}}},name:'QA'},true);pass('malformed shared display data rejected'); + const {id}=await call(owner,'create',{localId:snapshot.event.id,snapshot,name:'QA Runner'});let room=await call(owner,'read',{roomId:id}); + await call(outsider,'read',{roomId:id},true);pass('nonmember cannot read room'); + assert((await outsider.from('ue_live_rooms').select('*')).error);pass('direct table access denied'); + await call(crew,'join',{code:room.invite,name:'QA Crew'});await call(crew,'publish',{roomId:id,revision:1,snapshot},true);pass('crew cannot edit plan'); + const op={roomId:id,requestId:randomUUID(),checkpointId:'half',kind:'in',at:iso(300),revision:0}; + await call(owner,'report',op);await call(owner,'report',op);room=await call(owner,'read',{roomId:id});assert.equal(room.reports.length,1);assert.equal(room.reports[0].revision,1);pass('duplicate network retry counted once'); + await call(owner,'report',{...op,requestId:randomUUID(),at:iso(310)},true);pass('stale correction rejected'); + await call(crew,'report',{...op,requestId:randomUUID(),at:iso(310),authorId:room.ownerId});room=await call(owner,'read',{roomId:id});assert.equal(room.reports.length,2);assert.equal(new Set(room.reports.map(r=>r.authorId)).size,2);pass('author identity cannot be forged; both reports retained'); + await call(owner,'report',{...op,kind:'out',requestId:randomUUID(),at:iso(290)},true);pass('departure before arrival rejected'); + await call(owner,'report',{...op,requestId:randomUUID(),checkpointId:'missing'},true);pass('missing checkpoint rejected'); + await call(owner,'publish',{roomId:id,snapshot},true);pass('missing plan revision rejected'); + await call(owner,'publish',{roomId:id,revision:1,snapshot});await call(owner,'publish',{roomId:id,revision:1,snapshot},true);pass('plan version conflicts rejected'); + const crewId=room.members.find(m=>m.id!==room.ownerId).id;const oldInvite=room.invite; + await call(owner,'revoke',{roomId:id,userId:crewId});await call(crew,'read',{roomId:id},true);await call(crew,'join',{code:oldInvite,name:'QA Crew'},true);pass('revocation blocks reads and rotates invite'); + room=await call(owner,'read',{roomId:id});await call(outsider,'join',{code:room.invite,name:'QA Other'});await call(outsider,'leave',{roomId:id});await call(outsider,'read',{roomId:id},true);pass('leave removes access'); + await call(owner,'close',{roomId:id});await call(owner,'read',{roomId:id},true);pass('close deletes shared room'); + }finally{ + for(const c of clients){await call(c,'delete_identity');c.auth.stopAutoRefresh();}console.log('PASS disposable guest identities removed'); + } + console.log(`${passed} live backend checks passed`); +})().catch(e=>{console.error(e.message);process.exitCode=1;}); diff --git a/src/__tests__/attachments.test.ts b/src/__tests__/attachments.test.ts new file mode 100644 index 0000000..3e4e9b4 --- /dev/null +++ b/src/__tests__/attachments.test.ts @@ -0,0 +1,32 @@ +jest.mock('expo-file-system/legacy',()=>({documentDirectory:'file:///first/Documents/',cacheDirectory:'file:///cache/',EncodingType:{Base64:'base64'},getInfoAsync:jest.fn(),readAsStringAsync:jest.fn(),makeDirectoryAsync:jest.fn(),copyAsync:jest.fn()})); +jest.mock('expo-crypto',()=>({randomUUID:()=> 'unique-file'})); +import * as FS from 'expo-file-system/legacy'; +import { attachmentUri, saveAttachment, attachmentShareUri } from '../lib/attachments'; +beforeEach(()=>{jest.clearAllMocks();(FS.getInfoAsync as jest.Mock).mockResolvedValue({exists:true,size:100});(FS.readAsStringAsync as jest.Mock).mockResolvedValue('JVBERi0=');}); +test('persists a copied file with a sandbox-independent path and rebuilds its URI after an app update',async()=>{ + const attachment=await saveAttachment('file:///cache/race.pdf','Race guide.pdf','application/pdf'); + expect(attachment.path).toBe('unique-file.pdf'); + expect(FS.copyAsync).toHaveBeenCalledWith({from:'file:///cache/race.pdf',to:'file:///first/Documents/plan-attachments/unique-file.pdf'}); + (FS as any).documentDirectory='file:///updated/Documents/'; + expect(attachmentUri(attachment.path)).toBe('file:///updated/Documents/plan-attachments/unique-file.pdf'); +}); +test('rejects non-PDF content, empty/oversized files and unsafe paths before copying',async()=>{ + (FS.readAsStringAsync as jest.Mock).mockResolvedValue('not-a-pdf'); + await expect(saveAttachment('file:///bad','bad.pdf','application/pdf')).rejects.toThrow('not a PDF'); + (FS.getInfoAsync as jest.Mock).mockResolvedValue({exists:true,size:51*1024*1024}); + await expect(saveAttachment('file:///big','photo','image/jpeg')).rejects.toThrow('50 MB'); + (FS.getInfoAsync as jest.Mock).mockResolvedValue({exists:true,size:0}); + await expect(saveAttachment('file:///empty','photo','image/jpeg')).rejects.toThrow(); + expect(()=>attachmentUri('../private.pdf')).toThrow(); + expect(FS.copyAsync).not.toHaveBeenCalled(); +}); +test('copy failure rejects instead of returning an unusable saved attachment',async()=>{ + (FS.copyAsync as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await expect(saveAttachment('file:///photo','photo','image/jpeg')).rejects.toThrow('Disk full'); +}); + +test('shares a readable safe filename rather than the internal attachment identifier',async()=>{ + const uri=await attachmentShareUri({path:'unique-file.pdf',name:'../Official guide.pdf',mimeType:'application/pdf'}); + expect(uri).toBe('file:///cache/guide-share-unique-file/.._Official guide.pdf'); + expect(FS.copyAsync).toHaveBeenCalledWith(expect.objectContaining({to:uri})); +}); diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts index 25af630..1836086 100644 --- a/src/__tests__/auth.test.ts +++ b/src/__tests__/auth.test.ts @@ -1,98 +1,36 @@ -/** - * Authentication Test Suite - * Tests for auth context and flows - */ - -describe('Authentication Flow', () => { - describe('Anonymous Usage', () => { - it('should allow app usage without authentication', () => { - // App should start at HomeScreen, not login - const INITIAL_ROUTE = 'Main'; - expect(INITIAL_ROUTE).toBe('Main'); - }); - - it('should store data locally for anonymous users', () => { - const STORAGE_KEY = '@ultraedge'; - expect(STORAGE_KEY).toBeDefined(); - }); - - it('should not require auth for free features', () => { - const freeFeatures = [ - 'create_event', - 'add_gear', - 'plan_checkpoints', - 'create_drop_bags', - 'track_mover_weight', - ]; - - freeFeatures.forEach(feature => { - // All free features should work without auth - expect(feature).toBeDefined(); - }); - }); - }); - - describe('Sign In Methods', () => { - it('should support email/password auth', () => { - const authMethods = ['email', 'apple', 'google']; - expect(authMethods).toContain('email'); - }); - - it('should support Apple Sign In (App Store requirement)', () => { - const authMethods = ['email', 'apple', 'google']; - expect(authMethods).toContain('apple'); - }); - - it('should support Google Sign In', () => { - const authMethods = ['email', 'apple', 'google']; - expect(authMethods).toContain('google'); - }); - }); - - describe('Auth State Management', () => { - it('should persist session across app restarts', () => { - // Session should be stored in AsyncStorage - const SESSION_STORAGE_KEY = 'supabase-auth'; - expect(SESSION_STORAGE_KEY).toBeDefined(); - }); - - it('should link anonymous data when signing in', () => { - // When user signs in, their local data should sync - const shouldMergeLocalData = true; - expect(shouldMergeLocalData).toBe(true); - }); - }); - - describe('Sign Out Flow', () => { - it('should preserve local data on sign out', () => { - // User's local data remains after signing out - const preserveLocalData = true; - expect(preserveLocalData).toBe(true); - }); - - it('should clear session but not local storage', () => { - const clearSession = true; - const clearLocalStorage = false; - - expect(clearSession).toBe(true); - expect(clearLocalStorage).toBe(false); - }); +import { isSupabaseConfigured, parseAuthCallback, recoveryTokens } from '../lib/authLinks'; + +describe('Account configuration', () => { + it.each([['', ''], ['invalid', 'key'], ['https://xxxxx.supabase.co', 'long-placeholder-key-here'], + ['http://project.supabase.co', 'a'.repeat(30)], ['https://user:pass@project.supabase.co', 'a'.repeat(30)]])( + 'disables missing or invalid configuration %s', (url, key) => expect(isSupabaseConfigured(url, key)).toBe(false)); + it('accepts a configured HTTPS endpoint and public key', () => { + expect(isSupabaseConfigured('https://project.supabase.co', 'sb_publishable_12345678901234567890')).toBe(true); }); }); -describe('Auth Screen Navigation', () => { - it('should present auth screens as modals', () => { - const PRESENTATION_MODE = 'modal'; - expect(PRESENTATION_MODE).toBe('modal'); +describe('Auth callback boundary', () => { + it.each(['https://evil.example/auth/reset-password', 'other://auth/reset-password', + 'ultraedge://evil/auth/reset-password', 'ultraedge://auth/reset-password/extra', + 'ultraedge://auth/reset-password-evil', 'ultraedge://user@auth/reset-password', + 'ultraedge://auth:8080/reset-password', 'ultraedge://auth/callback'])('rejects unrelated destinations %s', url => { + expect(parseAuthCallback(url, 'reset-password')).toBeNull(); }); - - it('should be accessible from Profile screen', () => { - const accessPoints = ['ProfileScreen', 'SettingsScreen']; - expect(accessPoints).toContain('ProfileScreen'); + it('accepts exact registered destinations and parses PKCE codes', () => { + expect(parseAuthCallback('ultraedge://auth/reset-password?code=example-code', 'reset-password')?.get('code')).toBe('example-code'); }); - - it('should allow dismissing without signing in', () => { - const canDismiss = true; - expect(canDismiss).toBe(true); + it('rejects ambiguous parameters across fragment and query', () => { + expect(parseAuthCallback('ultraedge://auth/reset-password?code=one#code=two', 'reset-password')).toBeNull(); + expect(parseAuthCallback('ultraedge://auth/reset-password#code=one&code=two', 'reset-password')).toBeNull(); + }); + it('accepts recovery token pairs, but rejects wrong types, errors and malformed tokens', () => { + const params = new URLSearchParams('type=recovery&access_token=header.payload.signature&refresh_token=refresh-token-123'); + expect(recoveryTokens(params)?.refresh_token).toBe('refresh-token-123'); + params.set('type', 'signup'); + expect(recoveryTokens(params)).toBeNull(); + params.set('type', 'recovery'); params.set('error', 'expired'); + expect(recoveryTokens(params)).toBeNull(); + params.delete('error'); params.set('access_token', 'not-a-token'); + expect(recoveryTokens(params)).toBeNull(); }); }); diff --git a/src/__tests__/authContext.test.tsx b/src/__tests__/authContext.test.tsx new file mode 100644 index 0000000..ad64f84 --- /dev/null +++ b/src/__tests__/authContext.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Linking } from 'react-native'; +import { AuthProvider, AuthContextType, useAuth } from '../context/AuthContext'; +const { act, create } = require('react-test-renderer'); + +let mockAvailable = true; +let mockAuthEvent: (event: string, session: unknown) => void; +const mockUnsubscribe = jest.fn(); +const mockSession = { user: { id: 'runner-1' }, access_token: 'test' }; +const mockAuth = { + getSession: jest.fn(), onAuthStateChange: jest.fn(), signInWithPassword: jest.fn(), signUp: jest.fn(), + resetPasswordForEmail: jest.fn(), updateUser: jest.fn(), signOut: jest.fn(), refreshSession: jest.fn(), + exchangeCodeForSession: jest.fn(), setSession: jest.fn(), +}; +jest.mock('../lib/supabase', () => ({ + get isAuthAvailable() { return mockAvailable; }, supabase: { get auth() { return mockAuth; } }, +})); +jest.mock('expo-apple-authentication', () => ({})); +jest.mock('expo-web-browser', () => ({ maybeCompleteAuthSession: jest.fn() })); +jest.mock('expo-crypto', () => ({})); +let current: AuthContextType; +function Probe() { current = useAuth(); return null; } +let tree: any; +async function mount() { await act(async () => { tree = create(); }); } + +beforeEach(() => { + jest.clearAllMocks(); mockAvailable = true; + mockAuth.getSession.mockResolvedValue({ data: { session: null }, error: null }); + mockAuth.onAuthStateChange.mockImplementation((callback) => { + mockAuthEvent = callback; return { data: { subscription: { unsubscribe: mockUnsubscribe } } }; + }); + jest.spyOn(Linking, 'getInitialURL').mockResolvedValue(null); +}); +afterEach(async () => { if (tree) await act(async () => { tree.unmount(); tree = null; }); jest.restoreAllMocks(); }); + +it('boots locally without credentials and performs no auth requests', async () => { + mockAvailable = false; await mount(); + expect(current.isLoading).toBe(false); expect(current.supabase).toBeNull(); + expect(mockAuth.getSession).not.toHaveBeenCalled(); + expect((await current.signInWithEmail('runner@example.com', 'password')).success).toBe(false); + expect(mockAuth.signInWithPassword).not.toHaveBeenCalled(); +}); +it('unsubscribes the shared client listener on unmount', async () => { + await mount(); await act(async () => { tree.unmount(); tree = null; }); + expect(mockUnsubscribe).toHaveBeenCalledTimes(1); +}); +it('opens recovery on cold launch only after code exchange succeeds and updates the password', async () => { + jest.spyOn(Linking, 'getInitialURL').mockResolvedValue('ultraedge://auth/reset-password?code=valid-code'); + mockAuth.exchangeCodeForSession.mockImplementation(async () => { + mockAuthEvent('SIGNED_IN', mockSession); return { data: { session: mockSession }, error: null }; + }); + await mount(); expect(current.isPasswordRecovery).toBe(true); + mockAuth.updateUser.mockResolvedValue({ error: null }); + let result; + await act(async () => { result = await current.updatePassword('new-password'); }); + expect(result).toEqual({ success: true }); expect(mockAuth.updateUser).toHaveBeenCalledWith({ password: 'new-password' }); + expect(current.isPasswordRecovery).toBe(false); +}); +it('does not enable password update without a recovery session', async () => { + await mount(); expect((await current.updatePassword('new-password')).success).toBe(false); + expect(mockAuth.updateUser).not.toHaveBeenCalled(); +}); +it('does not report sign-out success when session removal fails', async () => { + await mount(); await act(async () => { mockAuthEvent('SIGNED_IN', mockSession); }); + mockAuth.signOut.mockResolvedValue({ error: new Error('Storage unavailable') }); + await expect(current.signOut()).rejects.toThrow('Storage unavailable'); + expect(current.isAuthenticated).toBe(true); +}); +it('keeps recovery open if the password update fails', async () => { + await mount(); await act(async () => { mockAuthEvent('PASSWORD_RECOVERY', mockSession); }); + mockAuth.updateUser.mockResolvedValue({ error: { message: 'Password rejected' } }); + expect((await current.updatePassword('new-password')).success).toBe(false); + expect(current.isPasswordRecovery).toBe(true); +}); diff --git a/src/__tests__/courseXml.test.ts b/src/__tests__/courseXml.test.ts new file mode 100644 index 0000000..b634eda --- /dev/null +++ b/src/__tests__/courseXml.test.ts @@ -0,0 +1,32 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { normalizeCourseXml } from '../lib/courseXml'; +import { parseGpx, parseGpxCheckpoints, computeRouteMetrics } from '../lib/gpx'; +const fixture = (name: string) => readFileSync(resolve(__dirname, '../../docs/release/fixtures', name), 'utf8'); +it('normalizes the original Fat Dog KML to exactly the GPX route and checkpoints', () => { + const gpx = fixture('fatdog-120-gaia.xml'); + const normalized = normalizeCourseXml(fixture('fatdog-120-gaia.kml')); + expect(parseGpx(normalized)).toEqual(parseGpx(gpx)); + const metrics = computeRouteMetrics(parseGpx(gpx))!; + expect(parseGpxCheckpoints(normalized, metrics)).toEqual(parseGpxCheckpoints(gpx, metrics)); +}); +it('preserves GPX input and rejects unrelated or malformed XML', () => { + expect(normalizeCourseXml('')).toBe(''); + expect(() => normalizeCourseXml('')).toThrow('GPX or KML'); + expect(() => normalizeCourseXml('')).toThrow(); +}); +const kml = (body: string) => `${body}`; +const line = (coords: string) => `${coords}`; +it('rejects disconnected sections, competing courses, invalid coordinates and point-only files', () => { + expect(() => normalizeCourseXml(kml(`${line('0,0 0,1')}${line('10,10 10,11')}`))).toThrow('disconnected'); + expect(() => normalizeCourseXml(kml(`${line('0,0 0,1')}${line('0,1 0,2')}`))).toThrow('multiple courses'); + expect(() => normalizeCourseXml(kml(`${line('bad,0 0,1')}`))).toThrow('invalid'); + expect(() => normalizeCourseXml(kml('0,0'))).toThrow('no supported'); +}); +it('escapes marker text and preserves optional elevations with prefixed KML', () => { + const xml = 'A & B]]>0,00,0 0,1,100'; + const normalized = normalizeCourseXml(xml), points = parseGpx(normalized); + expect(points[0].ele).toBeUndefined(); + expect(points[1].ele).toBe(100); + expect(parseGpxCheckpoints(normalized, computeRouteMetrics(points)!)[0]).toMatchObject({ name: 'A & B', description: '' }); +}); diff --git a/src/__tests__/crewCreationHandoff.test.tsx b/src/__tests__/crewCreationHandoff.test.tsx new file mode 100644 index 0000000..fe2c5e6 --- /dev/null +++ b/src/__tests__/crewCreationHandoff.test.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { Text, TextInput, TouchableOpacity } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +const { act, create } = require('react-test-renderer'); +import { loadEventCrewAssignments } from '../lib/eventCrew'; +jest.mock('expo-haptics', () => ({ impactAsync: jest.fn(), ImpactFeedbackStyle: { Light: 'light', Medium: 'medium' } })); +jest.mock('@expo/vector-icons', () => ({ Ionicons: () => null })); +jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0 }) })); +jest.mock('../theme', () => ({ useTheme: () => ({ theme: { colors: {}, spacing: {}, radius: {} } }) })); +jest.mock('../components/ui', () => { + const { Text, View, TouchableOpacity } = require('react-native'); + const React = require('react'); + return { Text, H1: Text, H2: Text, H3: Text, Body: Text, BodySmall: Text, Label: Text, Card: View, CardContent: View, + Button: ({ children, ...props }: any) => React.createElement(TouchableOpacity, props, React.createElement(Text, null, children)), + }; +}); +let mockMembers: any[] = []; +const mockCreateCrewMember = jest.fn(); +jest.mock('../context/CrewContext', () => ({ + ...jest.requireActual('../context/CrewContext'), + useCrewMembers: () => ({ crewMembers: mockMembers, loading: false, error: null, createCrewMember: mockCreateCrewMember }), +})); +const Screen = require('../screens/events/SelectCrewScreen').default; +const buttonWithText = (tree: any, label: string) => tree.root.findAllByType(TouchableOpacity).find((node: any) => node.findAllByType(Text).some((text: any) => text.props.children === label)); + +beforeEach(async () => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + await AsyncStorage.clear(); + mockMembers = [{ id: 'existing', name: 'Existing crew' }, { id: 'new', name: 'New crew' }]; + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{ id: 'race' }])); + await AsyncStorage.setItem('@ultraedge/crew', JSON.stringify(mockMembers)); + await AsyncStorage.setItem('@ultraedge/event-crew', JSON.stringify([{ eventId: 'race', crewMemberId: 'existing', roles: ['driver'] }])); +}); + +it('keeps newly created crew selected after assignments load and saves alongside existing roles', async () => { + let tree: any; + const navigation = { goBack: jest.fn(), replace: jest.fn() }; + await act(async () => { tree = create(); }); + await act(async () => { await buttonWithText(tree, 'Save').props.onPress(); }); + expect(await loadEventCrewAssignments()).toEqual([ + expect.objectContaining({ crewMemberId: 'existing', roles: ['driver'] }), + expect.objectContaining({ crewMemberId: 'new', roles: [] }), + ]); + expect(navigation.goBack).toHaveBeenCalledTimes(1); + await act(async () => { tree.unmount(); }); +}); + +it('replaces an empty selector with creation so there is no stale selector underneath', async () => { + mockMembers = []; + await AsyncStorage.setItem('@ultraedge/event-crew', '[]'); + let tree: any; + const navigation = { goBack: jest.fn(), replace: jest.fn() }; + await act(async () => { tree = create(); }); + const button = tree.root.findAllByType(TouchableOpacity).find((node: any) => node.findAllByType(Text).some((text: any) => typeof text.props.children === 'string' && text.props.children.includes('Create'))); + await act(async () => { button.props.onPress(); }); + expect(navigation.replace).toHaveBeenCalledWith('CreateCrew', { eventId: 'race' }); + await act(async () => { tree.unmount(); }); +}); + +const roleChip = (tree: any, label: string) => tree.root.findAllByType(TouchableOpacity).find((node: any) => node.props.accessibilityLabel === label); + +it('lets a newly created member choose multiple roles before saving', async () => { + let tree: any; + await act(async () => { tree = create(); }); + expect(tree.root.findAllByType(TouchableOpacity).filter((node: any) => node.props.accessibilityRole === 'checkbox')[0].props.accessibilityLabel).toBe('New crew'); + await act(async () => { roleChip(tree, 'Pacer for New crew').props.onPress(); }); + await act(async () => { roleChip(tree, 'Driver for New crew').props.onPress(); }); + expect(roleChip(tree, 'Pacer for New crew').props.accessibilityState.checked).toBe(true); + await act(async () => { await buttonWithText(tree, 'Save').props.onPress(); }); + expect((await loadEventCrewAssignments()).find(a => a.crewMemberId === 'new')?.roles).toEqual(['pacer', 'driver']); + await act(async () => { tree.unmount(); }); +}); + +it('edits an existing assignment without changing the same member’s roles at another race', async () => { + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{ id: 'race' }, { id: 'other-race' }])); + await AsyncStorage.setItem('@ultraedge/event-crew', JSON.stringify([ + { eventId: 'race', crewMemberId: 'existing', roles: ['driver'] }, + { eventId: 'other-race', crewMemberId: 'existing', roles: ['crew_chief'] }, + ])); + let tree: any; + await act(async () => { tree = create(); }); + await act(async () => { roleChip(tree, 'Driver for Existing crew').props.onPress(); }); + await act(async () => { roleChip(tree, 'Pacer for Existing crew').props.onPress(); }); + await act(async () => { await buttonWithText(tree, 'Save').props.onPress(); }); + const saved = await loadEventCrewAssignments(); + expect(saved.find(a => a.eventId === 'race')?.roles).toEqual(['pacer']); + expect(saved.find(a => a.eventId === 'other-race')?.roles).toEqual(['crew_chief']); + await act(async () => { tree.unmount(); }); +}); + + +it('continues straight from creating a race crew member to choosing their roles', async () => { + const CreateScreen = require('../screens/crew/CreateCrewScreen').default; + mockCreateCrewMember.mockResolvedValue({ id: 'created' }); + const navigation = { goBack: jest.fn(), replace: jest.fn() }; + let tree: any; + await act(async () => { tree = create(); }); + await act(async () => { tree.root.findAllByType(TextInput)[0].props.onChangeText('Taylor'); }); + await act(async () => { await buttonWithText(tree, 'Next: Roles').props.onPress(); }); + expect(mockCreateCrewMember).toHaveBeenCalledWith(expect.objectContaining({ name: 'Taylor' })); + expect(navigation.replace).toHaveBeenCalledWith('SelectCrew', { eventId: 'race', selectedCrewId: 'created' }); + expect(navigation.goBack).not.toHaveBeenCalled(); + await act(async () => { tree.unmount(); }); +}); + +jest.mock('@react-navigation/native', () => ({ + useFocusEffect: (callback: () => any) => require('react').useEffect(callback, [callback]), +})); +jest.mock('../context/EventContext', () => ({ + useEvents: () => ({ getEvent: (id: string) => ({ id, name: 'Mountain race' }) }), +})); + +it('opens race-specific role editing from the crew member’s assigned event', async () => { + const DetailScreen = require('../screens/crew/CrewDetailScreen').default; + const navigation = { navigate: jest.fn(), goBack: jest.fn() }; + let tree: any; + await act(async () => { tree = create(); }); + await act(async () => { buttonWithText(tree, 'Edit roles').props.onPress(); }); + expect(navigation.navigate).toHaveBeenCalledWith('SelectCrew', { eventId: 'race', selectedCrewId: 'existing' }); + await act(async () => { tree.unmount(); }); +}); diff --git a/src/__tests__/crewMigration.test.ts b/src/__tests__/crewMigration.test.ts new file mode 100644 index 0000000..da47b14 --- /dev/null +++ b/src/__tests__/crewMigration.test.ts @@ -0,0 +1,115 @@ +/** + * Tests for the legacy crew role migration — profile roles move onto + * per-event assignments, then get stripped from member records. + */ + +import { + migrateLegacyCrewRoles, + StoredCrewMemberRecord, + EventCrewAssignment, +} from '../lib/eventCrew'; + +const baseMember = ( + overrides: Partial & { id: string } +): StoredCrewMemberRecord => ({ + name: 'Test Member', + phone: null, + email: null, + notes: null, + avatar_url: null, + created_at: '2026-01-01T00:00:00.000Z', + updated_at: '2026-01-01T00:00:00.000Z', + ...overrides, +}); + +describe('migrateLegacyCrewRoles', () => { + it('copies a legacy profile role onto all of that member\'s assignments', () => { + const members = [baseMember({ id: 'm1', role: 'pacer', customRole: null })]; + const assignments = [ + { eventId: 'e1', crewMemberId: 'm1' }, + { eventId: 'e2', crewMemberId: 'm1', notes: 'meet at CP3' }, + ] as unknown as EventCrewAssignment[]; + + const result = migrateLegacyCrewRoles(members, assignments); + + expect(result.changed).toBe(true); + expect(result.assignments[0].roles).toEqual(['pacer']); + expect(result.assignments[1].roles).toEqual(['pacer']); + expect(result.assignments[1].notes).toBe('meet at CP3'); + }); + + it('strips role and customRole from stored member records', () => { + const members = [ + baseMember({ id: 'm1', role: 'driver', customRole: null }), + baseMember({ id: 'm2', role: 'other', customRole: 'Chef' }), + ]; + + const result = migrateLegacyCrewRoles(members, []); + + expect(result.changed).toBe(true); + for (const member of result.members) { + expect(member).not.toHaveProperty('role'); + expect(member).not.toHaveProperty('customRole'); + } + // Other fields survive + expect(result.members[0].id).toBe('m1'); + expect(result.members[0].name).toBe('Test Member'); + }); + + it('carries a custom label onto assignments when the legacy role is "other"', () => { + const members = [baseMember({ id: 'm1', role: 'other', customRole: 'Support Runner' })]; + const assignments = [ + { eventId: 'e1', crewMemberId: 'm1' }, + ] as unknown as EventCrewAssignment[]; + + const result = migrateLegacyCrewRoles(members, assignments); + + expect(result.assignments[0].roles).toEqual(['other']); + expect(result.assignments[0].customRole).toBe('Support Runner'); + }); + + it('gives assignments an empty roles array when the member has no legacy role', () => { + const members = [baseMember({ id: 'm1' })]; + const assignments = [ + { eventId: 'e1', crewMemberId: 'm1' }, + { eventId: 'e1', crewMemberId: 'missing-member' }, + ] as unknown as EventCrewAssignment[]; + + const result = migrateLegacyCrewRoles(members, assignments); + + expect(result.assignments[0].roles).toEqual([]); + expect(result.assignments[1].roles).toEqual([]); + }); + + it('is idempotent — a second run reports no changes and alters nothing', () => { + const members = [ + baseMember({ id: 'm1', role: 'medical', customRole: null }), + baseMember({ id: 'm2', role: 'other', customRole: 'Navigator' }), + ]; + const assignments = [ + { eventId: 'e1', crewMemberId: 'm1' }, + { eventId: 'e1', crewMemberId: 'm2' }, + ] as unknown as EventCrewAssignment[]; + + const first = migrateLegacyCrewRoles(members, assignments); + expect(first.changed).toBe(true); + + const second = migrateLegacyCrewRoles(first.members, first.assignments); + expect(second.changed).toBe(false); + expect(second.members).toEqual(first.members); + expect(second.assignments).toEqual(first.assignments); + }); + + it('does not overwrite assignments that already have roles', () => { + const members = [baseMember({ id: 'm1', role: 'pacer', customRole: null })]; + const assignments: EventCrewAssignment[] = [ + { eventId: 'e1', crewMemberId: 'm1', roles: ['driver', 'medical'] }, + ]; + + const result = migrateLegacyCrewRoles(members, assignments); + + // Member still needs stripping, but the modern assignment is untouched + expect(result.changed).toBe(true); + expect(result.assignments[0].roles).toEqual(['driver', 'medical']); + }); +}); diff --git a/src/__tests__/dateOnly.test.ts b/src/__tests__/dateOnly.test.ts new file mode 100644 index 0000000..33457cb --- /dev/null +++ b/src/__tests__/dateOnly.test.ts @@ -0,0 +1,26 @@ +import { parseDateOnly, formatDateOnly, daysUntilDate } from '../lib/dateOnly'; + +it('round-trips the selected calendar day without UTC conversion', () => { + const date = parseDateOnly('2026-09-19')!; + expect([date.getFullYear(), date.getMonth(), date.getDate()]).toEqual([2026, 8, 19]); + expect(formatDateOnly(date)).toBe('2026-09-19'); + // Picker values may carry any time, including shortly after midnight or evening. + expect(formatDateOnly(new Date(2026, 8, 19, 0, 15))).toBe('2026-09-19'); + expect(formatDateOnly(new Date(2026, 8, 19, 23, 45))).toBe('2026-09-19'); +}); +it('counts overnight date boundaries rather than remaining 24-hour periods', () => { + expect(daysUntilDate('2026-09-19', new Date(2026, 8, 19, 23, 59))).toBe(0); + expect(daysUntilDate('2026-09-20', new Date(2026, 8, 19, 23, 59))).toBe(1); + expect(daysUntilDate('2026-09-19', new Date(2026, 8, 20, 0, 1))).toBe(-1); +}); +it('counts correctly across spring and fall daylight-saving boundaries', () => { + expect(daysUntilDate('2026-03-09', new Date(2026, 2, 7, 23))).toBe(2); + expect(daysUntilDate('2026-11-02', new Date(2026, 9, 31, 1))).toBe(2); +}); +it('rejects impossible dates and accepts leap days', () => { + expect(parseDateOnly('2026-02-29')).toBeNull(); + expect(parseDateOnly('2026-13-01')).toBeNull(); + expect(parseDateOnly('garbage')).toBeNull(); + expect(parseDateOnly('2028-02-29')).not.toBeNull(); + expect(daysUntilDate(null)).toBeNull(); +}); diff --git a/src/__tests__/dropBagGearPicker.test.tsx b/src/__tests__/dropBagGearPicker.test.tsx new file mode 100644 index 0000000..e7ab48e --- /dev/null +++ b/src/__tests__/dropBagGearPicker.test.tsx @@ -0,0 +1,63 @@ +jest.mock('expo-image-picker', () => ({ launchImageLibraryAsync: jest.fn() })); +jest.mock('../lib/attachments', () => ({ attachmentUri: (v: string) => v, saveAttachment: jest.fn() })); +import React from 'react'; +import { Keyboard, ScrollView, Text, TextInput, TouchableOpacity } from 'react-native'; +const { act, create } = require('react-test-renderer'); +jest.mock('@expo/vector-icons', () => ({ Ionicons: () => null })); +jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0 }) })); +jest.mock('../theme', () => ({ useTheme: () => ({ theme: { colors: {}, spacing: {}, radius: {} } }) })); +jest.mock('../components/ui', () => { + const { Text, View, TouchableOpacity } = require('react-native'); + const React = require('react'); + return { Text, H2: Text, H3: Text, Body: Text, BodySmall: Text, Caption: Text, Card: View, CardContent: View, + Button: ({ children, ...props }: any) => React.createElement(TouchableOpacity, props, React.createElement(Text, null, children)), + }; +}); +const mockCreateBag = jest.fn(); +const mockUpdateBag = jest.fn(); +const mockBag = { id: 'bag', name: 'Existing bag', eventId: 'race', checkpointId: null, notes: null, items: [] }; +jest.mock('../context/DropBagContext', () => ({ useDropBags: () => ({ + createDropBag: mockCreateBag, updateDropBag: mockUpdateBag, getDropBag: () => mockBag, + templates: [], loading: false, error: null, +}) })); +jest.mock('../context/EventContext', () => ({ useEvents: () => ({ events: [{ id: 'race', name: 'Test race' }], getEvent: () => ({ id: 'race', name: 'Test race' }) }) })); +jest.mock('../context/CheckpointContext', () => ({ useCheckpoints: () => ({ getCheckpointsByEventId: () => [] }), CHECKPOINT_TYPE_INFO: {} })); +jest.mock('../context/GearContext', () => ({ useGear: () => ({ gearItems: [{ id: 'lamp', name: 'Headlamp', retired: false }] }) })); +const CreateScreen = require('../screens/dropbags/CreateDropBagScreen').default; +const EditScreen = require('../screens/dropbags/EditDropBagScreen').default; +const buttonWithText = (tree: any, label: string) => tree.root.findAllByType(TouchableOpacity).find((node: any) => node.findAllByType(Text).some((text: any) => text.props.children === label)); + +beforeEach(() => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + mockCreateBag.mockReset().mockResolvedValue({ id: 'new-bag' }); + mockUpdateBag.mockReset().mockResolvedValue(mockBag); +}); + +it.each([ + ['create', CreateScreen, 'Create Drop Bag'], + ['edit', EditScreen, 'Save Changes'], +])('%s: typed name → one gear tap → save retains contents and blocks save behind picker', async (_kind, Screen, saveLabel) => { + let tree: any; + const navigation = { goBack: jest.fn() }; + const dismiss = jest.spyOn(Keyboard, 'dismiss'); + await act(async () => { tree = create(); }); + const name = tree.root.findAllByType(TextInput)[0]; + await act(async () => { name.props.onChangeText('Night gear'); }); + await act(async () => { buttonWithText(tree, 'Add Gear').props.onPress(); }); + expect(dismiss).toHaveBeenCalled(); + expect(buttonWithText(tree, saveLabel).props.disabled).toBe(true); + await act(async () => { await buttonWithText(tree, saveLabel).props.onPress(); }); + expect(mockCreateBag).not.toHaveBeenCalled(); + expect(mockUpdateBag).not.toHaveBeenCalled(); + const picker = tree.root.findAllByType(ScrollView).find((node: any) => node.findAllByProps({ accessibilityLabel: 'Add Headlamp to drop bag' }).length > 0); + expect(picker.props.keyboardShouldPersistTaps).toBe('handled'); + const addGear = tree.root.findAllByType(TouchableOpacity).find((node: any) => node.props.accessibilityLabel === 'Add Headlamp to drop bag'); + await act(async () => { addGear.props.onPress(); }); + expect(buttonWithText(tree, saveLabel).props.disabled).toBe(false); + await act(async () => { await buttonWithText(tree, saveLabel).props.onPress(); }); + const payload = _kind === 'create' ? mockCreateBag.mock.calls[0][0] : mockUpdateBag.mock.calls[0][1]; + expect(payload).toEqual(expect.objectContaining({ name: 'Night gear', items: [expect.objectContaining({ name: 'Headlamp', refId: 'lamp', quantity: 1 })] })); + expect(navigation.goBack).toHaveBeenCalledTimes(1); + await act(async () => { tree.unmount(); }); + dismiss.mockRestore(); +}); diff --git a/src/__tests__/feedbackPlanning.test.ts b/src/__tests__/feedbackPlanning.test.ts new file mode 100644 index 0000000..e9e9381 --- /dev/null +++ b/src/__tests__/feedbackPlanning.test.ts @@ -0,0 +1,28 @@ +import { requirementStatus } from '../lib/mandatoryGear'; +import { EventGearAllocation } from '../lib/eventGear'; +import { emptyOperations, projectRace, raceStart } from '../lib/raceOperations'; +import { Event, Checkpoint } from '../lib/database.types'; +const requirement = { id: 'lights', name: 'Light sources', quantity: 2, gearIds: ['lamp', 'torch'] }; +const allocation = (id: string, quantity: number, packed: boolean): EventGearAllocation => ({ eventId: 'race', gearItemId: id, quantity, isPacked: packed, isCarried: true, isWorn: false }); +test('mandatory quantity counts distinct assigned items and packing; removed or retired gear never satisfies it', () => { + const rows = [allocation('lamp', 1, true), allocation('torch', 1, false)]; + expect(requirementStatus(requirement, rows, ['lamp','torch'])).toEqual({ assigned: 2, packed: 1, missing: 1, ready: false }); + rows[1].isPacked = true; + expect(requirementStatus(requirement, rows, ['lamp','torch']).ready).toBe(true); + expect(requirementStatus(requirement, rows, ['lamp']).ready).toBe(false); + expect(requirementStatus(requirement, [], ['lamp','torch']).missing).toBe(2); + expect(requirementStatus(requirement, [{...rows[0],isCarried:false}], ['lamp']).assigned).toBe(0); +}); +test('a two-minute versus ten-minute stop shifts downstream ETAs and finish eight minutes, without changing arrival at that stop', () => { + const event = {id:'race',event_date:'2027-08-13',event_time:'08:00',target_time:'30:00',total_distance:120} as Event; + const checkpoints = [{id:'a',name:'A',distance_from_start:40,order_index:0},{id:'b',name:'B',distance_from_start:80,order_index:1}] as Checkpoint[]; + const ops = emptyOperations('race'); + const short = projectRace(event,checkpoints,{...ops,stops:{a:2}})!; + const long = projectRace(event,checkpoints,{...ops,stops:{a:10}})!; + expect(long.rows[0].arrival).toBe(short.rows[0].arrival); + expect(long.rows[1].arrival-short.rows[1].arrival).toBe(8*60000); + expect(long.finish-short.finish).toBe(8*60000); + // A local actual observation must not move the original planning projection. + const recorded = {...ops,stops:{a:10},startAt:new Date(raceStart(event,ops)!+60000).toISOString(),reports:[{checkpointId:'a',kind:'in' as const,source:'runner' as const,at:new Date(short.rows[0].arrival+30*60000).toISOString(),recordedAt:new Date().toISOString()}]}; + expect(projectRace(event,checkpoints,{...recorded,startAt:null,reports:[]})!.finish).toBe(long.finish); +}); diff --git a/src/__tests__/gpx.test.ts b/src/__tests__/gpx.test.ts index 53d5d77..1279564 100644 --- a/src/__tests__/gpx.test.ts +++ b/src/__tests__/gpx.test.ts @@ -151,3 +151,24 @@ describe('distanceMarkers with display units', () => { ); }); }); + +describe('untrusted GPX data', () => { + it('rejects malformed XML and other XML documents', () => { + expect(() => parseGpx('')).toThrow(); + expect(() => parseGpx('')).toThrow(); + }); + it('skips out-of-range and partially numeric coordinates, ignores infinite elevations', () => { + const xml = '' + + '' + + '' + + 'Infinity' + + '100' + + ''; + const points = parseGpx(xml); + expect(points).toHaveLength(2); + expect(points[0].ele).toBeUndefined(); + const metrics = computeRouteMetrics(points)!; + expect(Number.isFinite(metrics.totalDistanceMi)).toBe(true); + expect(metrics.hasElevation).toBe(false); + }); +}); diff --git a/src/__tests__/gpxCheckpoints.test.ts b/src/__tests__/gpxCheckpoints.test.ts new file mode 100644 index 0000000..456aa14 --- /dev/null +++ b/src/__tests__/gpxCheckpoints.test.ts @@ -0,0 +1,51 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { computeRouteMetrics, parseGpx, parseGpxCheckpoints } from '../lib/gpx'; +import { saveGpxPlan } from '../lib/importGpxPlan'; +import { runLocalPlanOperation, readCheckpointMap } from '../lib/localPlanStorage'; + +const xml = `${Array.from({length:20}, (_, i) => `CP ${i+1}100Race marker`).join('')}`; +const metrics = computeRouteMetrics(parseGpx(xml))!; +const checkpoints = parseGpxCheckpoints(xml, metrics); +beforeEach(async () => { + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{id:'race', distance_unit:'kilometers', elevation_unit:'meters'}, {id:'other'}])); +}); +it('extracts all 20 waypoints with interpolated course mileage and metadata', () => { + expect(checkpoints).toHaveLength(20); + expect(checkpoints[0]).toMatchObject({name:'CP 1', ele:100, description:'Race marker'}); + expect(checkpoints[0].distanceMi).toBeCloseTo(metrics.totalDistanceMi / 21, 5); + expect(checkpoints[19].distanceMi).toBeCloseTo(metrics.totalDistanceMi * 20 / 21, 5); +}); +it('supports prefixed GPX, named route/track points and skips invalid coordinates and ordinary samples', () => { + const source = 'FinishAid'; + const route = computeRouteMetrics(parseGpx(source))!; + expect(parseGpxCheckpoints(source, route).map(p => p.name)).toEqual(['Aid', 'Finish']); + expect(parseGpxCheckpoints('', route)).toEqual([]); +}); +it('saves checkpoints and route, deduplicates reimport, preserves edited fields and other races', async () => { + const stats = {...metrics, checkpoints}; + expect(await saveGpxPlan('race', 'file:///course.gpx', stats)).toBe(20); + let map = await readCheckpointMap(); + map.race[0].notes = 'Bring gels'; map.race[0].has_drop_bag = true; + map.other = [{id:'unrelated'}]; + await AsyncStorage.setItem('ultraedge_checkpoints', JSON.stringify(map)); + expect(await saveGpxPlan('race', 'file:///course.gpx', stats)).toBe(0); + map = await readCheckpointMap(); + expect(map.race).toHaveLength(20); + expect(map.race[0]).toMatchObject({notes:'Bring gels',has_drop_bag:true}); + expect(map.race[0].elevation).toBeCloseTo(328.084); + expect(map.other).toEqual([{id:'unrelated'}]); + const events = JSON.parse((await AsyncStorage.getItem('@ultraedge/events'))!); + expect(events[0].gpx_file_url).toBe('file:///course.gpx'); + expect(events[0].total_distance).toBeCloseTo(metrics.totalDistanceMi * 1.609344, 0); +}); +it('recovers both records after an interrupted write and rejects deleted races', async () => { + const original = (AsyncStorage.setItem as jest.Mock).getMockImplementation()!; + jest.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(original).mockRejectedValueOnce(new Error('Disk full')); + await expect(saveGpxPlan('race', 'file:///staged.gpx', {...metrics, checkpoints})).rejects.toThrow('Disk full'); + (AsyncStorage.setItem as jest.Mock).mockImplementation(original); + await runLocalPlanOperation(async () => {}); + expect((await readCheckpointMap()).race).toHaveLength(20); + expect(JSON.parse((await AsyncStorage.getItem('@ultraedge/events'))!)[0].gpx_file_url).toBe('file:///staged.gpx'); + await expect(saveGpxPlan('deleted', 'file:///x', {...metrics, checkpoints})).rejects.toThrow('deleted'); +}); diff --git a/src/__tests__/importGpx.test.ts b/src/__tests__/importGpx.test.ts new file mode 100644 index 0000000..95fc039 --- /dev/null +++ b/src/__tests__/importGpx.test.ts @@ -0,0 +1,79 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { saveGpxPlan } from '../lib/importGpxPlan'; +import { readCheckpointMap } from '../lib/localPlanStorage'; +import { importLocalGpx } from '../lib/importGpx'; +let mockXml = ''; +let mockSize = 100; +const mockCopy = jest.fn(); +const mockDelete = jest.fn(); +jest.mock('expo-file-system', () => ({ + File: class { + uri: string; exists = true; + constructor(base: unknown, name?: string) { this.uri = name ? `file:///gpx/${name}` : String(base); } + get size() { return mockSize; } + text = async () => mockXml; + copy = mockCopy; + write = mockCopy; + delete() { mockDelete(this.uri); } + }, + Directory: class { create() {} }, Paths: { document: 'file:///docs' }, +})); +beforeEach(() => { + jest.clearAllMocks(); mockSize = 100; + mockXml = ''; +}); +it('does not copy or change the saved route for invalid or oversized imports', async () => { + const save = jest.fn(); mockSize = 11 * 1024 * 1024; + await expect(importLocalGpx('file:///picked.gpx', 'race', save)).rejects.toThrow('10 MB'); + mockSize = 100; mockXml = ''; + await expect(importLocalGpx('file:///picked.gpx', 'race', save)).rejects.toThrow('two valid'); + expect(save).not.toHaveBeenCalled(); expect(mockCopy).not.toHaveBeenCalled(); +}); +it('keeps the staged file recoverable when a journaled save fails', async () => { + const save = jest.fn().mockRejectedValue(new Error('Disk full')); + await expect(importLocalGpx('file:///old.gpx', 'race', save)).rejects.toThrow('Disk full'); + const staged = save.mock.calls[0][0]; + expect(staged).not.toBe('file:///old.gpx'); + expect(mockDelete).not.toHaveBeenCalled(); + expect(mockDelete).not.toHaveBeenCalledWith('file:///old.gpx'); +}); +it('returns a new file only after the saved reference succeeds', async () => { + const save = jest.fn().mockResolvedValue(undefined); + const uri = await importLocalGpx('file:///picked.gpx', 'race', save); + expect(save).toHaveBeenCalledWith(uri, expect.objectContaining({ elevationGainFt: null })); + expect(mockDelete).not.toHaveBeenCalled(); +}); + +it.each(['fatdog-120-gaia.xml', 'fatdog-120-gaia.kml'])('imports original Gaia %s into 15 ordered, persistent checkpoints without duplicates', async (fixture) => { + mockXml = readFileSync(resolve(__dirname, '../../docs/release/fixtures', fixture), 'utf8'); + mockSize = Buffer.byteLength(mockXml); + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{ id: 'fatdog', distance_unit: 'miles', elevation_unit: 'feet' }])); + const save = async (uri: string, stats: Parameters[2]) => { + await saveGpxPlan('fatdog', uri, stats); + }; + await importLocalGpx('file:///fatdog.xml', 'fatdog', save); + const checkpoints = (await readCheckpointMap()).fatdog; + expect(checkpoints.map(cp => cp.name)).toEqual([ + 'Start Line', 'Cathedral Aid Station', 'Ashnola Aid Station', 'Trapper Aid Station', + 'Calcite Aid Station', 'Pasayten Aid Station', 'Bonnevier Aid Station', 'Heather Aid Station', + 'Nicomen Aid Station', 'Grainger Creek Aid Station', 'Hope Pass Aid Station', + 'Blackwall Peak Aid Station', "Windy Joe's Aid Station", 'Strawberry Flats Aid Station', 'Finish Line', + ]); + expect(checkpoints[0].distance_from_start).toBeCloseTo(0, 1); + expect(checkpoints[14].distance_from_start).toBeCloseTo(122.83, 2); + expect(checkpoints[4].location_description).toBe('Major Aid Station, drop bags, no crew access'); + expect(checkpoints[4].has_crew_access).toBe(false); + expect(checkpoints[1].elevation).toBeCloseTo(2141 * 3.28084); + await importLocalGpx('file:///fatdog.xml', 'fatdog', save); + expect((await readCheckpointMap()).fatdog.map(cp => cp.id)).toEqual(checkpoints.map(cp => cp.id)); +}); +it('rejects XML that is not GPX before copying or saving', async () => { + mockXml = ''; + const save = jest.fn(); + await expect(importLocalGpx('file:///unrelated.xml', 'race', save)).rejects.toThrow('Choose a GPX'); + expect(save).not.toHaveBeenCalled(); + expect(mockCopy).not.toHaveBeenCalled(); +}); diff --git a/src/__tests__/listStorageErrors.test.tsx b/src/__tests__/listStorageErrors.test.tsx new file mode 100644 index 0000000..b341cc3 --- /dev/null +++ b/src/__tests__/listStorageErrors.test.tsx @@ -0,0 +1,54 @@ +jest.mock('../components/LiveRacePanel', () => () => null); +import React from 'react'; +import { Text, TouchableOpacity } from 'react-native'; +const { act, create } = require('react-test-renderer'); +const mockRefresh = jest.fn(async () => {}); +let mockError: string | null = 'Saved data could not be loaded'; +jest.mock('@expo/vector-icons', () => ({ Ionicons: () => null })); +jest.mock('@react-navigation/native', () => ({ useFocusEffect: () => {} })); +jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ top: 0, bottom: 0 }) })); +jest.mock('react-native-gesture-handler', () => ({ + GestureHandlerRootView: ({ children }: any) => children, + Swipeable: ({ children }: any) => children, +})); +jest.mock('../theme', () => ({ useTheme: () => ({ theme: { colors: {}, spacing: {}, radius: {} } }) })); +jest.mock('../components/ui', () => { + const { Text, View, TouchableOpacity } = require('react-native'); + const React = require('react'); + return { + Text, H1: Text, H2: Text, H3: Text, Body: Text, BodySmall: Text, Caption: Text, + Card: View, CardContent: View, WeightBadge: () => null, + Button: ({ children, onPress, disabled }: any) => React.createElement(TouchableOpacity, { onPress, disabled }, React.createElement(Text, null, children)), + }; +}); +jest.mock('../context/EventContext', () => ({ useEvents: () => ({ events: [], loading: false, error: mockError, refreshEvents: mockRefresh }) })); +jest.mock('../context/CrewContext', () => ({ useCrewMembers: () => ({ crewMembers: [], loading: false, error: mockError, refreshCrewMembers: mockRefresh }) })); +jest.mock('../context/GearContext', () => ({ useGear: () => ({ gearItems: [], loading: false, error: mockError, refreshGear: mockRefresh }) })); +jest.mock('../context/DropBagContext', () => ({ useDropBags: () => ({ dropBags: [], loading: false, error: mockError, refreshDropBags: mockRefresh }) })); +jest.mock('../context/CheckpointContext', () => ({ useCheckpoints: () => ({ loading: false, error: null, refreshCheckpoints: mockRefresh }) })); + +it.each([ + ['events', '../screens/events/EventsListScreen', 'Create Event'], + ['crew', '../screens/crew/CrewListScreen', 'Add Your First Crew Member'], + ['gear', '../screens/gear/GearListScreen', 'Add Your First Item'], + ['drop bags', '../screens/dropbags/DropBagsListScreen', 'Create Your First Drop Bag'], +])('%s reports read failure instead of a false first-use state and offers real retry', async (_, modulePath, emptyCTA) => { + mockError = 'Saved data could not be loaded'; + mockRefresh.mockClear(); + const Screen = require(modulePath).default; + let tree: any; + await act(async () => { tree = create(); }); + const labels = tree.root.findAllByType(Text).map((node: any) => node.props.children).flat().join(' '); + expect(labels).toContain(mockError); + expect(labels).not.toContain(emptyCTA); + const retry = tree.root.findAllByType(TouchableOpacity).find((node: any) => + node.findAllByType(Text).some((text: any) => text.props.children === 'Retry loading saved data')); + expect(retry).toBeDefined(); + await act(async () => { retry.props.onPress(); }); + expect(mockRefresh).toHaveBeenCalled(); + mockError = null; + await act(async () => { tree.update(); }); + const recoveredLabels = tree.root.findAllByType(Text).map((node: any) => node.props.children).flat().join(' '); + expect(recoveredLabels).toContain(emptyCTA); + await act(async () => { tree.unmount(); }); +}); diff --git a/src/__tests__/liveRaceModel.test.ts b/src/__tests__/liveRaceModel.test.ts new file mode 100644 index 0000000..59743eb --- /dev/null +++ b/src/__tests__/liveRaceModel.test.ts @@ -0,0 +1,27 @@ +import { liveOperations, liveProjection, makeLiveSnapshot, nextReportRevision, LiveRoom } from '../lib/liveRaceModel'; +import {emptyOperations} from '../lib/raceOperations'; +import {Event,Checkpoint} from '../lib/database.types'; +const start='2026-01-01T08:00:00Z'; +const event={id:'race',name:'Test',event_date:'2026-01-01',event_time:'08:00',target_time:'10:00',total_distance:100,distance_unit:'kilometers',description:'private',gpx_file_url:'private'} as Event; +const cps=[{id:'half',name:'Half',order_index:0,distance_from_start:50},{id:'finish',name:'Finish',order_index:1,distance_from_start:100}] as Checkpoint[]; +const snapshot=makeLiveSnapshot(event,cps,{...emptyOperations('race'),startAt:start,stops:{half:30}},['A van']); +const room=():LiveRoom=>({id:'room',ownerId:'runner',revision:1,invite:null,snapshot,serverTime:start,members:[],reports:[]}); +const report=(authorId:string,kind:'in'|'out',at:string|null,checkpointId='half')=>({authorId,checkpointId,kind,at,revision:1,receivedAt:'2026-01-01T14:00:00Z'}); +test('shared snapshot excludes contact details, notes and local time reports',()=>{ + expect(snapshot.event).not.toHaveProperty('description');expect(snapshot.event).not.toHaveProperty('gpx_file_url');expect(snapshot.operations.reports).toEqual([]);expect(Date.parse(snapshot.startAt)).toBe(Date.parse(start)); +}); +test('multiple crew reports do not outweigh runner and tombstones do not affect average',()=>{ + const r=room();r.reports=[report('runner','in','2026-01-01T13:00:00Z'),report('crew1','in','2026-01-01T13:10:00Z'),report('crew2','in','2026-01-01T13:30:00Z'),report('crew3','in',null)]; + const ops=liveOperations(r);expect(ops.reports).toHaveLength(2);expect(ops.reports.find(r=>r.source==='crew')!.at).toBe('2026-01-01T13:20:00.000Z');expect(liveProjection(r)!.rows[0].arrival).toBe(Date.parse('2026-01-01T13:10:00Z')); +}); +test('conflicting cross-author observations remain visible but suppress impossible ETAs',()=>{ + const r=room();r.reports=[report('runner','in','2026-01-01T14:00:00Z'),report('crew','out','2026-01-01T13:00:00Z')];expect(liveProjection(r)).toBeNull();expect(r.reports).toHaveLength(2); +}); +test('outbox correction revisions follow pending operations and isolate actors/stations',()=>{ + const r=room();r.reports=[report('runner','in','2026-01-01T13:00:00Z')]; + const p={roomId:'room',authorId:'runner',checkpointId:'half',kind:'in' as const,at:null,revision:1,requestId:'one'}; + expect(nextReportRevision(r,[p],'runner','half','in')).toBe(2);expect(nextReportRevision(r,[p],'crew','half','in')).toBe(0);expect(nextReportRevision(r,[p],'runner','finish','in')).toBe(0); +}); +test('absolute start produces the same timeline regardless of event date formatting',()=>{ + const r=room();r.snapshot={...snapshot,event:{...event,event_time:'19:30'}};expect(liveProjection(r)!.start).toBe(Date.parse(start)); +}); diff --git a/src/__tests__/liveRaceSync.test.ts b/src/__tests__/liveRaceSync.test.ts new file mode 100644 index 0000000..95761a8 --- /dev/null +++ b/src/__tests__/liveRaceSync.test.ts @@ -0,0 +1,37 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import {LiveRoom} from '../lib/liveRaceModel'; +import {emptyOperations} from '../lib/raceOperations'; +const mockRpc=jest.fn(); +jest.mock('expo-crypto',()=>({randomUUID:()=>require('crypto').randomUUID()})); +jest.mock('../lib/supabase',()=>({createSecureStorageAdapter:()=>({})})); +jest.mock('@supabase/supabase-js',()=>({createClient:()=>({auth:{getSession:async()=>({data:{session:{user:{id:'runner'}}},error:null})},rpc:(...args:unknown[])=>mockRpc(...args)})})); +import {queueLiveReport,syncLiveRoom,readLiveCache} from '../lib/liveRaceSync'; +const key='@ultraedge/live-rooms-v1'; +const room:LiveRoom={id:'room',ownerId:'runner',revision:1,invite:null,serverTime:'2026-01-01T08:00:00Z',snapshot:{event:{id:'race'} as any,checkpoints:[{id:'half'} as any],startAt:'2026-01-01T08:00:00Z',operations:emptyOperations('race'),logistics:[]},members:[],reports:[]}; +beforeEach(async()=>{process.env.EXPO_PUBLIC_SUPABASE_URL='https://example.supabase.co';process.env.EXPO_PUBLIC_SUPABASE_KEY='test';jest.clearAllMocks();await AsyncStorage.clear();await AsyncStorage.setItem(key,JSON.stringify({rooms:[room],actorId:'runner',pending:[],lastSync:{}}));}); +test('offline reports persist; reconnect sends each independently and acknowledges only successes',async()=>{ + await queueLiveReport(room,'half','in','2026-01-01T13:00:00Z');await queueLiveReport(room,'half','in','2026-01-01T13:10:00Z'); + expect((await readLiveCache()).pending.map(r=>r.revision)).toEqual([0,1]); + mockRpc.mockImplementation(async(_,{action}:any)=>action==='read'?{data:room,error:null}:{data:null,error:{message:'offline'}}); + await expect(syncLiveRoom('room')).rejects.toEqual({message:'offline'});expect((await readLiveCache()).pending).toHaveLength(2); + mockRpc.mockImplementation(async(_,{action}:any)=>({data:action==='read'?room:{revision:1},error:null})); + await syncLiveRoom('room');expect((await readLiveCache()).pending).toHaveLength(0);expect((await readLiveCache()).lastSync.room).toBeTruthy(); +}); +test('lost local acknowledgement retries the same operation id',async()=>{ + await queueLiveReport(room,'half','in','2026-01-01T13:00:00Z');const operation=(await readLiveCache()).pending[0];const requests:string[]=[]; + mockRpc.mockImplementation(async(_,{action,args}:any)=>{if(action==='report')requests.push(args.requestId);return {data:action==='read'?room:{revision:1},error:null};}); + const original=(AsyncStorage.setItem as jest.Mock).getMockImplementation()!;let fail=true; + const spy=jest.spyOn(AsyncStorage,'setItem').mockImplementation(async(k,v)=>{if(k===key&&fail&&JSON.parse(v).pending.length===0){fail=false;throw new Error('disk error');}return original(k,v);}); + await expect(syncLiveRoom('room')).rejects.toThrow('disk error');spy.mockImplementation(original); + await syncLiveRoom('room');expect(requests).toEqual([operation.requestId,operation.requestId]);expect((await readLiveCache()).pending).toHaveLength(0); +}); +test('revoked access does not upload or silently discard offline evidence',async()=>{ + await queueLiveReport(room,'half','in','2026-01-01T13:00:00Z');mockRpc.mockResolvedValue({data:null,error:{code:'42501',message:'revoked'}}); + await expect(syncLiveRoom('room')).rejects.toMatchObject({code:'42501'});expect(mockRpc).toHaveBeenCalledTimes(1);expect((await readLiveCache()).pending).toHaveLength(1); +}); +test('forget waits for an in-flight sync so it cannot resurrect the removed cache',async()=>{ + const {forgetLiveRoom}=require('../lib/liveRaceSync'); + let release!:()=>void,entered!:()=>void;const gate=new Promise(resolve=>{release=resolve;});const started=new Promise(resolve=>{entered=resolve;});let first=true; + mockRpc.mockImplementation(async()=>{if(first){first=false;entered();await gate;}return {data:room,error:null};}); + const sync=syncLiveRoom('room');await started;const forgetting=forgetLiveRoom('room');release();await sync;await forgetting;expect((await readLiveCache()).rooms).toEqual([]); +}); diff --git a/src/__tests__/localApp.test.tsx b/src/__tests__/localApp.test.tsx new file mode 100644 index 0000000..645a267 --- /dev/null +++ b/src/__tests__/localApp.test.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Text } from 'react-native'; +const { act, create } = require('react-test-renderer'); + +// These modules must stay outside the local release's startup graph. +jest.mock('../context/AuthContext', () => { throw new Error('Local app imported account services'); }); +jest.mock('../context/SubscriptionContext', () => { throw new Error('Local app imported billing'); }); +jest.mock('@sentry/react-native', () => { throw new Error('Local app imported diagnostics'); }); +jest.mock('react-native-get-random-values', () => ({})); +jest.mock('react-native-url-polyfill/auto', () => ({})); +jest.mock('expo-status-bar', () => ({ StatusBar: () => null })); +jest.mock('react-native-safe-area-context', () => ({ SafeAreaProvider: ({ children }: any) => children })); +jest.mock('react-native-gesture-handler', () => ({ GestureHandlerRootView: ({ children }: any) => children })); +jest.mock('../theme', () => ({ + ThemeProvider: ({ children }: any) => children, + useTheme: () => ({ isDarkMode: false }), +})); +jest.mock('../hooks/useFonts', () => ({ useFonts: () => ({ fontsLoaded: true }) })); +jest.mock('../context/MoverContext', () => ({ MoverProvider: ({ children }: any) => children })); +jest.mock('../context/EventContext', () => ({ EventProvider: ({ children }: any) => children })); +jest.mock('../context/CheckpointContext', () => ({ CheckpointProvider: ({ children }: any) => children })); +jest.mock('../context/GearContext', () => ({ GearProvider: ({ children }: any) => children })); +jest.mock('../context/CrewContext', () => ({ CrewProvider: ({ children }: any) => children })); +jest.mock('../context/DropBagContext', () => ({ DropBagProvider: ({ children }: any) => children })); +jest.mock('../components/ErrorBoundary', () => ({ children }: any) => children); +jest.mock('../navigation/AppNavigator', () => { + const React = require('react'); + const { Text } = require('react-native'); + return () => React.createElement(Text, null, 'Local planner'); +}); + +it('reaches the local planner without importing account, billing or diagnostic providers', async () => { + const App = require('../../App').default; + let tree: any; + await act(async () => { tree = create(); }); + expect(tree.root.findByType(Text).props.children).toBe('Local planner'); + await act(async () => { tree.unmount(); }); +}); diff --git a/src/__tests__/localGpxUri.test.ts b/src/__tests__/localGpxUri.test.ts new file mode 100644 index 0000000..103bb89 --- /dev/null +++ b/src/__tests__/localGpxUri.test.ts @@ -0,0 +1,20 @@ +import { resolveLocalGpxUri } from '../lib/localGpxUri'; +const documents = 'file:///new-container/Documents/'; +it('finds the preserved course after an iOS update relocates its data container', () => { + const old = 'file:///old-container/Documents/gpx/race-123.gpx'; + expect(resolveLocalGpxUri(old, documents)).toBe('file:///new-container/Documents/gpx/race-123.gpx'); +}); +it('is stable for current-container URIs and preserves encoded filenames', () => { + const current = 'file:///new-container/Documents/gpx/race%20day.gpx'; + expect(resolveLocalGpxUri(current, documents)).toBe(current); +}); +it.each([ + 'file:///old-container/Library/Caches/course.gpx', + 'file:///old-container/Documents/other/course.gpx', + 'file:///old-container/Documents/gpx/../secret', + 'file:///old-container/Documents/gpx/%2E%2E', + 'file:///old-container/Documents/gpx/a%2Fb.gpx', + 'file:///old-container/Documents/gpx/%GG', +])('does not reinterpret files outside the app-owned GPX directory: %s', uri => { + expect(resolveLocalGpxUri(uri, documents)).toBe(uri); +}); diff --git a/src/__tests__/localRelease.test.ts b/src/__tests__/localRelease.test.ts new file mode 100644 index 0000000..110d457 --- /dev/null +++ b/src/__tests__/localRelease.test.ts @@ -0,0 +1,31 @@ +/** Build secrets must never silently turn the free local release into an online app. */ +jest.mock('@supabase/supabase-js', () => ({ createClient: jest.fn() })); +jest.mock('expo-secure-store', () => ({ + getItemAsync: jest.fn(), setItemAsync: jest.fn(), deleteItemAsync: jest.fn(), +})); +jest.mock('react-native-get-random-values', () => ({})); +jest.mock('react-native-url-polyfill/auto', () => ({})); + +it('does not construct a cloud client or touch session storage even with valid build credentials', async () => { + const previousUrl = process.env.EXPO_PUBLIC_SUPABASE_URL; + const previousKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY; + process.env.EXPO_PUBLIC_SUPABASE_URL = 'https://race-planning.supabase.co'; + process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY = 'eyJvalid-looking-public-key-for-build-test'; + try { + const { isAuthAvailable, supabase, requireSupabase, signIn } = require('../lib/supabase'); + const { createClient } = require('@supabase/supabase-js'); + const secureStore = require('expo-secure-store'); + expect(isAuthAvailable).toBe(false); + expect(supabase).toBeNull(); + expect(createClient).not.toHaveBeenCalled(); + expect(secureStore.getItemAsync).not.toHaveBeenCalled(); + expect(() => requireSupabase()).toThrow('Account services are unavailable'); + await expect(signIn('runner@example.com', 'unused')).rejects.toThrow('Account services are unavailable'); + expect(createClient).not.toHaveBeenCalled(); + } finally { + if (previousUrl === undefined) delete process.env.EXPO_PUBLIC_SUPABASE_URL; + else process.env.EXPO_PUBLIC_SUPABASE_URL = previousUrl; + if (previousKey === undefined) delete process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY; + else process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY = previousKey; + } +}); diff --git a/src/__tests__/onboarding.test.ts b/src/__tests__/onboarding.test.ts new file mode 100644 index 0000000..ee77eea --- /dev/null +++ b/src/__tests__/onboarding.test.ts @@ -0,0 +1,34 @@ +/** + * Tests for the first-launch onboarding flag. + */ + +import AsyncStorage from '@react-native-async-storage/async-storage'; + +import { + isOnboardingComplete, + setOnboardingComplete, + ONBOARDING_COMPLETE_KEY, +} from '../lib/onboarding'; + +describe('onboarding flag', () => { + beforeEach(async () => { + await AsyncStorage.clear(); + }); + + it('reports incomplete on first launch', async () => { + expect(await isOnboardingComplete()).toBe(false); + }); + + it('reports complete after the flag is set', async () => { + await setOnboardingComplete(); + expect(await isOnboardingComplete()).toBe(true); + expect(await AsyncStorage.getItem(ONBOARDING_COMPLETE_KEY)).toBe('true'); + }); + + it('defaults to complete if storage is unreadable', async () => { + jest + .spyOn(AsyncStorage, 'getItem') + .mockRejectedValueOnce(new Error('storage unavailable')); + expect(await isOnboardingComplete()).toBe(true); + }); +}); diff --git a/src/__tests__/privacySupport.test.tsx b/src/__tests__/privacySupport.test.tsx new file mode 100644 index 0000000..24bf406 --- /dev/null +++ b/src/__tests__/privacySupport.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Alert, Linking, Text, TouchableOpacity } from 'react-native'; +import PrivacySupportScreen from '../screens/settings/PrivacySupportScreen'; +const { act, create } = require('react-test-renderer'); +jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: () => ({ bottom: 0 }) })); +jest.mock('../theme', () => ({ useTheme: () => ({ theme: { colors: {} } }) })); +jest.mock('../components/ui', () => { + const { Text } = require('react-native'); + return { H2: Text, Body: Text, BodySmall: Text }; +}); +afterEach(() => { jest.restoreAllMocks(); }); + +it('renders privacy and support offline and opens the public tracker only when explicitly pressed', async () => { + const open = jest.spyOn(Linking, 'openURL').mockResolvedValue(undefined); + let tree: any; + await act(async () => { tree = create(); }); + expect(open).not.toHaveBeenCalled(); + const copy = tree.root.findAllByType(Text).map((node: any) => node.props.children).flat().join(' '); + expect(copy).toContain('Offload App keeps'); + expect(copy).toContain('Issues are public'); + expect(copy).toContain('cannot restore an editable plan'); + await act(async () => { tree.root.findByType(TouchableOpacity).props.onPress(); }); + expect(open).toHaveBeenCalledWith('https://github.com/beaux-riel/UltraEdge/issues'); + await act(async () => { tree.unmount(); }); +}); + +it('keeps offline help available when the external support link cannot open', async () => { + jest.spyOn(Linking, 'openURL').mockRejectedValue(new Error('Offline')); + const alert = jest.spyOn(Alert, 'alert').mockImplementation(() => {}); + let tree: any; + await act(async () => { tree = create(); }); + await act(async () => { tree.root.findByType(TouchableOpacity).props.onPress(); }); + expect(alert).toHaveBeenCalledWith('Could not open support', expect.stringContaining('github.com/beaux-riel/UltraEdge/issues')); + await act(async () => { tree.unmount(); }); +}); diff --git a/src/__tests__/raceOperations.test.ts b/src/__tests__/raceOperations.test.ts new file mode 100644 index 0000000..4413a9c --- /dev/null +++ b/src/__tests__/raceOperations.test.ts @@ -0,0 +1,100 @@ +import { Event, Checkpoint } from '../lib/database.types'; +import { emptyOperations, projectRace, projectedDistance, durationMinutes, putTimeReport, reportAverage, raceStart, changeOperations, loadOperations, OPERATIONS_KEY } from '../lib/raceOperations'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { deleteLocalEvent } from '../lib/localPlanStorage'; +const event = { id: 'race', event_date: '2025-01-01', event_time: '08:00', total_distance: 100, target_time: '10:00' } as Event; +const cp = (id: string, distance: number, index: number): Checkpoint => ({ id, distance_from_start: distance, order_index: index, name: id, event_id: 'race', estimated_duration: null } as Checkpoint); +const checkpoints = [cp('half', 50, 0), cp('finish', 100, 1)]; +const start = raceStart(event, emptyOperations('race'))!; +const time = (minutes: number) => new Date(start + minutes * 60000).toISOString(); +const report = (checkpointId: string, source: 'runner' | 'crew', kind: 'in' | 'out', minutes: number) => ({ checkpointId, source, kind, at: time(minutes), recordedAt: time(minutes) }); + +test('accepts ultra durations over 24h and rejects ambiguous durations', () => { + expect(durationMinutes('36:30')).toBe(2190); + expect(durationMinutes('24:00:30')).toBe(1440.5); + expect(durationMinutes('10:99')).toBeNull(); expect(durationMinutes('fast')).toBeNull(); +}); +test('planned dwell adds to downstream arrival and finish but not uncertainty', () => { + const plan = projectRace(event, checkpoints, { ...emptyOperations('race'), stops: { half: 30 } })!; + expect(plan.rows[0].arrival).toBe(start + 300 * 60000); + expect(plan.rows[0].departure).toBe(start + 330 * 60000); + expect(plan.finish).toBe(start + 630 * 60000); + expect(plan.bestFinish).toBe(start + 570 * 60000); + expect(plan.slowFinish).toBe(start + 690 * 60000); +}); +test('runner and crew receive equal weight and a correction replaces the same source', () => { + let ops = putTimeReport(emptyOperations('race'), report('half', 'runner', 'in', 300), checkpoints, start); + ops = putTimeReport(ops, report('half', 'crew', 'in', 320), checkpoints, start); + expect(reportAverage(ops.reports, 'half', 'in')).toBe(start + 310 * 60000); + ops = putTimeReport(ops, report('half', 'runner', 'in', 310), checkpoints, start); + expect(ops.reports).toHaveLength(2); + expect(reportAverage(ops.reports, 'half', 'in')).toBe(start + 315 * 60000); +}); +test('live departure excludes real dwell from observed moving pace', () => { + const ops = { ...emptyOperations('race'), stops: { half: 30 }, reports: [report('half','runner','in',360),report('half','runner','out',420)] }; + const plan = projectRace(event, checkpoints, ops)!; + expect(plan.finish).toBe(start + 780 * 60000); + expect(plan.rows[0].departure).toBe(start + 420 * 60000); +}); +test('arrival-only anchor preserves planned dwell before next travel', () => { + const ops = { ...emptyOperations('race'), stops: { half: 30 }, reports: [report('half','runner','in',360)] }; + expect(projectRace(event, checkpoints, ops)!.finish).toBe(start + 750 * 60000); +}); +test('rejects reversed reports, future observations and missing start', () => { + const ops = putTimeReport(emptyOperations('race'), report('half','runner','in',300),checkpoints,start); + expect(() => putTimeReport(ops,report('half','runner','out',299),checkpoints,start)).toThrow(); + expect(() => putTimeReport(ops,report('finish','crew','in',200),checkpoints,start)).toThrow(); + expect(() => putTimeReport(ops,{...report('finish','crew','in',200),at:new Date(Date.now()+3600000).toISOString()},checkpoints,start)).toThrow(); + expect(() => putTimeReport(ops,report('half','runner','in',300),checkpoints,null)).toThrow(); +}); +test('missing distance and decreasing station distances do not invent ETAs', () => { + expect(projectRace(event,[cp('a',80,0),cp('b',40,1)],emptyOperations('race'))).toBeNull(); + expect(projectRace(event,[{...checkpoints[0],distance_from_start:null}],emptyOperations('race'))).toBeNull(); +}); +test('progress remains stationary through dwell and respects uncertainty', () => { + const plan = projectRace(event, checkpoints, { ...emptyOperations('race'), stops: { half: 30 } })!; + expect(projectedDistance(plan,100,start+315*60000)).toBe(50); + expect(projectedDistance(plan,100,start+150*60000)).toBe(25); + expect(projectedDistance(plan,100,start+150*60000,'best')).toBeGreaterThan(25); + expect(projectedDistance(plan,100,start+150*60000,'slow')).toBeLessThan(25); + expect(projectedDistance(plan,100,start+900*60000)).toBe(100); +}); +test('zero-distance start dwell is included, multi-day rollover remains absolute', () => { + const long = { ...event, target_time: '30:00' }; + const plan = projectRace(long,[cp('start',0,0),...checkpoints.map(c=>({...c,order_index:c.order_index+1}))],{...emptyOperations('race'),stops:{start:10}})!; + expect(plan.finish-start).toBe(1810*60000); +}); +test('two colocated stations each retain their own stop', () => { + const plan = projectRace(event,[cp('a',50,0),cp('b',50,1),cp('finish',100,2)],{...emptyOperations('race'),stops:{a:10,b:20},reports:[report('a','runner','in',300)]})!; + expect(plan.rows[1].arrival).toBe(start+310*60000); + expect(plan.finish).toBe(start+630*60000); +}); +test('serialized changes preserve parallel station and vehicle edits and survive reload', async () => { + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events',JSON.stringify([event])); + await AsyncStorage.setItem('ultraedge_checkpoints',JSON.stringify({race:checkpoints})); + await Promise.all([ + changeOperations('race', current=>({...current,stops:{half:20}})), + changeOperations('race', current=>({...current,vehicles:[{id:'v',name:'Van',ownerId:null,crewIds:[]}]})), + ]); + const saved = await loadOperations('race'); + expect(saved.stops.half).toBe(20);expect(saved.vehicles[0].name).toBe('Van'); + await deleteLocalEvent('race'); + expect(JSON.parse((await AsyncStorage.getItem(OPERATIONS_KEY))!)).toEqual([]); +}); +test('deleted station and crew references are excluded without deleting vehicles', async () => { + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events',JSON.stringify([event])); + await AsyncStorage.setItem(OPERATIONS_KEY,JSON.stringify([{...emptyOperations('race'),stops:{deleted:30},duties:[{checkpointId:'deleted',crewMemberId:'missing',role:'support'}],vehicles:[{id:'v',name:'Van',ownerId:'missing',crewIds:['missing']}],reports:[report('deleted','runner','in',300)]}])); + const saved = await loadOperations('race'); + expect(saved.stops).toEqual({});expect(saved.duties).toEqual([]);expect(saved.reports).toEqual([]);expect(saved.vehicles[0]).toMatchObject({ownerId:null,crewIds:[]}); +}); + +test('different race logistics stay independent and invalid settings never persist', async () => { + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([event,{...event,id:'second'}])); + await changeOperations('race',current=>({...current,variation:20})); + await expect(changeOperations('race',current=>({...current,variation:99}))).rejects.toThrow(); + expect((await loadOperations('race')).variation).toBe(20); + expect((await loadOperations('second')).variation).toBe(10); +}); diff --git a/src/__tests__/racePlanPdf.test.ts b/src/__tests__/racePlanPdf.test.ts new file mode 100644 index 0000000..329141e --- /dev/null +++ b/src/__tests__/racePlanPdf.test.ts @@ -0,0 +1,66 @@ +import { buildRacePlanHtml, loadGpxXmlForEvent, exportRacePlan, RacePlanData } from '../lib/racePlanPdf'; +import * as Sharing from 'expo-sharing'; +import * as Print from 'expo-print'; + +let mockXml = ''; +const mockOpenedFiles: string[] = []; +jest.mock('expo-file-system', () => ({ + File: class { constructor(uri: string) { mockOpenedFiles.push(uri); } exists = true; uri = 'file:///plan.pdf'; text = async () => mockXml; delete() {} move() {} }, + Directory: class { create() {} }, Paths: { document: { uri: 'file:///new-container/Documents/' }, cache: 'file:///cache' }, +})); +jest.mock('expo-print', () => ({ printToFileAsync: jest.fn().mockResolvedValue({ uri: 'file:///plan.pdf' }) })); +jest.mock('expo-sharing', () => ({ isAvailableAsync: jest.fn(), shareAsync: jest.fn() })); +jest.mock('../lib/gpxStorage', () => ({ isRemoteGpxPath: (path: string) => !path.startsWith('file:'), downloadGpx: jest.fn() })); +const plan = { + event: { name: 'Mountain 100 ', event_date: '2026-09-05', event_time: '05:00', total_distance: 161, + distance_unit: 'kilometers', elevation_unit: 'meters', total_elevation_gain: 8000, gpx_file_url: 'file:///course.gpx' }, + checkpoints: [], crew: [], + gear: [{ name: 'Headlamp', category: 'safety', quantity: 2, notes: 'Spare batteries at km 80 ' }], + dropBags: [{ name: 'Summit bag', checkpointName: 'Summit aid', items: [] }], +} as unknown as RacePlanData; + +it('keeps essential packing notes and named empty bags in the exported plan', () => { + const html = buildRacePlanHtml(plan); + expect(html).toContain('Spare batteries at km 80 <mandatory>'); + expect(html).toContain('Summit bag'); expect(html).toContain('No items listed.'); + expect(html).toContain('161 km'); expect(html).toContain('8,000 m'); + expect(html).toContain('September 5, 2026'); + expect(html).toContain('Mountain 100 <Trail>'); +}); +it('labels omitted saved routes in the shared document, including malformed routes', () => { + expect(buildRacePlanHtml(plan)).toContain('Course Route Unavailable'); + expect(buildRacePlanHtml({ ...plan, gpxXml: '' })).toContain('Course Route Unavailable'); +}); +it('treats malformed and one-point cached GPX as unavailable so export can ask first', async () => { + mockXml = ''; + expect(await loadGpxXmlForEvent('race', 'file:///course.gpx')).toBeNull(); + mockXml = ''; + expect(await loadGpxXmlForEvent('race', 'file:///course.gpx')).toBeNull(); +}); +it('surfaces native print failure and does not share an incomplete document', async () => { + (Print.printToFileAsync as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await expect(exportRacePlan(plan)).rejects.toThrow('Disk full'); + expect(Sharing.shareAsync).not.toHaveBeenCalled(); +}); +it('surfaces unavailable sharing', async () => { + (Sharing.isAvailableAsync as jest.Mock).mockResolvedValue(false); + await expect(exportRacePlan(plan)).rejects.toThrow('Sharing is not available'); + expect(Sharing.shareAsync).not.toHaveBeenCalled(); +}); + +it('loads the preserved route from the new sandbox after app update', async () => { + mockXml = ''; + expect(await loadGpxXmlForEvent('race', 'file:///old-container/Documents/gpx/race.gpx')).toBe(mockXml); + expect(mockOpenedFiles).toContain('file:///new-container/Documents/gpx/race.gpx'); +}); + + it('exports race-specific vest quantities and packed status', () => { + const html = buildRacePlanHtml({ ...plan, gear: [ + { name: 'Soft flask', category: 'hydration', quantity: 2, isCarried: true, isPacked: true }, + { name: 'Rain shell', category: 'clothing', quantity: 1, isWorn: true, isPacked: false }, + ] }); + expect(html).toContain('Soft flask ×2'); + expect(html).toContain('in vest · packed'); + expect(html).toContain('worn · not packed'); + expect(html).toContain('✓'); +}); diff --git a/src/__tests__/structuredTimeInput.test.tsx b/src/__tests__/structuredTimeInput.test.tsx new file mode 100644 index 0000000..3aa9185 --- /dev/null +++ b/src/__tests__/structuredTimeInput.test.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { Text, View, TextInput, TouchableOpacity } from 'react-native'; +const { act, create } = require('react-test-renderer'); +jest.mock('@react-native-community/datetimepicker',()=> 'NativeDateTimePicker'); +jest.mock('../theme',()=>({useTheme:()=>({theme:{colors:{}}})})); +jest.mock('../components/ui',()=>{ + const {Text,TouchableOpacity}=require('react-native');const React=require('react'); + return {BodySmall:Text,Button:({children,...props}:any)=>React.createElement(TouchableOpacity,props,React.createElement(Text,null,children))}; +}); +import { DurationInput, DateTimeInput } from '../components/StructuredTimeInput'; +function DurationHarness({clock=false}:{clock?:boolean}){const [value,set]=useState('');return {value};} +beforeEach(()=>{(globalThis as any).IS_REACT_ACT_ENVIRONMENT=true;}); +test('accepts multi-day duration using numeric fields and flags impossible minutes and clock hours',async()=>{ + let tree:any;await act(async()=>{tree=create();}); + await act(async()=>{tree.root.findByProps({accessibilityLabel:'Time hours'}).props.onChangeText('30hours');}); + await act(async()=>{tree.root.findByProps({accessibilityLabel:'Time minutes'}).props.onChangeText('15');}); + expect(tree.root.findByProps({testID:'value'}).props.children).toBe('30:15'); + expect(tree.root.findAllByProps({accessibilityRole:'alert'})).toHaveLength(0); + await act(async()=>{tree.root.findByProps({accessibilityLabel:'Time minutes'}).props.onChangeText('99');}); + expect(tree.root.findAllByProps({accessibilityRole:'alert'}).length).toBeGreaterThan(0); + await act(async()=>{tree.update();}); + await act(async()=>{tree.root.findByProps({accessibilityLabel:'Time minutes'}).props.onChangeText('15');}); + expect(tree.root.findAllByProps({accessibilityRole:'alert'}).length).toBeGreaterThan(0); + await act(async()=>{tree.unmount();}); +}); +test('actual time uses native date and time pickers instead of a prose field',async()=>{ + let tree:any;const onChange=jest.fn();await act(async()=>{tree=create();}); + expect(tree.root.findAllByType(TextInput)).toHaveLength(0); + const select=tree.root.findAllByType(TouchableOpacity).find((n:any)=>n.findAllByType(Text).some((t:any)=>t.props.children==='Select date')); + await act(async()=>{select.props.onPress();}); + const picker=tree.root.findByType('NativeDateTimePicker'); + expect(picker.props.value.getDate()).toBe(14); + await act(async()=>{picker.props.onChange({type:'set'},new Date(2027,7,15,2,15));}); + expect(onChange).toHaveBeenCalledWith('2027-08-15T02:15'); + await act(async()=>{tree.unmount();}); +}); diff --git a/src/components/BrandSelect.tsx b/src/components/BrandSelect.tsx new file mode 100644 index 0000000..7e298a9 --- /dev/null +++ b/src/components/BrandSelect.tsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import { View, Modal, ScrollView, TextInput } from 'react-native'; +import { Button, BodySmall, H2 } from './ui'; +import { useTheme } from '../theme'; +import { useGear } from '../context/GearContext'; +export const GEAR_BRANDS = [ + 'Altra', + 'Arc’teryx', + 'ASICS', + 'Black Diamond', + 'Brooks', + 'CamelBak', + 'COROS', + 'Garmin', + 'HOKA', + 'HydraPak', + 'Injinji', + 'La Sportiva', + 'LEKI', + 'NNormal', + 'Nathan', + 'New Balance', + 'Nike', + 'Osprey', + 'Patagonia', + 'Petzl', + 'Salomon', + 'Saucony', + 'Silva', + 'Suunto', + 'The North Face', + 'Topo Athletic', + 'Ultimate Direction', +]; +export default function BrandSelect({ + value, + onChange, +}: { + value: string; + onChange: (value: string) => void; +}) { + const { + theme: { colors }, + } = useTheme(); + const { gearItems } = useGear(); + const [open, setOpen] = useState(false); + const [other, setOther] = useState(false); + const [query, setQuery] = useState(''); + const brands = [ + ...new Set([ + ...GEAR_BRANDS, + ...gearItems.map((g) => g.brand?.trim()).filter((b): b is string => !!b), + ]), + ].sort((a, b) => a.localeCompare(b)); + return ( + + + {(other || (!!value && !brands.includes(value))) && ( + + )} + setOpen(false)} + > + +

Select brand

+ + + + {brands + .filter((b) => b.toLowerCase().includes(query.toLowerCase())) + .map((b) => ( + + ))} + + + + Custom brands from your saved gear appear here next time. + + +
+
+
+ ); +} diff --git a/src/components/ExportRacePlanButton.tsx b/src/components/ExportRacePlanButton.tsx index 70dd87d..6713a01 100644 --- a/src/components/ExportRacePlanButton.tsx +++ b/src/components/ExportRacePlanButton.tsx @@ -1,3 +1,4 @@ +import { loadOperations } from '../lib/raceOperations'; /** * ExportRacePlanButton — generates a shareable race day plan PDF. * @@ -18,6 +19,10 @@ import { Button } from './ui'; import { useEvents } from '../context/EventContext'; import { useCheckpoints } from '../context/CheckpointContext'; import { useCrewMembers, ROLE_CONFIG } from '../context/CrewContext'; +import { + EVENT_CREW_KEY, + EventCrewAssignment, +} from '../lib/eventCrew'; import { useGear } from '../context/GearContext'; import { useDropBags } from '../context/DropBagContext'; import { @@ -28,24 +33,7 @@ import { RacePlanGearItem, } from '../lib/racePlanPdf'; -// Per-event relationship records (same storage as EventDetailScreen). -const EVENT_GEAR_KEY = '@ultraedge/event-gear'; -const EVENT_CREW_KEY = '@ultraedge/event-crew'; - -interface EventGearAllocation { - eventId: string; - gearItemId: string; - isWorn: boolean; - isCarried: boolean; - quantity: number; - notes?: string; -} - -interface EventCrewAssignment { - eventId: string; - crewMemberId: string; - notes?: string; -} +import { EVENT_GEAR_KEY, EventGearAllocation } from '../lib/eventGear'; interface ExportRacePlanButtonProps { eventId: string; @@ -54,29 +42,38 @@ interface ExportRacePlanButtonProps { } async function readJson(key: string): Promise { - try { - const raw = await AsyncStorage.getItem(key); - return raw ? (JSON.parse(raw) as T[]) : []; - } catch { - return []; - } + const raw = await AsyncStorage.getItem(key); + if (!raw) return []; + const parsed: unknown = JSON.parse(raw); + if (!Array.isArray(parsed)) throw new Error('Saved plan relationships could not be read.'); + return parsed as T[]; } export function ExportRacePlanButton({ eventId, fullWidth = true, style }: ExportRacePlanButtonProps) { const { theme } = useTheme(); const { colors } = theme; - const { getEvent } = useEvents(); - const { getCheckpointsByEventId, getCheckpointById } = useCheckpoints(); - const { getCrewMember } = useCrewMembers(); - const { getGearItem } = useGear(); - const { getDropBagsByEvent } = useDropBags(); + const events = useEvents(); + const { getEvent } = events; + const checkpointsState = useCheckpoints(); + const { getCheckpointsByEventId, getCheckpointById } = checkpointsState; + const crewState = useCrewMembers(); + const { getCrewMember } = crewState; + const gearState = useGear(); + const { getGearItem } = gearState; + const bagsState = useDropBags(); + const { getDropBagsByEvent } = bagsState; const [generating, setGenerating] = useState(false); const handleExport = async () => { if (generating) {return;} + if ([events, checkpointsState, crewState, gearState, bagsState].some(state => state.loading || state.error)) { + Alert.alert('Plan Not Ready', 'All saved planning data must load successfully before exporting. Please reopen the plan and try again.'); + return; + } + const event = getEvent(eventId); if (!event) { Alert.alert('Export Failed', 'This event could not be found.'); @@ -94,11 +91,18 @@ export function ExportRacePlanButton({ eventId, fullWidth = true, style }: Expor const crew: RacePlanCrewMember[] = []; for (const assignment of crewAssignments) { const member = getCrewMember(assignment.crewMemberId); - if (!member) {continue;} + if (!member) {throw new Error('An assigned crew member is missing. Review crew assignments before exporting.');} + const roles = assignment.roles ?? []; const roleLabel = - member.role === 'other' && member.customRole - ? member.customRole - : ROLE_CONFIG[member.role]?.label ?? 'Crew'; + roles.length > 0 + ? roles + .map(role => + role === 'other' && assignment.customRole + ? assignment.customRole + : ROLE_CONFIG[role]?.label ?? 'Crew', + ) + .join(' / ') + : 'Crew'; crew.push({ name: member.name, role: roleLabel, @@ -115,7 +119,7 @@ export function ExportRacePlanButton({ eventId, fullWidth = true, style }: Expor const gear: RacePlanGearItem[] = []; for (const allocation of gearAllocations) { const item = getGearItem(allocation.gearItemId); - if (!item) {continue;} + if (!item) {throw new Error('An allocated gear item is missing. Review gear assignments before exporting.');} gear.push({ name: item.name, brand: item.brand ?? null, @@ -123,6 +127,7 @@ export function ExportRacePlanButton({ eventId, fullWidth = true, style }: Expor quantity: allocation.quantity || 1, isWorn: allocation.isWorn, isCarried: allocation.isCarried, + isPacked: allocation.isPacked, notes: allocation.notes ?? item.notes ?? null, }); } @@ -131,7 +136,7 @@ export function ExportRacePlanButton({ eventId, fullWidth = true, style }: Expor const dropBags: RacePlanDropBag[] = getDropBagsByEvent(eventId).map(bag => ({ name: bag.name, checkpointName: bag.checkpointId - ? getCheckpointById(eventId, bag.checkpointId)?.name ?? null + ? getCheckpointById(eventId, bag.checkpointId)?.name ?? 'Checkpoint unavailable — confirm bag location' : null, items: bag.items.map(item => ({ name: item.name, @@ -144,7 +149,23 @@ export function ExportRacePlanButton({ eventId, fullWidth = true, style }: Expor // Course GPX (best-effort; the PDF omits the route section when null). const gpxXml = await loadGpxXmlForEvent(eventId, event.gpx_file_url); - await exportRacePlan({ event, checkpoints, crew, gear, dropBags, gpxXml }); + if (event.gpx_file_url && !gpxXml) { + const proceed = await new Promise(resolve => Alert.alert( + 'Route Unavailable', + 'The course route could not be loaded. Export the plan without its route map?', + [{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) }, + { text: 'Export Without Route', onPress: () => resolve(true) }], + { cancelable: true, onDismiss: () => resolve(false) }, + )); + if (!proceed) return; + } + const operations = await loadOperations(eventId); + const operationsLogistics = operations.duties.map(duty => `${getCheckpointById(eventId, duty.checkpointId)?.name ?? 'Checkpoint'}: ${getCrewMember(duty.crewMemberId)?.name ?? 'Crew'} — ${duty.role.replace('_', ' ')}`); + for (const vehicle of operations.vehicles) { + operationsLogistics.push(`${vehicle.name} • Owner: ${vehicle.ownerId ? getCrewMember(vehicle.ownerId)?.name ?? 'Unassigned' : 'Unassigned'} • Crew: ${vehicle.crewIds.map(id => getCrewMember(id)?.name ?? 'Unassigned').join(', ') || 'None'} • Cargo: ${operations.cargo.filter(item => item.vehicleId === vehicle.id).map(item => item.label).join(', ') || 'None'}`); + } + operationsLogistics.push('Bag items travel with their bag unless a separate item allocation is listed.'); + await exportRacePlan({ event, checkpoints, crew, gear, dropBags, gpxXml, operations, operationsLogistics }); } catch (err) { const message = err instanceof Error ? err.message : 'Something went wrong.'; Alert.alert('Export Failed', `Could not generate the race plan PDF. ${message}`); diff --git a/src/components/LiveRacePanel.tsx b/src/components/LiveRacePanel.tsx new file mode 100644 index 0000000..d65c35c --- /dev/null +++ b/src/components/LiveRacePanel.tsx @@ -0,0 +1,57 @@ +import { DateTimeInput } from './StructuredTimeInput'; +import React,{useEffect,useState,useRef} from 'react'; +import {View,Modal,ScrollView,TextInput,Alert,Share,AppState,Keyboard} from 'react-native'; +import {Body,BodySmall,H2,H3,Button} from './ui'; +import {useTheme} from '../theme'; +import {LiveRoom,LiveSnapshot,liveProjection} from '../lib/liveRaceModel'; +import {readLiveCache,createLiveRoom,joinLiveRoom,syncLiveRoom,queueLiveReport,publishLiveRoom,removeLiveAccess,discardPendingReports,forgetLiveRoom,recoverLiveRooms,deleteLiveIdentity,LiveCache} from '../lib/liveRaceSync'; +import {clockLabel,parseLocalRaceTime,projectedDistance} from '../lib/raceOperations'; +interface Props { snapshot?:()=>LiveSnapshot; } +export default function LiveRacePanel({snapshot}:Props){ + const {theme:{colors}}=useTheme();const [open,setOpen]=useState(false);const [cache,setCache]=useState({rooms:[],pending:[],actorId:null,lastSync:{}}); + const [roomId,setRoomId]=useState(null);const [name,setName]=useState('');const [code,setCode]=useState('');const [at,setAt]=useState('');const [busy,setBusy]=useState(false);const [status,setStatus]=useState('');const active=useRef(false);const polling=useRef(false); + const room=cache.rooms.find(r=>r.id===roomId);const projection=room?liveProjection(room):null; + const refreshCache=async()=>{const value=await readLiveCache();if(active.current)setCache(value);}; + const sync=async(id:string)=>{if(polling.current)return;polling.current=true;try{await syncLiveRoom(id);if(active.current)setStatus('Connected — team updates checked every 10 seconds while open.');}catch(e){if(active.current)setStatus(`Not synced: ${e instanceof Error?e.message:(e as {message?:string})?.message??'Connection unavailable. Saved reports will retry.'}`);}finally{polling.current=false;await refreshCache();}}; + useEffect(()=>{active.current=open;if(open)refreshCache();return()=>{active.current=false;};},[open]); + useEffect(()=>{if(!open||!roomId)return;sync(roomId);const timer=setInterval(()=>{if(AppState.currentState==='active')sync(roomId);},10000);const sub=AppState.addEventListener('change',state=>{if(state==='active')sync(roomId);});return()=>{clearInterval(timer);sub.remove();};},[open,roomId]); + const work=async(fn:()=>Promise)=>{if(busy)return;setBusy(true);try{await fn();await refreshCache();}catch(e){Alert.alert('Live race',e instanceof Error?e.message:(e as {message?:string})?.message??'Please try again.');}finally{setBusy(false);}}; + const field=(label:string,value:string,set:(v:string)=>void)=>{label}; + const record=async(cp:string,kind:'in'|'out',remove=false)=>{if(!room)return;await queueLiveReport(room,cp,kind,remove?null:at.trim()?parseLocalRaceTime(at):new Date().toISOString());await refreshCache();setStatus('Saved on this device; waiting to sync.');void sync(room.id);}; + const confirm=(title:string,detail:string,fn:()=>Promise)=>Alert.alert(title,detail,[{text:'Cancel',style:'cancel'},{text:'Continue',onPress:()=>work(fn)}]); + return <>setOpen(false)}>

Live team race

+ {!room?<> + Share a private race with your crew. Only joined team members can read it or record times. + Team access is saved securely on this phone. Keep the app installed to retain access; a new phone needs a new crew invitation. Race timing, crew names and vehicle/cargo labels are shared. Phone numbers, emails, personal notes and route files are excluded. + {field('Your team display name',name,setName)} + {snapshot&&} + {field('Private invitation code',code,setCode)} + + {cache.actorId&&} + {cache.rooms.map(r=>)} + :<> +

{room.snapshot.event.name}

+ {status||'Showing saved team plan.'}Last successful sync: {cache.lastSync[room.id]?clockLabel(Date.parse(cache.lastSync[room.id])):'Not yet'} • {cache.pending.filter(p=>p.roomId===room.id).length} pending reports + + You report as {room.ownerId===cache.actorId?'runner':'crew'}. Each person’s observations are kept. Crew reports are averaged first, then weighted equally with the runner. Corrections only change your own reports. + {room.ownerId===cache.actorId&&<> + + {snapshot&&} + } + Plan revision {room.revision}. Times below use this phone’s timezone; all phones calculate from the same absolute race start. Estimates use course distance, not terrain or GPS. + {projection?Projected finish {clockLabel(projection.finish)}{'\n'}Best {clockLabel(projection.bestFinish)} / Slow {clockLabel(projection.slowFinish)}:Timing reports conflict or the plan is incomplete. Check the individual observations before using ETAs.} + {projection&&Estimated now: {(['best','arrival','slow'] as const).map(s=>`${s==='arrival'?'Expected':s==='best'?'Best':'Slow'} ${projectedDistance(projection,room.snapshot.event.total_distance!,Date.now(),s).toFixed(1)} ${room.snapshot.event.distance_unit==='miles'?'mi':'km'}`).join(' • ')} — not GPS tracking} + + {room.snapshot.checkpoints.map(cp=>{const row=projection?.rows.find(r=>r.id===cp.id);return

{cp.name}

{row&&<>In {clockLabel(row.arrival)} • Out {clockLabel(row.departure)}Best {clockLabel(row.best)} / Slow {clockLabel(row.slow)} • {row.stop} min planned stop} + {(['in','out'] as const).map(kind=>)} + {room.reports.filter(r=>r.checkpointId===cp.id&&r.at).map(r=>{room.members.find(m=>m.id===r.authorId)?.name??'Former member'} {r.kind}: {clockLabel(Date.parse(r.at!))}{r.authorId===cache.actorId&&})} + {cache.pending.filter(r=>r.roomId===room.id&&r.checkpointId===cp.id).map(r=>Pending {r.kind}: {r.at?clockLabel(Date.parse(r.at)):'remove report'} — saved only on this device)} +
;})} +

Team logistics

{room.snapshot.logistics.map((line,i)=>{line})} +

Team access

{room.members.map(m=>{m.name}{m.id===room.ownerId?' • runner / owner':''}{room.ownerId===cache.actorId&&m.id!==cache.actorId&&})} + {!!cache.pending.filter(r=>r.roomId===room.id).length&&} + + + } +
; +} diff --git a/src/components/MandatoryGearPanel.tsx b/src/components/MandatoryGearPanel.tsx new file mode 100644 index 0000000..620710a --- /dev/null +++ b/src/components/MandatoryGearPanel.tsx @@ -0,0 +1,239 @@ +import React, { useState, useRef } from 'react'; +import { View, TextInput, Alert, Keyboard } from 'react-native'; +import * as Crypto from 'expo-crypto'; +import { Event } from '../lib/database.types'; +import { useEvents } from '../context/EventContext'; +import { EventGearAllocation } from '../lib/eventGear'; +import { GearItem } from '../context/GearContext'; +import { requirementStatus, GearRequirement } from '../lib/mandatoryGear'; +import { Button, H3, Body, BodySmall, Card, CardContent } from './ui'; +import { useTheme } from '../theme'; +export default function MandatoryGearPanel({ + event, + gear, + allocations, +}: { + event: Event; + gear: GearItem[]; + allocations: EventGearAllocation[]; +}) { + const { updateEvent } = useEvents(); + const { + theme: { colors }, + } = useTheme(); + const [name, setName] = useState(''); + const [quantity, setQuantity] = useState('1'); + const [busy, setBusy] = useState(false); + const lock = useRef(false); + const [expanded, setExpanded] = useState(null); + const requirements = event.mandatoryGear ?? []; + const save = async (next: GearRequirement[]) => { + if (lock.current) { + return; + } + Keyboard.dismiss(); + lock.current = true; + setBusy(true); + try { + if (!(await updateEvent(event.id, { mandatoryGear: next }))) { + throw new Error('Race no longer exists.'); + } + return true; + } catch (e) { + Alert.alert( + 'Requirements not saved', + e instanceof Error ? e.message : 'Please try again.', + ); + } finally { + lock.current = false; + setBusy(false); + } + }; + const inputStyle = { + color: colors.bark, + borderColor: colors.border, + borderWidth: 1, + padding: 12, + borderRadius: 8, + minHeight: 48, + }; + return ( + + +

Mandatory race gear

+ + Add the requirements from your event’s official guide, then match + items from this race’s packing list. Required quantities count gear + worn or carried with you. + + {requirements.map((r) => { + const status = requirementStatus( + r, + allocations, + gear.filter((g) => !g.retired && g.isActive).map((g) => g.id), + ); + return ( + + + {r.name} ×{r.quantity} + + + {status.ready + ? '✓ Packed requirement' + : `${status.missing} still to pack`}{' '} + • {status.assigned} assigned / {status.packed} packed + + + {expanded === r.id && ( + <> + Required quantity + + + {r.quantity} + + + {gear + .filter((g) => + allocations.some((a) => a.gearItemId === g.id), + ) + .map((g) => ( + + ))} + + Add missing items to this race’s gear list below. Removing + an item leaves this requirement incomplete. + + + + )} + + ); + })} + + + Required quantity + + setQuantity(v.replace(/[^0-9]/g, '').slice(0, 3)) + } + keyboardType="number-pad" + style={inputStyle} + /> + + +
+
+ ); +} diff --git a/src/components/MoverStorageNotice.tsx b/src/components/MoverStorageNotice.tsx new file mode 100644 index 0000000..fac3e21 --- /dev/null +++ b/src/components/MoverStorageNotice.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { View } from 'react-native'; +import { useMover } from '../context/MoverContext'; +import { BodySmall, Button } from './ui'; + +export default function MoverStorageNotice() { + const { error, isLoading, refreshData } = useMover(); + if (!error && !isLoading) return null; + return ( + + {isLoading ? 'Loading your profile and weight history…' : error} + {error && } + + ); +} diff --git a/src/components/PhotoField.tsx b/src/components/PhotoField.tsx new file mode 100644 index 0000000..2c62e57 --- /dev/null +++ b/src/components/PhotoField.tsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect } from 'react'; +import { View, Image, Alert } from 'react-native'; +import * as ImagePicker from 'expo-image-picker'; +import { BodySmall, Button } from './ui'; +import { attachmentUri, saveAttachment } from '../lib/attachments'; +export default function PhotoField({ + value, + onChange, + disabled = false, + onBusyChange, +}: { + value?: string | null; + onChange?: (v: string | null) => void | Promise; + disabled?: boolean; + onBusyChange?: (busy: boolean) => void; +}) { + const [busy, setBusy] = useState(false); + useEffect(() => { + onBusyChange?.(busy); + return () => onBusyChange?.(false); + }, [busy, onBusyChange]); + const pick = async () => { + if (busy) { + return; + } + setBusy(true); + try { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], + quality: 0.8, + allowsEditing: false, + exif: false, + }); + if (!result.canceled) { + const asset = result.assets[0]; + const saved = await saveAttachment( + asset.uri, + asset.fileName || 'Gear photo', + asset.mimeType || 'image/jpeg', + ); + await onChange?.(saved.path); + } + } catch (e) { + Alert.alert( + 'Photo not saved', + e instanceof Error ? e.message : 'Please try again.', + ); + } finally { + setBusy(false); + } + }; + let uri: string | undefined; + try { + uri = value + ? value.startsWith('file:') || value.startsWith('https:') + ? value + : attachmentUri(value) + : undefined; + } catch { + /* Show replacement control for an invalid old reference. */ + } + return ( + + {uri && ( + + )} + {onChange && ( + <> + + {!!value && ( + + )} + Saved on this device for offline use. + + )} + + ); +} diff --git a/src/components/RaceGuidePanel.tsx b/src/components/RaceGuidePanel.tsx new file mode 100644 index 0000000..3764e6b --- /dev/null +++ b/src/components/RaceGuidePanel.tsx @@ -0,0 +1,118 @@ +import React, { useState, useRef } from 'react'; +import { Alert, View } from 'react-native'; +import * as DocumentPicker from 'expo-document-picker'; +import * as Sharing from 'expo-sharing'; +import * as FS from 'expo-file-system/legacy'; +import { useEvents } from '../context/EventContext'; +import { Event } from '../lib/database.types'; +import { saveAttachment, attachmentUri, attachmentShareUri } from '../lib/attachments'; +import { Button, H3, BodySmall, Card, CardContent } from './ui'; +export default function RaceGuidePanel({ event }: { event: Event }) { + const { updateEvent } = useEvents(); + const [busy, setBusy] = useState(false); + const lock = useRef(false); + const work = async (fn: () => Promise) => { + if (lock.current) { + return; + } + lock.current = true; + setBusy(true); + try { + await fn(); + } catch (e) { + Alert.alert( + 'Race guide', + e instanceof Error ? e.message : 'Please try again.', + ); + } finally { + lock.current = false; + setBusy(false); + } + }; + const pick = () => + work(async () => { + const result = await DocumentPicker.getDocumentAsync({ + type: 'application/pdf', + copyToCacheDirectory: true, + multiple: false, + }); + if (result.canceled) { + return; + } + const a = result.assets[0]; + const file = await saveAttachment(a.uri, a.name, 'application/pdf'); + if (!(await updateEvent(event.id, { raceGuide: file }))) { + throw new Error('Race no longer exists.'); + } + }); + const share = () => + work(async () => { + if (!event.raceGuide) { + return; + } + const uri = attachmentUri(event.raceGuide.path); + if (!(await FS.getInfoAsync(uri)).exists) { + throw new Error('This saved PDF is missing. Attach the guide again.'); + } + if (!(await Sharing.isAvailableAsync())) { + throw new Error('File sharing is unavailable on this device.'); + } + await Sharing.shareAsync(await attachmentShareUri(event.raceGuide), { + mimeType: 'application/pdf', + UTI: 'com.adobe.pdf', + dialogTitle: event.raceGuide.name, + }); + }); + return ( + + +

Official race guide

+ + {event.raceGuide?.name || + 'Attach the organizer’s PDF to keep it with this race plan.'} + + + {event.raceGuide && ( + <> + + + Saved for offline access on this device. Use the share sheet to + open in a PDF reader or send a copy to your crew; attachments + are not synced to the live team room. + + + )} + + {event.raceGuide && ( + + )} + +
+
+ ); +} diff --git a/src/components/RaceOperationsPanel.tsx b/src/components/RaceOperationsPanel.tsx new file mode 100644 index 0000000..f352f10 --- /dev/null +++ b/src/components/RaceOperationsPanel.tsx @@ -0,0 +1,135 @@ +import { DateTimeInput, DurationInput } from './StructuredTimeInput'; +import LiveRacePanel from './LiveRacePanel'; +import { makeLiveSnapshot } from '../lib/liveRaceModel'; +import React, { useEffect, useState, useRef } from 'react'; +import { View, TextInput, Alert, Modal, ScrollView, TouchableOpacity, Keyboard } from 'react-native'; +import { useTheme } from '../theme'; +import { Body, BodySmall, H2, H3, Button, Card, CardContent } from './ui'; +import { Event, Checkpoint } from '../lib/database.types'; +import { CrewMember } from '../context/CrewContext'; +import { DropBag } from '../context/DropBagContext'; +import { RaceOperations, emptyOperations, loadOperations, changeOperations, projectRace, projectedDistance, clockLabel, putTimeReport, raceStart, parseLocalRaceTime, TimeReport, StationDuty, durationMinutes } from '../lib/raceOperations'; + +interface Props { initialStation?: string; event: Event; checkpoints: Checkpoint[]; crew: CrewMember[]; bags: DropBag[]; gear: { id: string; name: string }[]; } +export default function RaceOperationsPanel({ event, checkpoints, crew, bags, gear, initialStation }: Props) { + const { theme: { colors } } = useTheme(); + const [ops, setOps] = useState(emptyOperations(event.id)); + const [ready, setReady] = useState(false); + const [busy, setBusy] = useState(false); + const saving = useRef(false); + const [startText, setStartText] = useState(''); + const [open, setOpen] = useState(!!initialStation); + const [tab, setTab] = useState<'planning' | 'timeline' | 'logistics'>('planning'); + const [station, setStation] = useState(initialStation ?? null); + const [finishGoal, setFinishGoal] = useState(''); + const [stop, setStop] = useState('0'); + const [variation, setVariation] = useState('10'); + const [vehicleName, setVehicleName] = useState(''); + const [previewHours, setPreviewHours] = useState('6'); + const [source, setSource] = useState('runner'); + const [reportAt, setReportAt] = useState(''); + const [now, setNow] = useState(Date.now()); + useEffect(() => { const timer = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(timer); }, []); + useEffect(() => { let active = true; setReady(false); loadOperations(event.id).then(value => { if (active) { setOps(value); setVariation(String(value.variation)); if (initialStation) setStop(String(value.stops[initialStation] ?? 0)); setReady(true); } }).catch(() => Alert.alert('Race plan unavailable', 'Reopen the race to retry.')); return () => { active = false; }; }, [event.id, open]); + useEffect(() => { if (initialStation) { setStation(initialStation); setOpen(true); } }, [initialStation]); + const save = async (change: (current: RaceOperations) => RaceOperations) => { + if (!ready || saving.current) return; + Keyboard.dismiss(); + saving.current = true; + setBusy(true); + try { setOps(await changeOperations(event.id, change)); } + catch (error) { Alert.alert('Not saved', error instanceof Error ? error.message : 'Please try again.'); } + finally { saving.current = false; setBusy(false); } + }; + const projection = projectRace(event, checkpoints, ops); + const plan = projectRace(event, checkpoints, { ...ops, startAt: null, reports: [] }); + const finishGoalMinutes = durationMinutes(finishGoal); + const goalMoving = finishGoalMinutes && plan ? finishGoalMinutes - plan.stopMinutes : null; + const selected = checkpoints.find(cp => cp.id === station); + const inputStyle = { color: colors.bark, backgroundColor: colors.parchment, borderColor: colors.border, borderWidth: 1, borderRadius: 8, padding: 12, minHeight: 48 }; + const field = (label: string, value: string, onChangeText: (value: string) => void, numeric = false) => {label}; + const choice = (label: string, checked: boolean, action: () => void) => {checked ? '✓ ' : ''}{label}; + const record = (kind: TimeReport['kind']) => { + if (!selected) return; + const at = reportAt.trim() ? parseLocalRaceTime(reportAt) : new Date().toISOString(); + save(current => putTimeReport(current, { checkpointId: selected.id, source, kind, at, recordedAt: new Date().toISOString() }, checkpoints, raceStart(event, current))); + }; + const cargo = [ + ...bags.flatMap(bag => [{ key: `bag:${bag.id}`, label: `Bag: ${bag.name}` }, ...bag.items.map(item => ({ key: `item:${bag.id}:${item.id}`, label: `${bag.name} / ${item.name} ×${item.quantity}` }))]), + ...gear.map(item => ({ key: `gear:${item.id}`, label: `Gear: ${item.name}` })), + ]; + const elapsed = projection ? Math.max(0, Math.floor((now - projection.start) / 1000)) : 0; + return +

Race operations

+ {projection ? `Projected finish ${clockLabel(projection.finish)} • ${projection.stopMinutes} min at stations` : 'Set a start date/time, target HH:MM, total distance and ordered checkpoint distances to see ETAs.'} + makeLiveSnapshot(event, checkpoints, ops, [...ops.duties.map(d => `${checkpoints.find(cp=>cp.id===d.checkpointId)?.name}: ${crew.find(c=>c.id===d.crewMemberId)?.name} — ${d.role}`), ...ops.vehicles.map(v=>`${v.name} • Owner: ${crew.find(c=>c.id===v.ownerId)?.name??'Unassigned'} • Crew: ${v.crewIds.map(id=>crew.find(c=>c.id===id)?.name).join(', ')} • Cargo: ${ops.cargo.filter(c=>c.vehicleId===v.id).map(c=>c.label).join(', ')}`)])} /> + + + setOpen(false)}> + +

Race operations

+ {choice('Race planning', tab === 'planning', () => setTab('planning'))}{choice('Race day', tab === 'timeline', () => setTab('timeline'))}{choice('Crew & vehicles', tab === 'logistics', () => setTab('logistics'))} +
+ + {!ready ? Loading saved plan… : tab === 'planning' ? <> +

Plan aid-station stops

+ Choose each station and set the minutes you expect to spend there. These are planned durations, separate from race-day arrival and departure reports. All dates and times use this device’s timezone ({Intl.DateTimeFormat().resolvedOptions().timeZone}). + {plan && <>Moving: {plan.movingMinutes} min • Stops: {plan.stopMinutes} minPlanned finish: {clockLabel(plan.finish)}Average moving speed: {(event.total_distance! / (plan.movingMinutes / 60)).toFixed(2)} {event.distance_unit === 'miles' ? 'mi' : 'km'}/h. With the same moving pace, longer stops move the finish later. + + {finishGoal && (goalMoving && goalMoving > 0 ? To finish within {finishGoal}, allow {goalMoving} moving minutes and average {(event.total_distance! / (goalMoving / 60)).toFixed(2)} {event.distance_unit === 'miles' ? 'mi' : 'km'}/h while moving. This is a calculator; edit the race’s target moving time to adopt it. : Enter a valid finish duration longer than the total planned stops.)} + } + {!checkpoints.length && Add checkpoints to this race to plan station stops.} + {[...checkpoints].sort((a,b) => a.order_index - b.order_index).map(cp => { const row=plan?.rows.find(r=>r.id===cp.id);const minutes=ops.stops[cp.id] ?? durationMinutes(cp.estimated_duration) ?? 0;return

{cp.name}

{minutes} minutes planned stop{row ? ` • In ${clockLabel(row.arrival)} • Out ${clockLabel(row.departure)}` : ''}{station===cp.id&&<>{field('Planned stop (minutes)',stop,setStop,true)}{[0,2,5,10].map(m=>)}}
;})} + : tab === 'timeline' ? <> + Target time is moving time. Planned stops add to the finish. Projections use distance, not terrain. Times use this device’s timezone ({Intl.DateTimeFormat().resolvedOptions().timeZone}). + {field('Best / slow variation (%)', variation, setVariation, true)} + +

Race-day clock

+ {ops.startAt ? `${Math.floor(elapsed / 3600)}h ${Math.floor(elapsed / 60) % 60}m ${elapsed % 60}s elapsed` : 'Race not started'} + + + + Offline on this device. Record runner and crew reports separately; each gets equal weight. Recording again replaces that source’s report. + {projection ? <> +

Course progress

+ {field('Preview hours after start', previewHours, setPreviewHours, true)} + {(['best', 'arrival', 'slow'] as const).map(scenario => `${scenario === 'arrival' ? 'Expected' : scenario === 'best' ? 'Best' : 'Slow'}: ${projectedDistance(projection, event.total_distance!, projection.start + Math.max(0, Number(previewHours) || 0) * 3600000, scenario).toFixed(1)} ${event.distance_unit === 'miles' ? 'mi' : 'km'}`).join(' • ')} + {ops.startAt && Expected now: {projectedDistance(projection, event.total_distance!, now).toFixed(1)} {event.distance_unit === 'miles' ? 'mi' : 'km'} • an estimate, not GPS tracking} +

Finish projection

Best {clockLabel(projection.bestFinish)}{ '\n'}Expected {clockLabel(projection.finish)}{'\n'}Slow {clockLabel(projection.slowFinish)} + {projection.lastReport ? `Last report ${clockLabel(Date.parse(projection.lastReport))}` : 'No actual times yet — using the plan.'} + {projection.rows.filter(row => !station || station === row.id).map(row => { setStation(row.id); setStop(String(row.stop)); setReportAt(''); }} style={{ borderTopWidth: 1, borderColor: colors.border, paddingVertical: 14, gap: 6 }}> +

{row.name}

{row.distance.toFixed(1)} {event.distance_unit === 'miles' ? 'mi' : 'km'} • {row.stop} min stop + In {clockLabel(row.arrival)} • Out {clockLabel(row.departure)} + Best {clockLabel(row.best)} / Slow {clockLabel(row.slow)}{row.actualIn !== null ? '\nArrival recorded' : ''}{row.actualOut !== null ? ' • Departure recorded' : ''} + {ops.duties.filter(duty => duty.checkpointId === row.id).map(duty => `${crew.find(member => member.id === duty.crewMemberId)?.name ?? 'Removed crew'}: ${duty.role.replace('_', ' ')}`).join(' • ') || 'No crew allocated'} + Record actual arrival / departure +
)} + : Complete the race timing and checkpoint distances to calculate the timeline. You can still plan stops below.} + {!projection && checkpoints.map(cp => choice(cp.name, station === cp.id, () => { setStation(cp.id); setStop(String(ops.stops[cp.id] ?? 0)); }))} + {station && } + {selected && +

{selected.name}

Record what happened here on race day. Edit planned durations in Race planning. +

Time report

{choice('Runner', source === 'runner', () => setSource('runner'))}{choice('Crew', source === 'crew', () => setSource('crew'))} + + {(['in', 'out'] as const).map(kind => )} + {ops.reports.filter(report => report.checkpointId === selected.id).map(report => {report.source} {report.kind}: {clockLabel(Date.parse(report.at))})} + {(['in', 'out'] as const).map(kind => { const reports = ops.reports.filter(row => row.checkpointId === selected.id && row.kind === kind); return reports.length === 2 && Math.abs(Date.parse(reports[0].at) - Date.parse(reports[1].at)) > 10 * 60000 ? Reports differ by more than 10 minutes. Check the individual {kind} times before relying on the average. : null; })} +
} + : <> +

Station duties

Assignments are optional. A pacer assignment marks the beginning of their pacing leg. + {checkpoints.map(cp =>

{cp.name}

{!cp.has_crew_access && Crew access is not confirmed for this checkpoint — verify with the race guide.}{crew.map(member => {member.name}{(['support', 'first_aid', 'pacer'] as StationDuty['role'][]).map(role => choice(role.replace('_', ' '), ops.duties.some(d => d.checkpointId === cp.id && d.crewMemberId === member.id && d.role === role), () => save(current => { const matches = (d: StationDuty) => d.checkpointId === cp.id && d.crewMemberId === member.id && d.role === role; return { ...current, duties: current.duties.some(matches) ? current.duties.filter(d => !matches(d)) : [...current.duties, { checkpointId: cp.id, crewMemberId: member.id, role }] }; })))})}
)} + {!crew.length && Add crew to this race first to allocate station duties or vehicle seats.} +

Vehicles

{field('Vehicle name / identifier', vehicleName, setVehicleName)} + + {ops.vehicles.map(vehicle =>

{vehicle.name}

Owner (optional) + {crew.map(member => choice(member.name, vehicle.ownerId === member.id, () => save(current => ({ ...current, vehicles: current.vehicles.map(v => v.id === vehicle.id ? { ...v, ownerId: v.ownerId === member.id ? null : member.id } : v) }))))} + Travelling crew (one vehicle per person){crew.map(member => choice(member.name, vehicle.crewIds.includes(member.id), () => save(current => ({ ...current, vehicles: current.vehicles.map(v => ({ ...v, crewIds: v.id === vehicle.id && !v.crewIds.includes(member.id) ? [...v.crewIds, member.id] : v.crewIds.filter(id => id !== member.id) })) }))))} + Cargo • items inherit their bag’s vehicle unless allocated separately.{cargo.map(item => choice(item.label, ops.cargo.some(c => c.key === item.key && c.vehicleId === vehicle.id), () => save(current => ({ ...current, cargo: current.cargo.some(c => c.key === item.key && c.vehicleId === vehicle.id) ? current.cargo.filter(c => c.key !== item.key) : [...current.cargo.filter(c => c.key !== item.key), { ...item, vehicleId: vehicle.id }] }))))} + +
)} + } +
+
+
+
; +} diff --git a/src/components/StorageLoadNotice.tsx b/src/components/StorageLoadNotice.tsx new file mode 100644 index 0000000..9b89567 --- /dev/null +++ b/src/components/StorageLoadNotice.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { View, ActivityIndicator } from 'react-native'; +import { Body, Button } from './ui'; + +export default function StorageLoadNotice({ error, loading, onRetry }: { + error: string | null; + loading: boolean; + onRetry: () => Promise; +}) { + return ( + + {loading ? <> + + Loading saved data… + : <> + {error}. Your saved data has not been replaced. Retry loading before editing. + + } + + ); +} diff --git a/src/components/StructuredTimeInput.tsx b/src/components/StructuredTimeInput.tsx new file mode 100644 index 0000000..08fadfe --- /dev/null +++ b/src/components/StructuredTimeInput.tsx @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import { View, TextInput, Platform } from 'react-native'; +import DateTimePicker from '@react-native-community/datetimepicker'; +import { BodySmall, Button } from './ui'; +import { useTheme } from '../theme'; +import { formatDateOnly } from '../lib/dateOnly'; + +/** Numeric hours and minutes keep durations >24 hours distinct from clock times. */ +export function DurationInput({ + label, + value, + onChange, + clock = false, +}: { + label: string; + value: string; + onChange: (v: string) => void; + clock?: boolean; +}) { + const { + theme: { colors }, + } = useTheme(); + const parts = value.split(':'); + const update = (index: number, text: string) => { + const next = [parts[0] || '', parts[1] || '']; + next[index] = text + .replace(/[^0-9]/g, '') + .slice(0, index === 0 && !clock ? 3 : 2); + onChange(next.every((v) => !v) ? '' : next.join(':')); + }; + const invalid = + !!value && + !new RegExp( + clock ? '^([01]?\\d|2[0-3]):[0-5]\\d$' : '^\\d{1,3}:[0-5]\\d$', + ).test(value); + return ( + + {label} + + {['Hours', 'Minutes'].map((part, i) => ( + + update(i, v)} + keyboardType="number-pad" + placeholder={i ? '00' : '0'} + placeholderTextColor={colors.stone} + style={{ + color: colors.bark, + borderColor: colors.border, + borderWidth: 1, + borderRadius: 8, + padding: 12, + minHeight: 48, + }} + /> + + {part} + {clock && !i ? ' (24-hour)' : ''} + + + ))} + + {invalid && ( + + {clock + ? 'Hours must be 0–23; minutes 00–59.' + : 'Enter hours and two-digit minutes (00–59).'} + + )} + {!!value && ( + + )} + + ); +} + +/** Explicit local date + clock pickers; serializes local time for existing validation. */ +export function DateTimeInput({ + label, + value, + onChange, + nowLabel = 'Choose date and time', +}: { + label: string; + value: string; + onChange: (v: string) => void; + nowLabel?: string; +}) { + const [mode, setMode] = useState<'date' | 'time' | null>(null); + const parsed = value ? new Date(value) : new Date(); + const valid = Number.isFinite(parsed.getTime()); + const date = valid ? parsed : new Date(); + const serialize = (d: Date) => + `${formatDateOnly(d)}T${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; + return ( + + {label} + + {value + ? valid + ? date.toLocaleString() + : `Previously saved: ${value}. Choose a valid date and time.` + : nowLabel} + + + + + + {mode && ( + <> + { + if (Platform.OS !== 'ios') { + setMode(null); + } + if (event.type !== 'dismissed' && selected) { + onChange(serialize(selected)); + } + }} + /> + {Platform.OS === 'ios' && ( + + )} + + )} + {!!value && ( + + )} + + ); +} diff --git a/src/components/gpx/GPXRouteSection.tsx b/src/components/gpx/GPXRouteSection.tsx index 4c5eab8..3dae12f 100644 --- a/src/components/gpx/GPXRouteSection.tsx +++ b/src/components/gpx/GPXRouteSection.tsx @@ -1,23 +1,22 @@ +import { resolveLocalGpxUri } from '../../lib/localGpxUri'; /** * GPX route section for the event detail screen: * upload, preview, replace, and remove a course GPX file. * - * Signed-in users get cloud sync: files upload to Supabase Storage and - * `gpx_file_url` holds the storage path, so routes follow the account across - * devices. Signed-out (or offline) imports fall back to a device-local - * `file:` URI, and are opportunistically uploaded once the user is signed in. + * V1 imports remain on this device. Legacy remote references can still be + * read, but importing never uploads private course data automatically. */ import React, { useEffect, useState } from 'react'; -import { View, StyleSheet, Alert, Dimensions } from 'react-native'; +import { View, StyleSheet, Alert, useWindowDimensions } from 'react-native'; import * as DocumentPicker from 'expo-document-picker'; import { Directory, File, Paths } from 'expo-file-system'; import { useTheme } from '../../theme'; -import { useAuth } from '../../context/AuthContext'; import { H2, BodySmall, Button, Card, CardContent } from '../ui'; -import { isRemoteGpxPath, uploadGpx, downloadGpx, removeGpx } from '../../lib/gpxStorage'; -import { GpxRouteStats, parseGpx, computeRouteMetrics } from '../../lib/gpx'; +import { isRemoteGpxPath, downloadGpx } from '../../lib/gpxStorage'; +import { GpxRouteStats, computeRouteMetrics, parseGpx, parseGpxCheckpoints } from '../../lib/gpx'; +import { importLocalGpx } from '../../lib/importGpx'; import GPXViewer from './GPXViewer'; interface GPXRouteSectionProps { @@ -44,8 +43,9 @@ const cachedGpxFile = (eventId: string) => new File(gpxDir(), `${eventId}.gpx`); export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GPXRouteSectionProps) { const { theme } = useTheme(); const { spacing } = theme; - const { user } = useAuth(); + const { width } = useWindowDimensions(); const [busy, setBusy] = useState(false); + const [contentWidth, setContentWidth] = useState(null); const [resolvedUri, setResolvedUri] = useState(null); const [downloading, setDownloading] = useState(false); const [downloadFailed, setDownloadFailed] = useState(false); @@ -54,6 +54,8 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP useEffect(() => { let cancelled = false; setDownloadFailed(false); + setDownloading(false); + setResolvedUri(null); const resolve = async () => { if (!gpxFileUrl) { @@ -62,7 +64,7 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP } if (!isRemoteGpxPath(gpxFileUrl)) { - const local = new File(gpxFileUrl); + const local = new File(resolveLocalGpxUri(gpxFileUrl, Paths.document.uri)); const exists = (() => { try { return local.exists; @@ -70,18 +72,8 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP return false; } })(); - setResolvedUri(exists ? gpxFileUrl : null); + setResolvedUri(exists ? local.uri : null); - // Legacy device-local route + signed-in user: promote it to cloud - // storage so it syncs. Failures are silent; the local copy still works. - if (exists && user) { - try { - const path = await uploadGpx(user.id, eventId, local); - if (!cancelled) await onGpxChange(path); - } catch { - // offline or storage unavailable — retry next time - } - } return; } @@ -105,73 +97,58 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP } }; - resolve(); + resolve().catch(() => { + if (!cancelled) { + setResolvedUri(null); + setDownloadFailed(true); + setDownloading(false); + } + }); return () => { cancelled = true; }; // onGpxChange is an inline prop; re-running on its identity would loop the effect. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [gpxFileUrl, eventId, user?.id, retryToken]); + }, [gpxFileUrl, eventId, retryToken]); const handlePick = async () => { try { const result = await DocumentPicker.getDocumentAsync({ - // GPX mime types are inconsistent across providers; validate extension below + // Providers may label GPX as XML; importLocalGpx validates the file contents. type: '*/*', copyToCacheDirectory: true, }); if (result.canceled) return; const asset = result.assets[0]; - if (!asset.name.toLowerCase().endsWith('.gpx')) { - Alert.alert('Invalid File', 'Please select a GPX file (.gpx extension).'); - return; - } setBusy(true); - const dest = cachedGpxFile(eventId); - if (dest.exists) dest.delete(); - new File(asset.uri).copy(dest); - setResolvedUri(dest.uri); + const uri = await importLocalGpx(asset.uri, eventId, onGpxChange); + setResolvedUri(uri); + // Prior files remain recoverable until explicit local-data cleanup. - // Derive course stats from the imported route so the event's - // distance/elevation fields can be auto-populated. - let stats: GpxRouteStats | undefined; - try { - const metrics = computeRouteMetrics(parseGpx(await dest.text())); - if (metrics) { - stats = { - totalDistanceMi: metrics.totalDistanceMi, - elevationGainFt: metrics.elevationGainFt, - elevationLossFt: metrics.elevationLossFt, - }; - } - } catch { - // Unparseable file — the viewer will surface the error; skip stats. - } - - if (user) { - try { - const path = await uploadGpx(user.id, eventId, dest); - await onGpxChange(path, stats); - return; - } catch { - Alert.alert( - 'Saved On This Device', - 'The route was imported but could not be uploaded to your account yet. It will sync automatically later.' - ); - } - } - await onGpxChange(dest.uri, stats); } catch (e) { - Alert.alert('Error', 'Failed to import the GPX file. Please try again.'); + Alert.alert('Import Failed', e instanceof Error ? e.message : 'Failed to import the GPX file. Your previous route is unchanged.'); } finally { setBusy(false); } }; + const handleReadCheckpoints = async () => { + if (!resolvedUri || busy) return; + setBusy(true); + try { + const xml = await new File(resolvedUri).text(); + const metrics = computeRouteMetrics(parseGpx(xml)); + if (!metrics) throw new Error('This GPX needs at least two valid course points.'); + await onGpxChange(resolvedUri, { ...metrics, checkpoints: parseGpxCheckpoints(xml, metrics) }); + } catch (error) { + Alert.alert('Import Failed', error instanceof Error ? error.message : 'Unable to read checkpoints.'); + } finally { setBusy(false); } + }; + const handleRemove = () => { - Alert.alert('Remove Route?', 'The GPX file will be removed from this event on all devices.', [ + Alert.alert('Remove Route?', 'Remove the route from this plan on this device?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Remove', @@ -179,22 +156,9 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP onPress: async () => { setBusy(true); try { - if (isRemoteGpxPath(gpxFileUrl)) { - try { - await removeGpx(gpxFileUrl); - } catch { - // storage cleanup is best-effort; the event reference is the - // source of truth and is cleared below - } - } - const cached = cachedGpxFile(eventId); - if (cached.exists) cached.delete(); - if (gpxFileUrl && !isRemoteGpxPath(gpxFileUrl)) { - const legacy = new File(gpxFileUrl); - if (legacy.exists) legacy.delete(); - } - setResolvedUri(null); + // Clear the durable reference first. Existing files remain recoverable. await onGpxChange(null); + setResolvedUri(null); } catch (e) { Alert.alert('Error', 'Failed to remove the route.'); } finally { @@ -211,8 +175,11 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP <> + ); @@ -279,7 +246,7 @@ export default function GPXRouteSection({ eventId, gpxFileUrl, onGpxChange }: GP return ( -

Route

+

The course

{resolvedUri && ( ); - } + }, ); - UpgradeBottomSheet.displayName = 'UpgradeBottomSheet'; const styles = StyleSheet.create({ - content: { - paddingHorizontal: 20, - paddingTop: 8, - }, - closeButton: { - position: 'absolute', - top: 0, - right: 20, - width: 32, - height: 32, - alignItems: 'center', - justifyContent: 'center', - zIndex: 10, - }, - header: { - alignItems: 'center', - marginBottom: 24, - }, - iconCircle: { - width: 64, - height: 64, - borderRadius: 32, - alignItems: 'center', - justifyContent: 'center', - marginBottom: 16, - }, - headline: { - fontWeight: '700', - textAlign: 'center', - marginBottom: 8, - }, - subhead: { - textAlign: 'center', - maxWidth: 300, - lineHeight: 22, - }, - featureTag: { - flexDirection: 'row', - alignItems: 'center', - gap: 6, - paddingHorizontal: 12, - paddingVertical: 6, - marginTop: 12, - }, - featureTagText: { - fontWeight: '500', - }, - featuresSection: { - marginBottom: 24, - gap: 12, - }, - featureRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 12, - }, - featureIcon: { - width: 32, - height: 32, - borderRadius: 16, - alignItems: 'center', - justifyContent: 'center', - }, - featureText: { - flex: 1, - }, - errorContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 10, - padding: 12, - marginBottom: 16, - }, - errorText: { - flex: 1, - }, - pricingSection: { - gap: 12, - marginBottom: 24, - }, - pricingCard: { - // Additional styling if needed - }, - footer: { - alignItems: 'center', - gap: 12, - }, - legalText: { - textAlign: 'center', - lineHeight: 16, - paddingHorizontal: 8, - }, - legalLinks: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - legalLink: { - textDecorationLine: 'underline', - }, + content: { padding: 24 }, + paragraph: { marginTop: 20 }, + button: { marginTop: 24, height: undefined, minHeight: 48, paddingVertical: 14 }, }); export default UpgradeBottomSheet; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index b515ffe..838c882 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -20,6 +20,8 @@ type ButtonSize = 'sm' | 'md' | 'lg'; interface ButtonProps { children: React.ReactNode; + accessibilityLabel?: string; + testID?: string; variant?: ButtonVariant; size?: ButtonSize; onPress: () => void; @@ -33,6 +35,8 @@ interface ButtonProps { export function Button({ children, + accessibilityLabel, + testID, variant = 'primary', size = 'md', onPress, @@ -54,7 +58,7 @@ export function Button({ // Size configurations const sizeConfig = { - sm: { height: 36, paddingHorizontal: 16, fontSize: 14 }, + sm: { height: 44, paddingHorizontal: 16, fontSize: 14 }, md: { height: 48, paddingHorizontal: 24, fontSize: 16 }, lg: { height: 56, paddingHorizontal: 32, fontSize: 18 }, }; @@ -67,11 +71,11 @@ export function Button({ case 'primary': return { container: { - backgroundColor: isDisabled ? colors.mist : colors.forest, + backgroundColor: isDisabled ? colors.birch : colors.accent, ...shadows.md, }, text: { - color: colors.snow, + color: colors.onAccent, }, }; case 'secondary': @@ -92,8 +96,7 @@ export function Button({ }, text: { color: isDisabled ? colors.mist : colors.forest, - textDecorationLine: 'underline', - textDecorationStyle: 'dotted', + }, }; case 'danger': @@ -116,15 +119,20 @@ export function Button({ return ( {children} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 4aa6b4b..1200a7b 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,39 +1,15 @@ -/** - * UltraEdge Auth Context - * Handles authentication state with Supabase - * - * Supports: - * - Email/password auth - * - Apple Sign-In (required for App Store) - * - Google Sign-In - * - * Auth is OPTIONAL — the app works fully without signing in. - * Auth is only required for premium features (cloud sync, crew, etc.) - */ - -import React, { - createContext, - useContext, - useEffect, - useState, - useCallback, - ReactNode, -} from 'react'; +/** Optional account services. Race plans remain local and do not sync on sign-in. */ +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { Platform, Alert, Linking } from 'react-native'; -import { createClient, SupabaseClient, User, Session, AuthError } from '@supabase/supabase-js'; -import { createSecureStorageAdapter } from '../lib/supabase'; +import { SupabaseClient, User, Session } from '@supabase/supabase-js'; +import { supabase as sharedClient, isAuthAvailable } from '../lib/supabase'; +import { AUTH_CALLBACK_URL, PASSWORD_RESET_URL, parseAuthCallback, recoveryTokens } from '../lib/authLinks'; import * as AppleAuthentication from 'expo-apple-authentication'; -import * as Google from 'expo-auth-session/providers/google'; import * as WebBrowser from 'expo-web-browser'; import * as Crypto from 'expo-crypto'; -import { makeRedirectUri } from 'expo-auth-session'; - -// Complete web browser auth session for Google Sign-In -WebBrowser.maybeCompleteAuthSession(); -// ============================================================================ -// TYPES -// ============================================================================ +if (isAuthAvailable) WebBrowser.maybeCompleteAuthSession(); +const unavailable = 'Account services are unavailable in this build. Your local planner still works.'; export interface AuthState { user: User | null; @@ -41,651 +17,172 @@ export interface AuthState { isLoading: boolean; isAuthenticated: boolean; } - +export interface AuthResult { + success: boolean; + error?: string; + user?: User; + needsEmailConfirmation?: boolean; +} export interface AuthContextType extends AuthState { - // Email auth + isAuthAvailable: boolean; + isPasswordRecovery: boolean; signInWithEmail: (email: string, password: string) => Promise; signUpWithEmail: (email: string, password: string, displayName?: string) => Promise; resetPassword: (email: string) => Promise; - - // Social auth + updatePassword: (password: string) => Promise; + cancelPasswordRecovery: () => Promise; signInWithApple: () => Promise; signInWithGoogle: () => Promise; - - // Session management signOut: () => Promise; refreshSession: () => Promise; - - // Utilities supabase: SupabaseClient | null; } - -export interface AuthResult { - success: boolean; - error?: string; - user?: User; - needsEmailConfirmation?: boolean; -} - -// ============================================================================ -// CONFIGURATION -// ============================================================================ - -// Supabase credentials from environment variables. Expo SDK 52 loads .env -// files automatically and inlines EXPO_PUBLIC_* vars at build time. -// No hardcoded fallbacks — fail loudly if misconfigured. -const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL; -const SUPABASE_ANON_KEY = - process.env.EXPO_PUBLIC_SUPABASE_KEY || process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY; - -if (!SUPABASE_URL || !SUPABASE_ANON_KEY) { - throw new Error( - 'Missing Supabase configuration. Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_KEY in your .env file (see .env.example), then restart the Expo dev server.' - ); -} - -// Google OAuth config (configure in Supabase dashboard) -const GOOGLE_WEB_CLIENT_ID = process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID; -const GOOGLE_IOS_CLIENT_ID = process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID; -const GOOGLE_ANDROID_CLIENT_ID = process.env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID; - -// ============================================================================ -// DEEP LINK / TOKEN VALIDATION -// ============================================================================ - -/** - * Validate that a string is shaped like a JWT (three base64url segments) - * before it is passed to setSession. Never trust raw deep-link params. - */ -function isValidJwt(token: string | null | undefined): token is string { - return ( - typeof token === 'string' && - token.length >= 20 && - token.length <= 4096 && - /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token) - ); -} - -/** - * Validate an opaque refresh token (not a JWT): bounded length, safe charset. - */ -function isValidRefreshToken(token: string | null | undefined): token is string { - return ( - typeof token === 'string' && - token.length >= 8 && - token.length <= 512 && - /^[A-Za-z0-9._-]+$/.test(token) - ); -} - -/** - * Parse auth params from a deep-link URL. Supabase returns tokens in the - * hash fragment; some flows use the query string. Returns null for - * unparseable URLs. - */ -function parseAuthParamsFromUrl(url: string): URLSearchParams | null { - try { - const parsed = new URL(url); - const fragment = parsed.hash.startsWith('#') ? parsed.hash.slice(1) : parsed.hash; - const params = new URLSearchParams(fragment); - // Merge query-string params without letting them override fragment params - parsed.searchParams.forEach((value, key) => { - if (!params.has(key)) { - params.set(key, value); - } - }); - return params; - } catch { - return null; - } -} - -// ============================================================================ -// CONTEXT -// ============================================================================ - const AuthContext = createContext(undefined); +const anonymous = { user: null, session: null, isLoading: false, isAuthenticated: false }; -// ============================================================================ -// PROVIDER -// ============================================================================ - -interface AuthProviderProps { - children: ReactNode; -} - -export function AuthProvider({ children }: AuthProviderProps) { - const [state, setState] = useState({ - user: null, - session: null, - isLoading: true, - isAuthenticated: false, - }); - - const [supabase, setSupabase] = useState(null); - - // Google auth hook - const [googleRequest, googleResponse, googlePromptAsync] = Google.useAuthRequest({ - webClientId: GOOGLE_WEB_CLIENT_ID, - iosClientId: GOOGLE_IOS_CLIENT_ID, - androidClientId: GOOGLE_ANDROID_CLIENT_ID, - redirectUri: makeRedirectUri({ - scheme: 'ultraedge', - path: 'auth/callback', - }), - }); - - // Initialize Supabase client - useEffect(() => { - const initSupabase = async () => { - try { - const client = createClient(SUPABASE_URL!, SUPABASE_ANON_KEY!, { - auth: { - // Sessions are encrypted at rest via expo-secure-store - // (AsyncStorage fallback on web). - storage: createSecureStorageAdapter(), - autoRefreshToken: true, - persistSession: true, - detectSessionInUrl: false, - }, - }); - - setSupabase(client); +export function AuthProvider({ children }: { children: ReactNode }) { + const supabase: SupabaseClient | null = isAuthAvailable ? sharedClient : null; + const [state, setState] = useState({ ...anonymous, isLoading: isAuthAvailable }); + const [isPasswordRecovery, setPasswordRecovery] = useState(false); - // Get existing session - const { data: { session }, error } = await client.auth.getSession(); - - if (error) { - console.error('Error getting session:', error); - } - - setState({ - user: session?.user ?? null, - session: session ?? null, - isLoading: false, - isAuthenticated: !!session?.user, - }); - - // Listen for auth state changes - const { data: { subscription } } = client.auth.onAuthStateChange( - async (event, session) => { - console.log('Auth state changed:', event); - - setState(prev => ({ - ...prev, - user: session?.user ?? null, - session: session ?? null, - isAuthenticated: !!session?.user, - })); - - // Handle specific events - if (event === 'SIGNED_IN' && session?.user) { - // Ensure profile exists - await ensureProfileExists(client, session.user); - } - } - ); - - return () => { - subscription.unsubscribe(); - }; - } catch (error) { - console.error('Failed to initialize Supabase:', error); - setState(prev => ({ ...prev, isLoading: false })); - } - }; - - initSupabase(); - }, []); - - // Handle Google auth response - useEffect(() => { - if (googleResponse?.type === 'success' && supabase) { - const { id_token } = googleResponse.params; - handleGoogleIdToken(id_token); - } - }, [googleResponse, supabase]); - - // Handle password-reset deep links (ultraedge://auth/reset-password). - // Tokens arriving via deep link are attacker-reachable input: validate - // shape, type, and error params before establishing a session. useEffect(() => { if (!supabase) return; - - const handleDeepLink = async (url: string | null) => { - if (!url || !url.includes('auth/reset-password')) return; - - const params = parseAuthParamsFromUrl(url); - if (!params) { - console.warn('Ignoring malformed auth deep link'); - return; - } - - // Surface provider errors instead of silently proceeding - if (params.get('error') || params.get('error_code')) { - Alert.alert( - 'Password Reset Failed', - params.get('error_description') || 'The reset link is invalid or has expired. Please request a new one.' - ); - return; - } - - const type = params.get('type'); - const accessToken = params.get('access_token'); - const refreshToken = params.get('refresh_token'); - - // Only accept recovery-type links with well-formed tokens - if (type !== 'recovery' || !isValidJwt(accessToken) || !isValidRefreshToken(refreshToken)) { - console.warn('Ignoring auth deep link with invalid or missing recovery tokens'); - return; - } - - const { error } = await supabase.auth.setSession({ - access_token: accessToken, - refresh_token: refreshToken, - }); - - if (error) { - Alert.alert( - 'Password Reset Failed', - 'The reset link is invalid or has expired. Please request a new one.' - ); - } + let active = true; + let authEventReceived = false; + const applySession = (session: Session | null) => { + if (active) setState({ user: session?.user ?? null, session, isLoading: false, isAuthenticated: !!session?.user }); }; - - const subscription = Linking.addEventListener('url', ({ url }) => { - handleDeepLink(url); + // Subscribe synchronously and always clean up. Never await another auth call inside this callback. + const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { + authEventReceived = true; + applySession(session); + if (event === 'PASSWORD_RECOVERY') setPasswordRecovery(true); + if (event === 'SIGNED_OUT') setPasswordRecovery(false); }); + supabase.auth.getSession().then(({ data, error }) => { + if (!authEventReceived) applySession(error ? null : data.session); + }).catch(() => { if (!authEventReceived) applySession(null); }); + return () => { active = false; subscription.unsubscribe(); }; + }, [supabase]); - // Handle the case where the app was cold-started from the reset link - Linking.getInitialURL().then(handleDeepLink).catch(() => {}); - - return () => { - subscription.remove(); + useEffect(() => { + if (!supabase) return; + let active = true; + let processing = false; + const handleLink = async (url: string | null) => { + if (!url || processing) return; + const params = parseAuthCallback(url, 'reset-password'); + if (!params) return; + processing = true; + try { + if (params.get('error') || params.get('error_code')) throw new Error('Invalid recovery link'); + // PKCE codes can only be exchanged by the installation that requested recovery. + const code = params.get('code'); + const tokens = recoveryTokens(params); + if (!code && !tokens) throw new Error('Invalid recovery link'); + const { data, error } = code + ? await supabase.auth.exchangeCodeForSession(code) + : await supabase.auth.setSession(tokens!); + if (error || !data.session) throw new Error('Expired recovery link'); + if (active) setPasswordRecovery(true); + } catch { + if (active) Alert.alert('Password Reset Failed', 'The reset link is invalid or has expired. Request a new link from Sign In.'); + } finally { processing = false; } }; + const subscription = Linking.addEventListener('url', ({ url }) => { void handleLink(url); }); + Linking.getInitialURL().then(handleLink).catch(() => {}); + return () => { active = false; subscription.remove(); }; }, [supabase]); - // Ensure user profile exists in database - const ensureProfileExists = async (client: SupabaseClient, user: User) => { - try { - const { data: existingProfile, error: fetchError } = await client - .from('profiles') - .select('id') - .eq('id', user.id) - .single(); - - if (fetchError && fetchError.code !== 'PGRST116') { - // PGRST116 is "not found" which is expected for new users - console.error('Error checking profile:', fetchError); - return; - } - - if (!existingProfile) { - // Create profile for new user - const displayName = - user.user_metadata?.name || - user.user_metadata?.full_name || - user.email?.split('@')[0] || - 'Runner'; - - const { error: insertError } = await client - .from('profiles') - .insert({ - id: user.id, - name: displayName, - email: user.email, - is_premium: false, - preferences: { - distanceUnit: 'miles', - elevationUnit: 'ft', - notifications: true, - darkMode: false, - }, - created_at: new Date().toISOString(), - }); - - if (insertError) { - console.error('Error creating profile:', insertError); - } - } - } catch (error) { - console.error('Error in ensureProfileExists:', error); - } + const attempt = async (operation: () => Promise): Promise => { + if (!supabase) return { success: false, error: unavailable }; + try { return await operation(); } + catch { return { success: false, error: 'Account service could not be reached. Please try again.' }; } }; - // ============================================================================ - // EMAIL AUTH - // ============================================================================ - - const signInWithEmail = useCallback(async ( - email: string, - password: string - ): Promise => { - if (!supabase) { - return { success: false, error: 'Auth not initialized' }; - } - - try { - const { data, error } = await supabase.auth.signInWithPassword({ - email: email.trim().toLowerCase(), - password, - }); - - if (error) { - return { success: false, error: getAuthErrorMessage(error) }; - } - - return { success: true, user: data.user ?? undefined }; - } catch (error) { - console.error('Sign in error:', error); - return { success: false, error: 'An unexpected error occurred' }; - } - }, [supabase]); - - const signUpWithEmail = useCallback(async ( - email: string, - password: string, - displayName?: string - ): Promise => { - if (!supabase) { - return { success: false, error: 'Auth not initialized' }; - } - - try { - const { data, error } = await supabase.auth.signUp({ - email: email.trim().toLowerCase(), - password, - options: { - data: { - name: displayName || email.split('@')[0], - }, - }, - }); - - if (error) { - return { success: false, error: getAuthErrorMessage(error) }; - } - - // Check if email confirmation is required - if (data.user && !data.session) { - return { - success: true, - needsEmailConfirmation: true, - user: data.user, - }; - } - - return { success: true, user: data.user ?? undefined }; - } catch (error) { - console.error('Sign up error:', error); - return { success: false, error: 'An unexpected error occurred' }; - } - }, [supabase]); - - const resetPassword = useCallback(async (email: string): Promise => { - if (!supabase) { - return { success: false, error: 'Auth not initialized' }; - } - - try { - const { error } = await supabase.auth.resetPasswordForEmail( - email.trim().toLowerCase(), - { - redirectTo: 'ultraedge://auth/reset-password', - } - ); - - if (error) { - return { success: false, error: getAuthErrorMessage(error) }; - } - - return { success: true }; - } catch (error) { - console.error('Reset password error:', error); - return { success: false, error: 'An unexpected error occurred' }; - } - }, [supabase]); - - // ============================================================================ - // APPLE SIGN-IN - // ============================================================================ - - const signInWithApple = useCallback(async (): Promise => { - if (!supabase) { - return { success: false, error: 'Auth not initialized' }; - } - - // Apple Sign-In is only available on iOS - if (Platform.OS !== 'ios') { - return { success: false, error: 'Apple Sign-In is only available on iOS' }; - } - - try { - // Check if Apple authentication is available - const isAvailable = await AppleAuthentication.isAvailableAsync(); - if (!isAvailable) { - return { success: false, error: 'Apple Sign-In is not available on this device' }; - } - - // Request Apple authentication - const credential = await AppleAuthentication.signInAsync({ - requestedScopes: [ - AppleAuthentication.AppleAuthenticationScope.FULL_NAME, - AppleAuthentication.AppleAuthenticationScope.EMAIL, - ], - }); - - if (!credential.identityToken) { - return { success: false, error: 'No identity token received from Apple' }; - } - - // Sign in with Supabase using the Apple ID token - const { data, error } = await supabase.auth.signInWithIdToken({ - provider: 'apple', - token: credential.identityToken, - }); - - if (error) { - return { success: false, error: getAuthErrorMessage(error) }; - } - - // Update profile with Apple name if provided (Apple only provides name on first sign-in) - if (credential.fullName && data.user) { - const fullName = [ - credential.fullName.givenName, - credential.fullName.familyName, - ].filter(Boolean).join(' '); - - if (fullName) { - await supabase - .from('profiles') - .update({ name: fullName }) - .eq('id', data.user.id); - } - } - - return { success: true, user: data.user ?? undefined }; - } catch (error: any) { - if (error.code === 'ERR_REQUEST_CANCELED') { - return { success: false, error: 'Sign-in was cancelled' }; - } - console.error('Apple sign in error:', error); - return { success: false, error: 'Apple Sign-In failed' }; - } - }, [supabase]); - - // ============================================================================ - // GOOGLE SIGN-IN - // ============================================================================ - - const handleGoogleIdToken = async (idToken: string) => { + const signInWithEmail = (email: string, password: string) => attempt(async () => { + const { data, error } = await supabase!.auth.signInWithPassword({ email: email.trim().toLowerCase(), password }); + return error ? { success: false, error: error.message } : { success: true, user: data.user }; + }); + const signUpWithEmail = (email: string, password: string, displayName?: string) => attempt(async () => { + // Migration 002 creates user_profiles by trigger, keyed by user_id. Do not write the legacy profiles table. + const { data, error } = await supabase!.auth.signUp({ email: email.trim().toLowerCase(), password, + options: { data: { name: displayName?.trim() || email.split('@')[0] } } }); + return error ? { success: false, error: error.message } : { + success: true, user: data.user ?? undefined, needsEmailConfirmation: !!data.user && !data.session, + }; + }); + const resetPassword = (email: string) => attempt(async () => { + const { error } = await supabase!.auth.resetPasswordForEmail(email.trim().toLowerCase(), { redirectTo: PASSWORD_RESET_URL }); + return error ? { success: false, error: error.message } : { success: true }; + }); + const updatePassword = (password: string) => attempt(async () => { + if (!isPasswordRecovery || !state.session) return { success: false, error: 'Open a new password reset link first.' }; + if (password.length < 8) return { success: false, error: 'Use at least 8 characters.' }; + const { error } = await supabase!.auth.updateUser({ password }); + if (error) return { success: false, error: error.message }; + setPasswordRecovery(false); + return { success: true }; + }); + const signOut = async () => { if (!supabase) return; - - try { - const { data, error } = await supabase.auth.signInWithIdToken({ - provider: 'google', - token: idToken, - }); - - if (error) { - Alert.alert('Sign-In Error', getAuthErrorMessage(error)); - } - } catch (error) { - console.error('Google sign in error:', error); - Alert.alert('Sign-In Error', 'Google Sign-In failed'); - } + const { error } = await supabase.auth.signOut({ scope: 'local' }); + if (error) throw error; + setState(anonymous); + setPasswordRecovery(false); }; - - const signInWithGoogle = useCallback(async (): Promise => { - if (!supabase) { - return { success: false, error: 'Auth not initialized' }; - } - - if (!googleRequest) { - // Fallback: Use Supabase's OAuth flow directly - try { - const { data, error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo: makeRedirectUri({ - scheme: 'ultraedge', - path: 'auth/callback', - }), - skipBrowserRedirect: true, - }, - }); - - if (error) { - return { success: false, error: getAuthErrorMessage(error) }; - } - - if (data.url) { - const result = await WebBrowser.openAuthSessionAsync( - data.url, - makeRedirectUri({ scheme: 'ultraedge', path: 'auth/callback' }) - ); - - if (result.type === 'success' && result.url) { - // Parse and validate tokens from the callback URL (fragment or - // query). Reject anything that is not a well-formed token pair. - const params = parseAuthParamsFromUrl(result.url); - const accessToken = params?.get('access_token'); - const refreshToken = params?.get('refresh_token'); - - if (isValidJwt(accessToken) && isValidRefreshToken(refreshToken)) { - const { error: sessionError } = await supabase.auth.setSession({ - access_token: accessToken, - refresh_token: refreshToken, - }); - if (sessionError) { - return { success: false, error: getAuthErrorMessage(sessionError) }; - } - return { success: true }; - } - } - } - - return { success: false, error: 'Google Sign-In was cancelled or failed' }; - } catch (error) { - console.error('Google OAuth error:', error); - return { success: false, error: 'Google Sign-In failed' }; - } - } - - try { - const result = await googlePromptAsync(); - - if (result.type === 'success') { - // The useEffect above will handle the response - return { success: true }; - } else if (result.type === 'cancel') { - return { success: false, error: 'Sign-in was cancelled' }; - } - - return { success: false, error: 'Google Sign-In failed' }; - } catch (error) { - console.error('Google sign in error:', error); - return { success: false, error: 'Google Sign-In failed' }; - } - }, [supabase, googleRequest, googlePromptAsync]); - - // ============================================================================ - // SESSION MANAGEMENT - // ============================================================================ - - const signOut = useCallback(async (): Promise => { + const refreshSession = async () => { if (!supabase) return; - - try { - await supabase.auth.signOut(); - } catch (error) { - console.error('Sign out error:', error); + const { error } = await supabase.auth.refreshSession(); + if (error) throw error; + }; + const signInWithApple = () => attempt(async () => { + if (Platform.OS !== 'ios' || !(await AppleAuthentication.isAvailableAsync())) { + return { success: false, error: 'Apple Sign-In is unavailable on this device.' }; } - }, [supabase]); - - const refreshSession = useCallback(async (): Promise => { - if (!supabase) return; - + const nonce = Crypto.randomUUID(); + const hashedNonce = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, nonce); + let credential; try { - const { data, error } = await supabase.auth.refreshSession(); - if (error) { - console.error('Session refresh error:', error); - } + credential = await AppleAuthentication.signInAsync({ nonce: hashedNonce, requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, + ] }); } catch (error) { - console.error('Session refresh error:', error); - } - }, [supabase]); - - // ============================================================================ - // CONTEXT VALUE - // ============================================================================ - - const value: AuthContextType = { - ...state, - supabase, - signInWithEmail, - signUpWithEmail, - resetPassword, - signInWithApple, - signInWithGoogle, - signOut, - refreshSession, - }; - - return ( - - {children} - - ); + return { success: false, error: (error as { code?: string }).code === 'ERR_REQUEST_CANCELED' ? 'Sign-in was cancelled' : 'Apple Sign-In failed' }; + } + if (!credential.identityToken) return { success: false, error: 'Apple did not return an identity token.' }; + const { data, error } = await supabase!.auth.signInWithIdToken({ provider: 'apple', token: credential.identityToken, nonce }); + if (error) return { success: false, error: error.message }; + const name = [credential.fullName?.givenName, credential.fullName?.familyName].filter(Boolean).join(' '); + if (name) { + // User metadata preserves the one-time Apple name; profile creation is a server responsibility. + const { error: nameError } = await supabase!.auth.updateUser({ data: { full_name: name } }); + if (nameError) Alert.alert('Signed In', 'Your name could not be saved. You can edit your local profile.'); + } + return { success: true, user: data.user ?? undefined }; + }); + const signInWithGoogle = () => attempt(async () => { + const { data, error } = await supabase!.auth.signInWithOAuth({ provider: 'google', options: { + redirectTo: AUTH_CALLBACK_URL, skipBrowserRedirect: true, + } }); + if (error) return { success: false, error: error.message }; + if (!data.url) return { success: false, error: 'Google Sign-In could not be started.' }; + const response = await WebBrowser.openAuthSessionAsync(data.url, AUTH_CALLBACK_URL); + if (response.type !== 'success') return { success: false, error: 'Sign-in was cancelled' }; + const params = parseAuthCallback(response.url, 'callback'); + const code = params?.get('code'); + if (!code || params?.get('error')) return { success: false, error: 'Invalid Google Sign-In callback. Please try again.' }; + const { error: exchangeError } = await supabase!.auth.exchangeCodeForSession(code); + return exchangeError ? { success: false, error: exchangeError.message } : { success: true }; + }); + return {children}; } -// ============================================================================ -// HOOK -// ============================================================================ - export function useAuth(): AuthContextType { const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); - } + if (!context) throw new Error('useAuth must be used within an AuthProvider'); return context; } - -// ============================================================================ -// UTILITIES -// ============================================================================ - -function getAuthErrorMessage(error: AuthError): string { - const errorMessages: Record = { - 'Invalid login credentials': 'Invalid email or password', - 'Email not confirmed': 'Please check your email to confirm your account', - 'User already registered': 'An account with this email already exists', - 'Password should be at least 6 characters': 'Password must be at least 6 characters', - 'Email rate limit exceeded': 'Too many attempts. Please try again later', - 'Invalid email': 'Please enter a valid email address', - }; - - return errorMessages[error.message] || error.message || 'An error occurred'; -} - export default AuthProvider; diff --git a/src/context/CheckpointContext.tsx b/src/context/CheckpointContext.tsx index 8784b5b..2190a69 100644 --- a/src/context/CheckpointContext.tsx +++ b/src/context/CheckpointContext.tsx @@ -6,6 +6,7 @@ import React, { createContext, useState, useContext, useEffect, useCallback } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { runLocalPlanOperation, readCheckpointMap, subscribePlanChanges, requireLocalEvent, deleteLocalCheckpoints } from '../lib/localPlanStorage'; import { Checkpoint, CheckpointInsert, CheckpointUpdate, CheckpointType } from '../lib/database.types'; // Generate UUID (simple version for client-side) @@ -16,16 +17,18 @@ const generateId = (): string => { interface CheckpointContextType { checkpoints: Record; // eventId -> checkpoints[] loading: boolean; + error: string | null; + refreshCheckpoints: () => Promise; // CRUD operations getCheckpointsByEventId: (eventId: string) => Checkpoint[]; getCheckpointById: (eventId: string, checkpointId: string) => Checkpoint | undefined; - addCheckpoint: (eventId: string, checkpoint: Omit) => Checkpoint; - updateCheckpoint: (eventId: string, checkpointId: string, updates: CheckpointUpdate) => void; - deleteCheckpoint: (eventId: string, checkpointId: string) => void; - reorderCheckpoints: (eventId: string, checkpointIds: string[]) => void; + addCheckpoint: (eventId: string, checkpoint: Omit) => Promise; + updateCheckpoint: (eventId: string, checkpointId: string, updates: CheckpointUpdate) => Promise; + deleteCheckpoint: (eventId: string, checkpointId: string) => Promise; + reorderCheckpoints: (eventId: string, checkpointIds: string[]) => Promise; // Bulk operations - deleteAllCheckpointsForEvent: (eventId: string) => void; - duplicateCheckpoint: (eventId: string, checkpointId: string) => Checkpoint | undefined; + deleteAllCheckpointsForEvent: (eventId: string) => Promise; + duplicateCheckpoint: (eventId: string, checkpointId: string) => Promise; } const CheckpointContext = createContext(undefined); @@ -36,41 +39,43 @@ export function CheckpointProvider({ children }: { children: React.ReactNode }) const [checkpoints, setCheckpoints] = useState>({}); const [loading, setLoading] = useState(true); - // Load checkpoints from AsyncStorage on mount - useEffect(() => { - const loadCheckpoints = async () => { - try { - const stored = await AsyncStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - if (parsed && typeof parsed === 'object') { - setCheckpoints(parsed); - } - } - } catch (error) { - console.error('Failed to load checkpoints from storage:', error); - } finally { - setLoading(false); - } - }; - - loadCheckpoints(); + const [error, setError] = useState(null); + const [ready, setReady] = useState(false); + const refreshCheckpoints = useCallback(async () => { + setLoading(true); + try { + const loaded = await runLocalPlanOperation(() => readCheckpointMap()); + setCheckpoints(loaded); + setReady(true); + setError(null); + } catch { + setReady(false); + setError('Checkpoints could not be loaded. Your saved data has been preserved. Try again.'); + } finally { setLoading(false); } }, []); - - // Save checkpoints to AsyncStorage whenever they change useEffect(() => { - const saveCheckpoints = async () => { - if (!loading) { - try { - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(checkpoints)); - } catch (error) { - console.error('Failed to save checkpoints to storage:', error); - } - } - }; - - saveCheckpoints(); - }, [checkpoints, loading]); + void refreshCheckpoints(); + return subscribePlanChanges(keys => { + if (keys.includes(STORAGE_KEY)) void refreshCheckpoints(); + }); + }, [refreshCheckpoints]); + + const persist = useCallback(async (eventId: string, transform: (previous: Record) => Record) => { + if (!ready || loading) throw new Error('Load checkpoints successfully before editing.'); + try { + await runLocalPlanOperation(async () => { + await requireLocalEvent(eventId); + const previous = await readCheckpointMap(); + const next = transform(previous); + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next)); + setCheckpoints(next); + setError(null); + }); + } catch (cause) { + setError('Changes were not saved. Please try again.'); + throw cause; + } + }, [ready, loading]); // Get all checkpoints for an event, sorted by order_index const getCheckpointsByEventId = useCallback((eventId: string): Checkpoint[] => { @@ -85,20 +90,16 @@ export function CheckpointProvider({ children }: { children: React.ReactNode }) }, [checkpoints]); // Add a new checkpoint - const addCheckpoint = useCallback(( + const addCheckpoint = useCallback(async ( eventId: string, checkpointData: Omit - ): Checkpoint => { + ): Promise => { const now = new Date().toISOString(); - const eventCheckpoints = checkpoints[eventId] || []; - const maxOrder = eventCheckpoints.length > 0 - ? Math.max(...eventCheckpoints.map(cp => cp.order_index)) - : -1; const newCheckpoint: Checkpoint = { id: generateId(), event_id: eventId, - order_index: maxOrder + 1, + order_index: 0, name: checkpointData.name, checkpoint_type: checkpointData.checkpoint_type, distance_from_start: checkpointData.distance_from_start ?? null, @@ -120,22 +121,23 @@ export function CheckpointProvider({ children }: { children: React.ReactNode }) updated_at: now, }; - setCheckpoints(prev => ({ - ...prev, - [eventId]: [...(prev[eventId] || []), newCheckpoint], - })); + await persist(eventId, prev => { + newCheckpoint.order_index = Math.max(-1, ...(prev[eventId] || []).map(cp => cp.order_index)) + 1; + return { ...prev, [eventId]: [...(prev[eventId] || []), newCheckpoint] }; + }); return newCheckpoint; - }, [checkpoints]); + }, [persist]); // Update an existing checkpoint - const updateCheckpoint = useCallback(( + const updateCheckpoint = useCallback(async ( eventId: string, checkpointId: string, updates: CheckpointUpdate ) => { - setCheckpoints(prev => { + await persist(eventId, prev => { const eventCheckpoints = prev[eventId] || []; + if (!eventCheckpoints.some(cp => cp.id === checkpointId)) throw new Error('This checkpoint was deleted. Refresh before editing.'); return { ...prev, [eventId]: eventCheckpoints.map(cp => @@ -145,28 +147,19 @@ export function CheckpointProvider({ children }: { children: React.ReactNode }) ), }; }); - }, []); + }, [persist]); // Delete a checkpoint - const deleteCheckpoint = useCallback((eventId: string, checkpointId: string) => { - setCheckpoints(prev => { - const eventCheckpoints = prev[eventId] || []; - const filtered = eventCheckpoints.filter(cp => cp.id !== checkpointId); - // Reindex order_index after deletion - const reindexed = filtered - .sort((a, b) => a.order_index - b.order_index) - .map((cp, index) => ({ ...cp, order_index: index })); - return { - ...prev, - [eventId]: reindexed, - }; - }); - }, []); + const deleteCheckpoint = useCallback(async (eventId: string, checkpointId: string) => { + if (!ready || loading) throw new Error('Load checkpoints successfully before deleting.'); + await deleteLocalCheckpoints(eventId, checkpointId); + }, [ready, loading]); // Reorder checkpoints - const reorderCheckpoints = useCallback((eventId: string, checkpointIds: string[]) => { - setCheckpoints(prev => { + const reorderCheckpoints = useCallback(async (eventId: string, checkpointIds: string[]) => { + await persist(eventId, prev => { const eventCheckpoints = prev[eventId] || []; + if (new Set(checkpointIds).size !== eventCheckpoints.length || checkpointIds.length !== eventCheckpoints.length || eventCheckpoints.some(cp => !checkpointIds.includes(cp.id))) throw new Error('Checkpoint order must include every checkpoint exactly once.'); const reordered = checkpointIds.map((id, index) => { const cp = eventCheckpoints.find(c => c.id === id); return cp ? { ...cp, order_index: index, updated_at: new Date().toISOString() } : null; @@ -176,18 +169,16 @@ export function CheckpointProvider({ children }: { children: React.ReactNode }) [eventId]: reordered, }; }); - }, []); + }, [persist]); // Delete all checkpoints for an event - const deleteAllCheckpointsForEvent = useCallback((eventId: string) => { - setCheckpoints(prev => { - const { [eventId]: _, ...rest } = prev; - return rest; - }); - }, []); + const deleteAllCheckpointsForEvent = useCallback(async (eventId: string) => { + if (!ready || loading) throw new Error('Load checkpoints successfully before deleting.'); + await deleteLocalCheckpoints(eventId); + }, [ready, loading]); // Duplicate a checkpoint - const duplicateCheckpoint = useCallback((eventId: string, checkpointId: string): Checkpoint | undefined => { + const duplicateCheckpoint = useCallback(async (eventId: string, checkpointId: string): Promise => { const original = getCheckpointById(eventId, checkpointId); if (!original) return undefined; @@ -203,6 +194,8 @@ export function CheckpointProvider({ children }: { children: React.ReactNode }) value={{ checkpoints, loading, + error, + refreshCheckpoints, getCheckpointsByEventId, getCheckpointById, addCheckpoint, diff --git a/src/context/CrewContext.tsx b/src/context/CrewContext.tsx index 7c9017d..a78a89d 100644 --- a/src/context/CrewContext.tsx +++ b/src/context/CrewContext.tsx @@ -4,7 +4,11 @@ */ import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; +import { saveArrayChanges, deleteLocalCrewMember, subscribePlanChanges } from '../lib/localPlanStorage'; + +import { + loadAndMigrateCrew, +} from '../lib/eventCrew'; // Simple UUID generator for local storage const generateId = (): string => { @@ -26,8 +30,6 @@ export interface CrewMember { name: string; phone: string | null; email: string | null; - role: CrewRole; - customRole: string | null; notes: string | null; avatar_url: string | null; created_at: string; @@ -38,8 +40,6 @@ export interface CrewMemberInsert { name: string; phone?: string | null; email?: string | null; - role?: CrewRole; - customRole?: string | null; notes?: string | null; avatar_url?: string | null; } @@ -48,8 +48,6 @@ export interface CrewMemberUpdate { name?: string; phone?: string | null; email?: string | null; - role?: CrewRole; - customRole?: string | null; notes?: string | null; avatar_url?: string | null; } @@ -106,13 +104,9 @@ export function CrewProvider({ children }: CrewProviderProps) { try { setLoading(true); setError(null); - const stored = await AsyncStorage.getItem(CREW_STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored) as CrewMember[]; - // Sort by name alphabetically - parsed.sort((a, b) => a.name.localeCompare(b.name)); - setCrewMembers(parsed); - } + const loaded = await loadAndMigrateCrew() as unknown as CrewMember[]; + loaded.sort((a, b) => a.name.localeCompare(b.name)); + setCrewMembers(loaded); } catch (err) { console.error('Failed to load crew members:', err); setError('Failed to load crew members'); @@ -123,17 +117,22 @@ export function CrewProvider({ children }: CrewProviderProps) { // Save crew members to storage const saveCrewMembers = async (updatedMembers: CrewMember[]) => { + if (loading || error) throw new Error('Crew data must load successfully before saving.'); try { - await AsyncStorage.setItem(CREW_STORAGE_KEY, JSON.stringify(updatedMembers)); + const saved = await saveArrayChanges(CREW_STORAGE_KEY, crewMembers, updatedMembers); + setCrewMembers(saved); } catch (err) { console.error('Failed to save crew members:', err); - throw new Error('Failed to save crew members'); + throw err instanceof Error ? err : new Error('Failed to save crew members'); } }; // Initial load useEffect(() => { - loadCrewMembers(); + void loadCrewMembers(); + return subscribePlanChanges(keys => { + if (keys.includes(CREW_STORAGE_KEY)) void loadCrewMembers(); + }); }, [loadCrewMembers]); // Create a new crew member @@ -144,8 +143,6 @@ export function CrewProvider({ children }: CrewProviderProps) { name: memberData.name, phone: memberData.phone || null, email: memberData.email || null, - role: memberData.role || 'other', - customRole: memberData.customRole || null, notes: memberData.notes || null, avatar_url: memberData.avatar_url || null, created_at: now, @@ -156,7 +153,6 @@ export function CrewProvider({ children }: CrewProviderProps) { // Sort by name updatedMembers.sort((a, b) => a.name.localeCompare(b.name)); await saveCrewMembers(updatedMembers); - setCrewMembers(updatedMembers); return newMember; }; @@ -176,7 +172,6 @@ export function CrewProvider({ children }: CrewProviderProps) { // Re-sort by name updatedMembers.sort((a, b) => a.name.localeCompare(b.name)); await saveCrewMembers(updatedMembers); - setCrewMembers(updatedMembers); return updatedMember; }; @@ -185,9 +180,8 @@ export function CrewProvider({ children }: CrewProviderProps) { const index = crewMembers.findIndex(m => m.id === id); if (index === -1) return false; - const updatedMembers = crewMembers.filter(m => m.id !== id); - await saveCrewMembers(updatedMembers); - setCrewMembers(updatedMembers); + if (loading || error) throw new Error('Crew data must load successfully before deleting.'); + await deleteLocalCrewMember(id); return true; }; diff --git a/src/context/DropBagContext.tsx b/src/context/DropBagContext.tsx index 21994dd..b746249 100644 --- a/src/context/DropBagContext.tsx +++ b/src/context/DropBagContext.tsx @@ -5,7 +5,7 @@ */ import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; +import { saveArrayChanges, runLocalPlanOperation, readArray, subscribePlanChanges } from '../lib/localPlanStorage'; // Simple UUID generator for local storage const generateId = (): string => { @@ -26,6 +26,7 @@ export interface DropBagItem { } export interface DropBag { + imageUrl?: string | null; id: string; name: string; eventId: string; @@ -37,6 +38,7 @@ export interface DropBag { } export interface DropBagInsert { + imageUrl?: string | null; name: string; eventId: string; checkpointId?: string | null; @@ -45,13 +47,30 @@ export interface DropBagInsert { } export interface DropBagUpdate { + imageUrl?: string | null; name?: string; checkpointId?: string | null; items?: DropBagItem[]; notes?: string | null; } +export interface DropBagTemplate { + id: string; + name: string; + items: DropBagItem[]; + notes: string | null; + created_at: string; +} + +export const DROP_BAG_TEMPLATE_KEY = '@ultraedge/dropbag-templates'; + +export const copyDropBagItems = (items: DropBagItem[]): DropBagItem[] => + items.map(item => ({ ...item, id: generateId() })); + interface DropBagContextType { + templates: DropBagTemplate[]; + saveDropBagTemplate: (bagId: string) => Promise; + deleteDropBagTemplate: (id: string) => Promise; dropBags: DropBag[]; loading: boolean; error: string | null; @@ -84,6 +103,7 @@ interface DropBagProviderProps { } export function DropBagProvider({ children }: DropBagProviderProps) { + const [templates, setTemplates] = useState([]); const [dropBags, setDropBags] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -93,13 +113,13 @@ export function DropBagProvider({ children }: DropBagProviderProps) { try { setLoading(true); setError(null); - const stored = await AsyncStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored) as DropBag[]; - // Sort by created_at descending (newest first) - parsed.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); - setDropBags(parsed); - } + const [parsed, savedTemplates] = await runLocalPlanOperation(async () => Promise.all([ + readArray(STORAGE_KEY), readArray(DROP_BAG_TEMPLATE_KEY), + ])); + setTemplates(savedTemplates); + // Sort by created_at descending (newest first) + parsed.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + setDropBags(parsed); } catch (err) { console.error('Failed to load drop bags:', err); setError('Failed to load drop bags'); @@ -110,17 +130,22 @@ export function DropBagProvider({ children }: DropBagProviderProps) { // Save drop bags to storage const saveDropBags = async (updatedBags: DropBag[]) => { + if (loading || error) throw new Error('Saved data must load successfully before editing.'); try { - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedBags)); + const saved = await saveArrayChanges(STORAGE_KEY, dropBags, updatedBags); + setDropBags(saved); } catch (err) { console.error('Failed to save drop bags:', err); - throw new Error('Failed to save drop bags'); + throw err instanceof Error ? err : new Error('Failed to save drop bags'); } }; // Initial load useEffect(() => { - loadDropBags(); + void loadDropBags(); + return subscribePlanChanges(keys => { + if (keys.includes(STORAGE_KEY) || keys.includes(DROP_BAG_TEMPLATE_KEY)) void loadDropBags(); + }); }, [loadDropBags]); // Create a new drop bag @@ -131,18 +156,37 @@ export function DropBagProvider({ children }: DropBagProviderProps) { name: bagData.name, eventId: bagData.eventId, checkpointId: bagData.checkpointId || null, - items: bagData.items || [], + items: copyDropBagItems(bagData.items || []), notes: bagData.notes || null, + imageUrl: bagData.imageUrl || null, created_at: now, updated_at: now, }; const updatedBags = [newBag, ...dropBags]; await saveDropBags(updatedBags); - setDropBags(updatedBags); return newBag; }; + const saveDropBagTemplate = async (bagId: string): Promise => { + if (loading || error) throw new Error('Saved data must load successfully before editing.'); + const bag = dropBags.find(row => row.id === bagId); + if (!bag) throw new Error('Drop bag no longer exists.'); + const template: DropBagTemplate = { + id: generateId(), name: bag.name, items: copyDropBagItems(bag.items), + notes: bag.notes, created_at: new Date().toISOString(), + }; + const saved = await saveArrayChanges(DROP_BAG_TEMPLATE_KEY, templates, [template, ...templates]); + setTemplates(saved); + return template; + }; + + const deleteDropBagTemplate = async (id: string): Promise => { + if (loading || error) throw new Error('Saved data must load successfully before editing.'); + const saved = await saveArrayChanges(DROP_BAG_TEMPLATE_KEY, templates, templates.filter(row => row.id !== id)); + setTemplates(saved); + }; + // Update an existing drop bag const updateDropBag = async (id: string, updates: DropBagUpdate): Promise => { const index = dropBags.findIndex(b => b.id === id); @@ -157,7 +201,6 @@ export function DropBagProvider({ children }: DropBagProviderProps) { const updatedBags = [...dropBags]; updatedBags[index] = updatedBag; await saveDropBags(updatedBags); - setDropBags(updatedBags); return updatedBag; }; @@ -168,7 +211,6 @@ export function DropBagProvider({ children }: DropBagProviderProps) { const updatedBags = dropBags.filter(b => b.id !== id); await saveDropBags(updatedBags); - setDropBags(updatedBags); return true; }; @@ -237,6 +279,9 @@ export function DropBagProvider({ children }: DropBagProviderProps) { { - if (!a.event_date && !b.event_date) return 0; - if (!a.event_date) return 1; - if (!b.event_date) return -1; - return new Date(a.event_date).getTime() - new Date(b.event_date).getTime(); - }); - setEvents(parsed); - } + const parsed = await runLocalPlanOperation(() => readArray('@ultraedge/events')); + // Sort by event_date, nulls last + parsed.sort((a, b) => { + if (!a.event_date && !b.event_date) return 0; + if (!a.event_date) return 1; + if (!b.event_date) return -1; + return new Date(a.event_date).getTime() - new Date(b.event_date).getTime(); + }); + setEvents(parsed); } catch (err) { console.error('Failed to load events:', err); setError('Failed to load events'); @@ -85,17 +82,22 @@ export function EventProvider({ children }: EventProviderProps) { // Save events to storage const saveEvents = async (updatedEvents: Event[]) => { + if (loading || error) throw new Error('Saved data must load successfully before editing.'); try { - await AsyncStorage.setItem(EVENTS_STORAGE_KEY, JSON.stringify(updatedEvents)); + const saved = await saveArrayChanges(EVENTS_STORAGE_KEY, events, updatedEvents); + setEvents(saved); } catch (err) { console.error('Failed to save events:', err); - throw new Error('Failed to save events'); + throw err instanceof Error ? err : new Error('Failed to save events'); } }; // Initial load useEffect(() => { - loadEvents(); + void loadEvents(); + return subscribePlanChanges(keys => { + if (keys.includes('@ultraedge/events')) void loadEvents(); + }); }, [loadEvents]); // Create a new event @@ -132,7 +134,6 @@ export function EventProvider({ children }: EventProviderProps) { const updatedEvents = [...events, newEvent]; await saveEvents(updatedEvents); - setEvents(updatedEvents); return newEvent; }; @@ -150,7 +151,6 @@ export function EventProvider({ children }: EventProviderProps) { const updatedEvents = [...events]; updatedEvents[index] = updatedEvent; await saveEvents(updatedEvents); - setEvents(updatedEvents); return updatedEvent; }; @@ -159,9 +159,8 @@ export function EventProvider({ children }: EventProviderProps) { const index = events.findIndex(e => e.id === id); if (index === -1) return false; - const updatedEvents = events.filter(e => e.id !== id); - await saveEvents(updatedEvents); - setEvents(updatedEvents); + if (loading || error) throw new Error('Load events successfully before deleting.'); + await deleteLocalEvent(id); return true; }; diff --git a/src/context/GearContext.tsx b/src/context/GearContext.tsx index 6abef3d..25976cd 100644 --- a/src/context/GearContext.tsx +++ b/src/context/GearContext.tsx @@ -4,7 +4,7 @@ */ import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react'; -import AsyncStorage from '@react-native-async-storage/async-storage'; +import { readArray, runLocalPlanOperation, saveArrayChanges, subscribePlanChanges, deleteLocalGear } from '../lib/localPlanStorage'; // Types export type WeightUnit = 'g' | 'oz' | 'kg' | 'lbs'; @@ -97,11 +97,7 @@ export function GearProvider({ children }: GearProviderProps) { try { setLoading(true); setError(null); - const stored = await AsyncStorage.getItem(STORAGE_KEY); - if (stored) { - const parsed = JSON.parse(stored); - setGearItems(parsed); - } + setGearItems(await runLocalPlanOperation(() => readArray(STORAGE_KEY))); } catch (err) { console.error('Failed to load gear:', err); setError('Failed to load gear items'); @@ -112,9 +108,11 @@ export function GearProvider({ children }: GearProviderProps) { // Save gear to AsyncStorage const saveGear = async (items: GearItem[]) => { + if (loading || error) throw new Error('Reload your saved gear before editing.'); try { - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(items)); - setGearItems(items); + const saved = await saveArrayChanges(STORAGE_KEY, gearItems, items); + setGearItems(saved); + setError(null); } catch (err) { console.error('Failed to save gear:', err); setError('Failed to save gear items'); @@ -125,6 +123,7 @@ export function GearProvider({ children }: GearProviderProps) { // Load on mount useEffect(() => { loadGear(); + return subscribePlanChanges(keys => { if (keys.includes(STORAGE_KEY)) void loadGear(); }); }, []); // Add gear item @@ -167,9 +166,15 @@ export function GearProvider({ children }: GearProviderProps) { const index = gearItems.findIndex(g => g.id === id); if (index === -1) return false; - const updated = gearItems.filter(g => g.id !== id); - await saveGear(updated); - return true; + if (loading || error) throw new Error('Reload your saved gear before editing.'); + try { + await deleteLocalGear(id); + await loadGear(); + return true; + } catch (err) { + setError('Gear could not be deleted. Reload and try again.'); + throw err; + } }; // Get single gear item diff --git a/src/context/MoverContext.tsx b/src/context/MoverContext.tsx index 761a8ca..adc4c5f 100644 --- a/src/context/MoverContext.tsx +++ b/src/context/MoverContext.tsx @@ -3,8 +3,9 @@ * Local state management for Mover profile with AsyncStorage persistence */ -import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'; +import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { runLocalPlanOperation, commitPlanWrites, readArray } from '../lib/localPlanStorage'; import type { DistanceUnit, ElevationUnit, WeightUnit } from '../lib/database.types'; // ============================================================================ @@ -32,6 +33,7 @@ export interface MoverContextType { profile: MoverProfile; weightHistory: WeightEntry[]; isLoading: boolean; + error: string | null; // Profile actions updateProfile: (updates: Partial) => Promise; @@ -76,144 +78,124 @@ const DEFAULT_PROFILE: MoverProfile = { const MoverContext = createContext(undefined); +export const convertMoverWeight = (weight: number, from: WeightUnit, to: WeightUnit) => { + const grams = { lbs: 453.59237, kg: 1000, oz: 28.349523125, g: 1 }; + return weight * grams[from] / grams[to]; +}; + +async function readMoverData() { + const raw = await AsyncStorage.getItem(STORAGE_KEYS.PROFILE); + const saved = raw === null ? DEFAULT_PROFILE : JSON.parse(raw); + if (!saved || typeof saved !== 'object' || Array.isArray(saved) || + typeof saved.display_name !== 'string' || !['lbs', 'kg', 'oz', 'g'].includes(saved.weight_unit) || + !['miles', 'kilometers'].includes(saved.distance_unit) || !['feet', 'meters'].includes(saved.elevation_unit) || + (saved.current_weight !== null && (!Number.isFinite(saved.current_weight) || saved.current_weight <= 0))) { + throw new Error('Saved profile data is invalid.'); + } + const history = await readArray(STORAGE_KEYS.WEIGHT_HISTORY); + if (!history.every(entry => entry && typeof entry.id === 'string' && Number.isFinite(entry.weight) && + entry.weight > 0 && ['lbs', 'kg', 'oz', 'g'].includes(entry.weight_unit) && Number.isFinite(Date.parse(entry.recorded_at)))) { + throw new Error('Saved weight history is invalid.'); + } + return { profile: saved as MoverProfile, history: history.sort((a, b) => Date.parse(b.recorded_at) - Date.parse(a.recorded_at)) }; +} + export function MoverProvider({ children }: { children: React.ReactNode }) { const [profile, setProfile] = useState(DEFAULT_PROFILE); const [weightHistory, setWeightHistory] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const loaded = useRef(false); + + const applyData = (data: Awaited>) => { + setProfile(data.profile); + setWeightHistory(data.history); + }; - // --------------------------------------------------------------------------- - // Load data from AsyncStorage - // --------------------------------------------------------------------------- const loadData = useCallback(async () => { + setIsLoading(true); try { - setIsLoading(true); - - // Load profile - const storedProfile = await AsyncStorage.getItem(STORAGE_KEYS.PROFILE); - if (storedProfile) { - setProfile(JSON.parse(storedProfile)); - } - - // Load weight history - const storedHistory = await AsyncStorage.getItem(STORAGE_KEYS.WEIGHT_HISTORY); - if (storedHistory) { - setWeightHistory(JSON.parse(storedHistory)); - } - } catch (error) { - console.error('Error loading mover data:', error); - } finally { - setIsLoading(false); - } + await runLocalPlanOperation(async () => { + const data = await readMoverData(); + applyData(data); + loaded.current = true; + setError(null); + }); + } catch { + loaded.current = false; + setError('Profile and weight history could not be loaded. Your saved data has been preserved. Retry before editing.'); + } finally { setIsLoading(false); } }, []); - useEffect(() => { - loadData(); - }, [loadData]); + useEffect(() => { void loadData(); }, [loadData]); - // --------------------------------------------------------------------------- - // Save profile to AsyncStorage - // --------------------------------------------------------------------------- - const saveProfile = async (newProfile: MoverProfile) => { + const changeData = useCallback(async (change: (data: Awaited>) => void) => { + if (!loaded.current) throw new Error('Load your profile and weight history before editing.'); try { - await AsyncStorage.setItem(STORAGE_KEYS.PROFILE, JSON.stringify(newProfile)); - } catch (error) { - console.error('Error saving profile:', error); - throw error; + await runLocalPlanOperation(async () => { + if (!loaded.current) throw new Error('Reload your profile before editing.'); + const data = await readMoverData(); + change(data); + await commitPlanWrites([ + [STORAGE_KEYS.WEIGHT_HISTORY, JSON.stringify(data.history)], + [STORAGE_KEYS.PROFILE, JSON.stringify(data.profile)], + ]); + applyData(data); + setError(null); + }); + } catch (failure) { + // A durable journal may be awaiting recovery. Require a refresh so retrying + // an interrupted weight log cannot silently add the same entry twice. + loaded.current = false; + setError('The save could not be confirmed. Retry loading to recover any pending save before editing again.'); + throw failure; } - }; + }, []); - // --------------------------------------------------------------------------- - // Save weight history to AsyncStorage - // --------------------------------------------------------------------------- - const saveWeightHistory = async (history: WeightEntry[]) => { - try { - await AsyncStorage.setItem(STORAGE_KEYS.WEIGHT_HISTORY, JSON.stringify(history)); - } catch (error) { - console.error('Error saving weight history:', error); - throw error; + const updateProfile = useCallback((updates: Partial) => changeData(data => { + const next = { ...data.profile, ...updates }; + if (next.weight_unit !== data.profile.weight_unit && data.profile.current_weight !== null) { + next.current_weight = convertMoverWeight(data.profile.current_weight, data.profile.weight_unit, next.weight_unit); } - }; + data.profile = next; + }), [changeData]); - // --------------------------------------------------------------------------- - // Update profile - // --------------------------------------------------------------------------- - const updateProfile = useCallback(async (updates: Partial) => { - const newProfile = { ...profile, ...updates }; - setProfile(newProfile); - await saveProfile(newProfile); - }, [profile]); + const logWeight = useCallback((weight: number, notes?: string) => { + if (!Number.isFinite(weight) || weight <= 0) return Promise.reject(new Error('Enter a valid weight greater than zero.')); + return changeData(data => { + const now = new Date().toISOString(); + const entry: WeightEntry = { + id: `weight_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`, + weight, weight_unit: data.profile.weight_unit, recorded_at: now, notes, + }; + data.history = [entry, ...data.history]; + data.profile = { ...data.profile, current_weight: weight, weight_updated_at: now }; + }); + }, [changeData]); - // --------------------------------------------------------------------------- - // Log new weight entry - // --------------------------------------------------------------------------- - const logWeight = useCallback(async (weight: number, notes?: string) => { - const now = new Date().toISOString(); - - const newEntry: WeightEntry = { - id: `weight_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - weight, - weight_unit: profile.weight_unit, - recorded_at: now, - notes, + const deleteWeightEntry = useCallback((id: string) => changeData(data => { + data.history = data.history.filter(entry => entry.id !== id); + const latest = data.history[0]; + data.profile = { ...data.profile, + current_weight: latest ? convertMoverWeight(latest.weight, latest.weight_unit, data.profile.weight_unit) : null, + weight_updated_at: latest?.recorded_at ?? null, }; - - // Add to history (newest first) - const newHistory = [newEntry, ...weightHistory]; - setWeightHistory(newHistory); - await saveWeightHistory(newHistory); - - // Update current weight in profile - const updatedProfile = { - ...profile, - current_weight: weight, - weight_updated_at: now, - }; - setProfile(updatedProfile); - await saveProfile(updatedProfile); - }, [profile, weightHistory]); - - // --------------------------------------------------------------------------- - // Delete weight entry - // --------------------------------------------------------------------------- - const deleteWeightEntry = useCallback(async (id: string) => { - const newHistory = weightHistory.filter(entry => entry.id !== id); - setWeightHistory(newHistory); - await saveWeightHistory(newHistory); - - // Update current weight if we deleted the most recent entry - if (newHistory.length > 0) { - const mostRecent = newHistory[0]; - const updatedProfile = { - ...profile, - current_weight: mostRecent.weight, - weight_updated_at: mostRecent.recorded_at, - }; - setProfile(updatedProfile); - await saveProfile(updatedProfile); - } else { - const updatedProfile = { - ...profile, - current_weight: null, - weight_updated_at: null, - }; - setProfile(updatedProfile); - await saveProfile(updatedProfile); - } - }, [profile, weightHistory]); + }), [changeData]); // --------------------------------------------------------------------------- // Calculate weight trend (compare last 7 days to previous 7 days) // --------------------------------------------------------------------------- const getWeightTrend = useCallback(() => { - if (weightHistory.length < 2) return null; + const normalizedHistory = weightHistory.map(entry => ({ ...entry, weight: convertMoverWeight(entry.weight, entry.weight_unit, profile.weight_unit) })); + if (normalizedHistory.length < 2) return null; const now = new Date(); const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); // Get entries from last 7 days and previous 7 days - const recentEntries = weightHistory.filter(e => new Date(e.recorded_at) >= sevenDaysAgo); - const previousEntries = weightHistory.filter(e => { + const recentEntries = normalizedHistory.filter(e => new Date(e.recorded_at) >= sevenDaysAgo); + const previousEntries = normalizedHistory.filter(e => { const date = new Date(e.recorded_at); return date >= fourteenDaysAgo && date < sevenDaysAgo; }); @@ -221,8 +203,8 @@ export function MoverProvider({ children }: { children: React.ReactNode }) { // If we don't have entries in both periods, just compare most recent to previous if (recentEntries.length === 0 || previousEntries.length === 0) { if (weightHistory.length >= 2) { - const current = weightHistory[0].weight; - const previous = weightHistory[1].weight; + const current = normalizedHistory[0].weight; + const previous = normalizedHistory[1].weight; const change = current - previous; const percentChange = (change / previous) * 100; @@ -247,7 +229,7 @@ export function MoverProvider({ children }: { children: React.ReactNode }) { change: Math.abs(change), percentChange: Math.abs(percentChange), }; - }, [weightHistory]); + }, [weightHistory, profile.weight_unit]); // --------------------------------------------------------------------------- // Get weight history for last 30 days @@ -257,9 +239,10 @@ export function MoverProvider({ children }: { children: React.ReactNode }) { thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); return weightHistory + .map(entry => ({ ...entry, weight: convertMoverWeight(entry.weight, entry.weight_unit, profile.weight_unit), weight_unit: profile.weight_unit })) .filter(entry => new Date(entry.recorded_at) >= thirtyDaysAgo) .sort((a, b) => new Date(a.recorded_at).getTime() - new Date(b.recorded_at).getTime()); - }, [weightHistory]); + }, [weightHistory, profile.weight_unit]); // --------------------------------------------------------------------------- // Refresh data @@ -275,6 +258,7 @@ export function MoverProvider({ children }: { children: React.ReactNode }) { profile, weightHistory, isLoading, + error, updateProfile, logWeight, deleteWeightEntry, diff --git a/src/context/__tests__/CheckpointPersistence.test.tsx b/src/context/__tests__/CheckpointPersistence.test.tsx new file mode 100644 index 0000000..6ffc7be --- /dev/null +++ b/src/context/__tests__/CheckpointPersistence.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +const { act, create } = require('react-test-renderer'); +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { CheckpointProvider, useCheckpoints } from '../CheckpointContext'; + +const defaults = { distance_from_start: null, elevation: null, location_description: null, latitude: null, longitude: null, cutoff_time: null, cutoff_duration: null, estimated_arrival: null, estimated_duration: null, has_crew_access: false, has_drop_bag: false, has_pacer_pickup: false, has_pacer_dropoff: false, aid_supplies: [], notes: null }; +let context: ReturnType; +function Probe() { context = useCheckpoints(); return null; } +let tree: ReturnType; +const originalSet = (AsyncStorage.setItem as jest.Mock).getMockImplementation()!; +const originalGet = (AsyncStorage.getItem as jest.Mock).getMockImplementation()!; + +beforeEach(async () => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet).mockClear(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{ id: 'race' }])); +}); +afterEach(async () => { if (tree) await act(async () => { tree.unmount(); }); }); +const mount = async () => { await act(async () => { tree = create(); }); }; + +it('does not write an empty map after read failure and blocks edits until retry succeeds', async () => { + await AsyncStorage.setItem('ultraedge_checkpoints', JSON.stringify({ race: [{ id: 'existing', order_index: 0 }] })); + (AsyncStorage.setItem as jest.Mock).mockClear(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(async key => { + if (key === 'ultraedge_checkpoints') throw new Error('Unavailable'); + return originalGet(key); + }); + await mount(); + expect(context.error).toMatch(/could not be loaded/); + expect(AsyncStorage.setItem).not.toHaveBeenCalled(); + await expect(context.addCheckpoint('race', { ...defaults, name: 'New', checkpoint_type: 'aid_station' })).rejects.toThrow('Load checkpoints'); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await act(async () => { await context.refreshCheckpoints(); }); + expect(context.getCheckpointsByEventId('race')[0].id).toBe('existing'); +}); + +it('keeps visible and durable state unchanged after failed save, then supports retry', async () => { + await mount(); + (AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await act(async () => { + await expect(context.addCheckpoint('race', { ...defaults, name: 'Summit', checkpoint_type: 'aid_station' })).rejects.toThrow('Disk full'); + }); + expect(context.getCheckpointsByEventId('race')).toEqual([]); + expect(context.error).toMatch(/not saved/); + await act(async () => { await context.addCheckpoint('race', { ...defaults, name: 'Summit', checkpoint_type: 'aid_station' }); }); + expect(context.getCheckpointsByEventId('race')).toHaveLength(1); + expect(context.error).toBeNull(); +}); + +it('serializes rapid additions with distinct order and refuses lossy reorder', async () => { + await mount(); + await act(async () => { + await Promise.all([ + context.addCheckpoint('race', { ...defaults, name: 'First', checkpoint_type: 'start' }), + context.addCheckpoint('race', { ...defaults, name: 'Second', checkpoint_type: 'finish' }), + ]); + }); + expect(context.getCheckpointsByEventId('race').map(cp => cp.order_index)).toEqual([0, 1]); + await act(async () => { await expect(context.reorderCheckpoints('race', [])).rejects.toThrow('every checkpoint'); }); + expect(context.getCheckpointsByEventId('race')).toHaveLength(2); +}); diff --git a/src/context/__tests__/DropBagTemplates.test.tsx b/src/context/__tests__/DropBagTemplates.test.tsx new file mode 100644 index 0000000..074c542 --- /dev/null +++ b/src/context/__tests__/DropBagTemplates.test.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +const { act, create } = require('react-test-renderer'); +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { DropBagProvider, useDropBags, DROP_BAG_TEMPLATE_KEY } from '../DropBagContext'; +import { deleteLocalEvent } from '../../lib/localPlanStorage'; + +let context: ReturnType; +function Probe() { context = useDropBags(); return null; } +let tree: ReturnType; +const originalSet = (AsyncStorage.setItem as jest.Mock).getMockImplementation()!; +const originalGet = (AsyncStorage.getItem as jest.Mock).getMockImplementation()!; +const mount = async () => { await act(async () => { tree = create(); }); }; +beforeEach(async () => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet).mockClear(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{ id: 'race-a' }, { id: 'race-b' }])); + await AsyncStorage.setItem('ultraedge_checkpoints', JSON.stringify({ 'race-a': [{ id: 'aid-a' }] })); +}); +afterEach(async () => { if (tree) await act(async () => { tree.unmount(); }); }); + +it('saves a durable template, reuses it in another race and keeps every copy independent after deletion', async () => { + await mount(); + let bagId = ''; + await act(async () => { + const bag = await context.createDropBag({ name: 'Night gear', eventId: 'race-a', checkpointId: 'aid-a', notes: 'Keep dry', items: [{ id: 'lamp', type: 'gear', refId: 'gear-lamp', name: 'Headlamp', quantity: 2 }] }); + bagId = bag.id; + }); + await act(async () => { await context.saveDropBagTemplate(bagId); }); + const template = context.templates[0]; + expect(template).not.toHaveProperty('eventId'); + expect(template).not.toHaveProperty('checkpointId'); + expect(template.items[0].id).not.toBe(context.getDropBag(bagId)!.items[0].id); + await act(async () => { tree.unmount(); }); + await mount(); + expect(context.templates[0]).toEqual(template); + let secondId = ''; + await act(async () => { + const bag = await context.createDropBag({ ...context.templates[0], eventId: 'race-b' }); + secondId = bag.id; + }); + const second = context.getDropBag(secondId)!; + expect(second.checkpointId).toBeNull(); + expect(second.items[0].id).not.toBe(template.items[0].id); + await act(async () => { await context.updateItemInDropBag(secondId, second.items[0].id, { quantity: 5 }); }); + expect(context.templates[0].items[0].quantity).toBe(2); + expect(context.getDropBag(bagId)!.items[0].quantity).toBe(2); + await act(async () => { await deleteLocalEvent('race-a'); }); + expect(context.templates).toHaveLength(1); + expect(context.getDropBag(secondId)!.items[0].quantity).toBe(5); + await act(async () => { await context.deleteDropBagTemplate(template.id); }); + expect(context.getDropBag(secondId)).toBeDefined(); + await act(async () => { tree.unmount(); }); + await mount(); + expect(context.templates).toEqual([]); + expect(context.getDropBag(secondId)!.items[0].quantity).toBe(5); +}); + +it('rejects a failed template write without claiming success or losing the original bag', async () => { + await mount(); + await act(async () => { await context.createDropBag({ name: 'Night bag', eventId: 'race-a' }); }); + const id = context.dropBags[0].id; + (AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await act(async () => { await expect(context.saveDropBagTemplate(id)).rejects.toThrow('Disk full'); }); + expect(context.templates).toEqual([]); + expect(context.getDropBag(id)).toBeDefined(); + expect(await AsyncStorage.getItem(DROP_BAG_TEMPLATE_KEY)).toBeNull(); +}); + +it('preserves corrupt template storage and blocks writes until a successful reload', async () => { + await AsyncStorage.setItem(DROP_BAG_TEMPLATE_KEY, 'broken'); + await mount(); + expect(context.error).toBeTruthy(); + await expect(context.createDropBag({ name: 'Bag', eventId: 'race-a' })).rejects.toThrow('load successfully'); + await expect(context.deleteDropBagTemplate('anything')).rejects.toThrow('load successfully'); + expect(await AsyncStorage.getItem(DROP_BAG_TEMPLATE_KEY)).toBe('broken'); +}); diff --git a/src/context/__tests__/GearPersistence.test.tsx b/src/context/__tests__/GearPersistence.test.tsx new file mode 100644 index 0000000..ac7cbd1 --- /dev/null +++ b/src/context/__tests__/GearPersistence.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +const { act, create } = require('react-test-renderer'); +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { GearProvider, useGear, GearItem } from '../GearContext'; + +let context: ReturnType; +function Probe() { context = useGear(); return null; } +let tree: ReturnType; +const originalSet = (AsyncStorage.setItem as jest.Mock).getMockImplementation()!; +const originalGet = (AsyncStorage.getItem as jest.Mock).getMockImplementation()!; +const item: Omit = { name: 'Headlamp', category: 'lighting', weightUnit: 'g', weight: 80, quantity: 1, isActive: true, retired: false }; +beforeEach(async () => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet).mockClear(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await AsyncStorage.clear(); +}); +afterEach(async () => { if (tree) await act(async () => { tree.unmount(); }); }); +const mount = async () => { await act(async () => { tree = create(); }); }; + +it('preserves inventory after a read failure and permits edits only after successful refresh', async () => { + await AsyncStorage.setItem('@ultraedge/gear-items', JSON.stringify([{ ...item, id: 'lamp' }])); + (AsyncStorage.setItem as jest.Mock).mockClear(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(async key => { + if (key === '@ultraedge/gear-items') throw new Error('Unavailable'); + return originalGet(key); + }); + await mount(); + await expect(context.addGearItem(item)).rejects.toThrow('Reload'); + expect(AsyncStorage.setItem).not.toHaveBeenCalled(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await act(async () => { await context.refreshGear(); }); + expect(context.getGearItem('lamp')?.name).toBe('Headlamp'); +}); + +it('does not report a failed save as success and can retry after refreshing', async () => { + await mount(); + (AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await act(async () => { await expect(context.addGearItem(item)).rejects.toThrow('Disk full'); }); + expect(context.gearItems).toEqual([]); + expect(await AsyncStorage.getItem('@ultraedge/gear-items')).toBeNull(); + await act(async () => { await context.refreshGear(); }); + await act(async () => { await context.addGearItem(item); }); + expect(context.gearItems).toHaveLength(1); +}); + +it('preserves both rapid additions and rejects competing edits to the same item', async () => { + await mount(); + await act(async () => { await Promise.all([context.addGearItem(item), context.addGearItem({ ...item, name: 'Spare lamp' })]); }); + expect(context.gearItems).toHaveLength(2); + const id = context.gearItems[0].id; + await act(async () => { + const results = await Promise.allSettled([context.updateGearItem(id, { name: 'Night lamp' }), context.updateGearItem(id, { name: 'Lost update' })]); + expect(results.map(result => result.status)).toEqual(['fulfilled', 'rejected']); + }); + const saved = JSON.parse((await AsyncStorage.getItem('@ultraedge/gear-items'))!); + expect(saved).toHaveLength(2); + expect(saved[0].name).toBe('Night lamp'); +}); diff --git a/src/context/__tests__/MoverPersistence.test.tsx b/src/context/__tests__/MoverPersistence.test.tsx new file mode 100644 index 0000000..5661508 --- /dev/null +++ b/src/context/__tests__/MoverPersistence.test.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { MoverProvider, useMover } from '../MoverContext'; +const { act, create } = require('react-test-renderer'); +const PROFILE = '@ultraedge:mover_profile'; +const HISTORY = '@ultraedge:weight_history'; +const savedProfile = { display_name: 'Runner', current_weight: null, weight_unit: 'lbs', distance_unit: 'kilometers', elevation_unit: 'meters', weight_updated_at: null }; +const originalSet = (AsyncStorage.setItem as jest.Mock).getMockImplementation()!; +const originalGet = (AsyncStorage.getItem as jest.Mock).getMockImplementation()!; +let context: ReturnType; +let tree: any; +function Probe() { context = useMover(); return null; } +async function mount() { await act(async () => { tree = create(); }); } +beforeEach(async () => { + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet).mockClear(); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await AsyncStorage.clear(); + await AsyncStorage.setItem(PROFILE, JSON.stringify(savedProfile)); +}); +afterEach(async () => { if (tree) await act(async () => { tree.unmount(); tree = null; }); }); + +it('preserves saved state on load failure and blocks edits until recovery', async () => { + (AsyncStorage.getItem as jest.Mock).mockImplementation(async key => { + if (key === HISTORY) throw new Error('Read failure'); + return originalGet(key); + }); + await mount(); + expect(context.error).toMatch('could not be loaded'); + await expect(context.updateProfile({ display_name: 'Overwrite' })).rejects.toThrow('Load your profile'); + expect(JSON.parse((await AsyncStorage.getItem(PROFILE))!).display_name).toBe('Runner'); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await act(async () => { await context.refreshData(); }); + expect(context.profile.display_name).toBe('Runner'); + expect(context.error).toBeNull(); +}); + +it('does not publish an optimistic profile after a failed write', async () => { + await mount(); + (AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await act(async () => { await expect(context.updateProfile({ display_name: 'Changed' })).rejects.toThrow('Disk full'); }); + expect(context.profile.display_name).toBe('Runner'); + expect(JSON.parse((await AsyncStorage.getItem(PROFILE))!).display_name).toBe('Runner'); +}); + +it('recovers a partially written weight/profile pair once, before accepting another log', async () => { + await mount(); + (AsyncStorage.setItem as jest.Mock).mockImplementation(async (key, value) => { + if (key === PROFILE) throw new Error('Interrupted'); + return originalSet(key, value); + }); + await act(async () => { await expect(context.logWeight(160)).rejects.toThrow('Interrupted'); }); + expect(context.weightHistory).toEqual([]); + expect(context.profile.current_weight).toBeNull(); + await expect(context.logWeight(160)).rejects.toThrow('Load your profile'); + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet); + await act(async () => { await context.refreshData(); }); + expect(context.weightHistory).toHaveLength(1); + expect(context.profile.current_weight).toBe(160); + await act(async () => { tree.unmount(); tree = null; }); + await mount(); + expect(context.weightHistory).toHaveLength(1); +}); + +it('serializes rapid logs and preserves real weights when switching display units and deleting', async () => { + await mount(); + await act(async () => { await Promise.all([context.logWeight(160), context.logWeight(162)]); }); + expect(context.weightHistory).toHaveLength(2); + await act(async () => { await context.updateProfile({ weight_unit: 'kg' }); }); + expect(context.profile.current_weight).toBeCloseTo(73.48196, 4); + expect(context.getWeightHistory30Days().every(entry => entry.weight_unit === 'kg')).toBe(true); + expect(context.getWeightTrend()?.change).toBeCloseTo(0.90718474); + await act(async () => { await context.deleteWeightEntry(context.weightHistory[0].id); }); + expect(context.profile.current_weight).toBeCloseTo(72.5747792); +}); + +it('refuses malformed stored history without replacing it', async () => { + await AsyncStorage.setItem(HISTORY, '[null]'); + await mount(); + expect(context.error).toMatch('could not be loaded'); + await expect(context.logWeight(160)).rejects.toThrow('Load your profile'); + expect(await AsyncStorage.getItem(HISTORY)).toBe('[null]'); +}); diff --git a/src/context/__tests__/RacePlanningWorkflow.test.tsx b/src/context/__tests__/RacePlanningWorkflow.test.tsx new file mode 100644 index 0000000..f48eeac --- /dev/null +++ b/src/context/__tests__/RacePlanningWorkflow.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +const { act, create } = require('react-test-renderer'); +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { EventProvider, useEvents } from '../EventContext'; +import { CrewProvider, useCrewMembers } from '../CrewContext'; +import { GearProvider, useGear } from '../GearContext'; +import { addEventGear, updateEventGear, removeEventGear, EVENT_GEAR_KEY, EventGearAllocation } from '../../lib/eventGear'; +import { upsertEventCrewAssignments, loadEventCrewAssignments, removeEventCrewAssignment } from '../../lib/eventCrew'; +import { readArray } from '../../lib/localPlanStorage'; + +let events: ReturnType; +let crew: ReturnType; +let gear: ReturnType; +let tree: any; +function Probe() { events = useEvents(); crew = useCrewMembers(); gear = useGear(); return null; } +const mount = async () => { await act(async () => { tree = create(); }); }; +const remount = async () => { await act(async () => { tree.unmount(); }); await mount(); }; +const race = (name: string) => ({ name, event_date: '2027-08-01', total_distance: 120, distance_unit: 'miles' as const } as Parameters[0]); +const lamp = { name: 'Headlamp', category: 'lighting' as const, weightUnit: 'g' as const, weight: 100, quantity: 1, isActive: true, retired: false }; +beforeEach(async () => { (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; await AsyncStorage.clear(); await mount(); }); +afterEach(async () => { await act(async () => { tree.unmount(); }); }); + +it('creates two races and reuses one crew profile with separate roles through edits, restart and deletion', async () => { + let first: any, second: any, member: any; + await act(async () => { first = await events.createEvent(race('Fat Dog rehearsal')); }); + await act(async () => { second = await events.createEvent(race('Training ultra')); }); + await act(async () => { member = await crew.createCrewMember({ name: 'Alex', phone: '555-0100' }); }); + await upsertEventCrewAssignments([ + { eventId: first.id, crewMemberId: member.id, roles: ['pacer'], notes: 'Meet at halfway' }, + { eventId: second.id, crewMemberId: member.id, roles: ['driver'] }, + ]); + await act(async () => { await crew.updateCrewMember(member.id, { phone: '555-0101' }); }); + await act(async () => { await events.updateEvent(first.id, { name: 'Fat Dog 120' }); }); + await upsertEventCrewAssignments([{ eventId: first.id, crewMemberId: member.id, roles: ['crew_chief', 'pacer'] }]); + await remount(); + expect(events.getEvent(first.id)?.name).toBe('Fat Dog 120'); + expect(crew.crewMembers).toHaveLength(1); + expect(crew.getCrewMember(member.id)?.phone).toBe('555-0101'); + expect((await loadEventCrewAssignments()).map(row => row.roles)).toEqual([['crew_chief', 'pacer'], ['driver']]); + await removeEventCrewAssignment(first.id, member.id); + expect(await loadEventCrewAssignments()).toEqual([expect.objectContaining({ eventId: second.id, roles: ['driver'] })]); + await act(async () => { await events.deleteEvent(first.id); }); + await remount(); + expect(events.events).toHaveLength(1); + expect(crew.crewMembers).toHaveLength(1); + await act(async () => { await crew.deleteCrewMember(member.id); }); + await remount(); + expect(crew.crewMembers).toEqual([]); + expect(await loadEventCrewAssignments()).toEqual([]); +}); + +it('packs a shared item independently for two races and preserves inventory when a race is deleted', async () => { + let first: any, second: any, item: any; + await act(async () => { first = await events.createEvent(race('Race A')); }); + await act(async () => { second = await events.createEvent(race('Race B')); }); + await act(async () => { item = await gear.addGearItem(lamp); }); + await addEventGear(first.id, [item.id]); await addEventGear(second.id, [item.id]); + await updateEventGear(first.id, item.id, { quantity: 2, isWorn: true, isCarried: false, isPacked: true }); + await act(async () => { await gear.updateGearItem(item.id, { name: 'Night headlamp' }); }); + await remount(); + const rows = await readArray(EVENT_GEAR_KEY); + expect(rows).toEqual([ + expect.objectContaining({ eventId: first.id, quantity: 2, isWorn: true, isCarried: false, isPacked: true }), + expect.objectContaining({ eventId: second.id, quantity: 1, isCarried: true, isWorn: false }), + ]); + expect(rows[1].isPacked).toBeFalsy(); + expect(gear.getGearItem(item.id)).toMatchObject({ name: 'Night headlamp', quantity: 1 }); + await act(async () => { await events.deleteEvent(first.id); }); + await remount(); + expect(gear.gearItems).toHaveLength(1); + expect(await readArray(EVENT_GEAR_KEY)).toEqual([expect.objectContaining({ eventId: second.id })]); + await removeEventGear(second.id, item.id); + await expect(updateEventGear(second.id, item.id, { isPacked: true })).rejects.toThrow('removed'); + await addEventGear(second.id, [item.id]); + await act(async () => { await gear.deleteGearItem(item.id); }); + await remount(); + expect(gear.gearItems).toEqual([]); + expect(await readArray(EVENT_GEAR_KEY)).toEqual([]); +}); diff --git a/src/hooks/useFonts.ts b/src/hooks/useFonts.ts index 1053b66..fea271e 100644 --- a/src/hooks/useFonts.ts +++ b/src/hooks/useFonts.ts @@ -1,42 +1,8 @@ -/** - * Font Loading Hook - * Loads all custom fonts for the Organic design system - */ - +/** Native type with bundled monospaced figures. */ import { useFonts as useExpoFonts } from 'expo-font'; -import { - Nunito_400Regular, - Nunito_500Medium, - Nunito_600SemiBold, - Nunito_700Bold, - Nunito_800ExtraBold, -} from '@expo-google-fonts/nunito'; -import { - PlayfairDisplay_400Regular, - PlayfairDisplay_700Bold, -} from '@expo-google-fonts/playfair-display'; -import { - JetBrainsMono_400Regular, - JetBrainsMono_500Medium, -} from '@expo-google-fonts/jetbrains-mono'; - +import { JetBrainsMono_400Regular, JetBrainsMono_500Medium } from '@expo-google-fonts/jetbrains-mono'; export function useFonts() { - const [fontsLoaded, fontError] = useExpoFonts({ - // Nunito - Primary font - Nunito_400Regular, - Nunito_500Medium, - Nunito_600SemiBold, - Nunito_700Bold, - Nunito_800ExtraBold, - // Playfair Display - Display numbers - PlayfairDisplay_400Regular, - PlayfairDisplay_700Bold, - // JetBrains Mono - Data/weight values - JetBrainsMono_400Regular, - JetBrainsMono_500Medium, - }); - + const [fontsLoaded, fontError] = useExpoFonts({ JetBrainsMono_400Regular, JetBrainsMono_500Medium }); return { fontsLoaded, fontError }; } - export default useFonts; diff --git a/src/lib/__tests__/eventGear.test.ts b/src/lib/__tests__/eventGear.test.ts new file mode 100644 index 0000000..8ccff3d --- /dev/null +++ b/src/lib/__tests__/eventGear.test.ts @@ -0,0 +1,44 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { addEventGear, removeEventGear, EVENT_GEAR_KEY } from '../eventGear'; +import { deleteLocalEvent } from '../localPlanStorage'; + +beforeEach(async () => { + await AsyncStorage.clear(); + await AsyncStorage.setItem('@ultraedge/events', JSON.stringify([{ id: 'race' }, { id: 'other' }])); + await AsyncStorage.setItem('@ultraedge/gear-items', JSON.stringify([ + { id: 'lamp', isActive: true, retired: false }, + { id: 'shoes', isActive: true, retired: false }, + ])); +}); + +it('deduplicates simultaneous selection saves and preserves other events', async () => { + await Promise.all([addEventGear('race', ['lamp', 'lamp']), addEventGear('race', ['lamp', 'shoes']), addEventGear('other', ['lamp'])]); + expect(JSON.parse((await AsyncStorage.getItem(EVENT_GEAR_KEY))!)).toHaveLength(3); + const remaining = await removeEventGear('race', 'lamp'); + expect(remaining.map(row => [row.eventId, row.gearItemId])).toEqual([['race', 'shoes'], ['other', 'lamp']]); +}); + +it('does not recreate relationships after an event is deleted', async () => { + await addEventGear('race', ['lamp']); + await deleteLocalEvent('race'); + await expect(addEventGear('race', ['shoes'])).rejects.toThrow('deleted'); + expect(JSON.parse((await AsyncStorage.getItem(EVENT_GEAR_KEY))!)).toEqual([]); +}); + +it('preserves malformed data rather than overwriting it and rejects deleted gear', async () => { + await expect(addEventGear('race', ['missing'])).rejects.toThrow('no longer available'); + await AsyncStorage.setItem(EVENT_GEAR_KEY, '{'); + await expect(addEventGear('race', ['lamp'])).rejects.toThrow(); + expect(await AsyncStorage.getItem(EVENT_GEAR_KEY)).toBe('{'); +}); + +it('validates race packing quantities and preserves the last saved state on write failure', async () => { + const { updateEventGear } = require('../eventGear'); + await addEventGear('race', ['lamp']); + for (const quantity of [0, -1, 1.5, NaN]) await expect(updateEventGear('race', 'lamp', { quantity })).rejects.toThrow('whole number'); + (AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(new Error('Disk full')); + await expect(updateEventGear('race', 'lamp', { isPacked: true })).rejects.toThrow('Disk full'); + expect(JSON.parse((await AsyncStorage.getItem(EVENT_GEAR_KEY))!)[0].isPacked).toBeUndefined(); + await updateEventGear('race', 'lamp', { isPacked: true }); + expect(JSON.parse((await AsyncStorage.getItem(EVENT_GEAR_KEY))!)[0].isPacked).toBe(true); +}); diff --git a/src/lib/__tests__/localPlanStorage.test.ts b/src/lib/__tests__/localPlanStorage.test.ts new file mode 100644 index 0000000..4f9105d --- /dev/null +++ b/src/lib/__tests__/localPlanStorage.test.ts @@ -0,0 +1,153 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { deleteLocalEvent, runLocalPlanOperation, readArray, saveArrayChanges, deleteLocalCrewMember, deleteLocalCheckpoints, deleteLocalGear } from '../localPlanStorage'; +import { loadAndMigrateCrew, upsertEventCrewAssignments, loadEventCrewAssignments } from '../eventCrew'; + +const put = (key: string, value: unknown) => AsyncStorage.setItem(key, JSON.stringify(value)); +const get = async (key: string) => JSON.parse((await AsyncStorage.getItem(key))!); +const originalSet = (AsyncStorage.setItem as jest.Mock).getMockImplementation()!; +const originalGet = (AsyncStorage.getItem as jest.Mock).getMockImplementation()!; + +beforeEach(async () => { + jest.restoreAllMocks(); + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await AsyncStorage.clear(); +}); + +it.each(['@ultraedge/event-crew', '@ultraedge/crew'])('retains legacy custom roles when migration write fails at %s and retries', async failedKey => { + await put('@ultraedge/crew', [{ id: 'runner', name: 'Runner', role: 'other', customRole: 'Night pacer' }]); + await put('@ultraedge/event-crew', [{ eventId: 'race', crewMemberId: 'runner' }]); + jest.spyOn(AsyncStorage, 'setItem').mockImplementation(async (key, value) => { + if (key === failedKey) throw new Error('Disk full'); + return originalSet(key, value); + }); + await expect(loadAndMigrateCrew()).rejects.toThrow('Disk full'); + expect((await get('@ultraedge/crew'))[0].role).toBe('other'); + jest.restoreAllMocks(); + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + await loadAndMigrateCrew(); + expect((await get('@ultraedge/crew'))[0].role).toBeUndefined(); + expect((await get('@ultraedge/event-crew'))[0]).toMatchObject({ roles: ['other'], customRole: 'Night pacer' }); +}); + +it('concurrent assignment saves are unique and keep unrelated events', async () => { + await put('@ultraedge/events', [{ id: 'race' }, { id: 'other' }]); + await put('@ultraedge/crew', [{ id: 'a' }]); + await Promise.all([ + upsertEventCrewAssignments([{ eventId: 'race', crewMemberId: 'a', roles: ['pacer'] }]), + upsertEventCrewAssignments([{ eventId: 'race', crewMemberId: 'a', roles: ['driver'] }]), + upsertEventCrewAssignments([{ eventId: 'other', crewMemberId: 'a', roles: ['medical'] }]), + ]); + expect(await loadEventCrewAssignments()).toEqual([ + { eventId: 'race', crewMemberId: 'a', roles: ['driver'] }, + { eventId: 'other', crewMemberId: 'a', roles: ['medical'] }, + ]); +}); + +it('propagates assignment read failure instead of silently overwriting saved data', async () => { + jest.spyOn(AsyncStorage, 'getItem').mockRejectedValueOnce(new Error('Read unavailable')); + await expect(loadEventCrewAssignments()).rejects.toThrow('Read unavailable'); +}); + +it('recovers interrupted event cascade before the next read and preserves unrelated data', async () => { + await put('@ultraedge/events', [{ id: 'race' }, { id: 'other' }]); + for (const key of ['@ultraedge/event-crew', '@ultraedge/event-gear', '@ultraedge/dropbags']) { + await put(key, [{ eventId: 'race', id: 'a' }, { eventId: 'other', id: 'b' }]); + } + await put('ultraedge_checkpoints', { race: [{ id: 'cp' }], other: [{ id: 'keep' }] }); + jest.spyOn(AsyncStorage, 'setItem').mockImplementation(async (key, value) => { + if (key === 'ultraedge_checkpoints') throw new Error('Disk full'); + return originalSet(key, value); + }); + await expect(deleteLocalEvent('race')).rejects.toThrow(); + expect(await get('@ultraedge/events')).toHaveLength(2); + expect(await AsyncStorage.getItem('@ultraedge/pending-plan-write')).not.toBeNull(); + jest.restoreAllMocks(); + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet); + (AsyncStorage.getItem as jest.Mock).mockImplementation(originalGet); + expect(await runLocalPlanOperation(() => readArray('@ultraedge/events'))).toEqual([{ id: 'other' }]); + expect(await get('ultraedge_checkpoints')).toEqual({ other: [{ id: 'keep' }] }); + for (const key of ['@ultraedge/event-crew', '@ultraedge/event-gear', '@ultraedge/dropbags']) { + expect(await get(key)).toEqual([{ eventId: 'other', id: 'b' }]); + } + expect(await AsyncStorage.getItem('@ultraedge/pending-plan-write')).toBeNull(); +}); + +it('does not start deletion when existing child data cannot be decoded', async () => { + await put('@ultraedge/events', [{ id: 'race' }]); + await AsyncStorage.setItem('ultraedge_checkpoints', 'bad json'); + await expect(deleteLocalEvent('race')).rejects.toThrow(); + expect(await get('@ultraedge/events')).toEqual([{ id: 'race' }]); + expect(await AsyncStorage.getItem('@ultraedge/pending-plan-write')).toBeNull(); +}); + +it('merges concurrent edits without losing newly added rows or resurrecting deletions', async () => { + const initial = [{ id: 'a', name: 'A' }]; + await put('items', initial); + await Promise.all([ + saveArrayChanges('items', initial, [...initial, { id: 'b', name: 'B' }]), + saveArrayChanges('items', initial, [{ id: 'a', name: 'Edited' }]), + ]); + expect(await get('items')).toEqual([{ id: 'a', name: 'Edited' }, { id: 'b', name: 'B' }]); + const refreshed = await get('items'); + await saveArrayChanges('items', refreshed, refreshed.filter((row: { id: string }) => row.id !== 'a')); + await expect(saveArrayChanges('items', initial, [{ id: 'a', name: 'Old screen' }])).rejects.toThrow('deleted'); +}); + + +it('rejects conflicting writes to the same record instead of silently losing changes', async () => { + const initial = [{ id: 'bag', items: [] }]; + await put('items', initial); + await saveArrayChanges('items', initial, [{ id: 'bag', items: ['water'] }]); + await expect(saveArrayChanges('items', initial, [{ id: 'bag', items: ['food'] }])).rejects.toThrow('changed'); + expect(await get('items')).toEqual([{ id: 'bag', items: ['water'] }]); +}); + +it('removes crew assignments and prevents stale screens from reassigning a deleted member', async () => { + await put('@ultraedge/events', [{ id: 'race' }]); + await put('@ultraedge/crew', [{ id: 'a' }, { id: 'b' }]); + await put('@ultraedge/event-crew', [{ eventId: 'race', crewMemberId: 'a', roles: ['pacer'] }, { eventId: 'race', crewMemberId: 'b', roles: [] }]); + await deleteLocalCrewMember('a'); + expect(await get('@ultraedge/event-crew')).toEqual([{ eventId: 'race', crewMemberId: 'b', roles: [] }]); + await expect(upsertEventCrewAssignments([{ eventId: 'race', crewMemberId: 'a', roles: [] }])).rejects.toThrow('deleted'); +}); + +it('unassigns bags from a deleted checkpoint while retaining their packed items and other locations', async () => { + await put('ultraedge_checkpoints', { race: [{ id: 'first', order_index: 0 }, { id: 'second', order_index: 1 }] }); + const bags = [{ id: 'bag', eventId: 'race', checkpointId: 'first', items: ['water'] }, { id: 'keep', eventId: 'race', checkpointId: 'second', items: [] }]; + await put('@ultraedge/dropbags', bags); + await deleteLocalCheckpoints('race', 'first'); + expect(await get('@ultraedge/dropbags')).toEqual([{ ...bags[0], checkpointId: null }, bags[1]]); + expect(await get('ultraedge_checkpoints')).toEqual({ race: [{ id: 'second', order_index: 0 }] }); +}); + +it('rejects child creation after its event has been deleted', async () => { + await expect(saveArrayChanges('@ultraedge/dropbags', [], [{ id: 'bag', eventId: 'gone' }])).rejects.toThrow('deleted'); + expect(await AsyncStorage.getItem('@ultraedge/dropbags')).toBeNull(); +}); + +it('cascades gear removal through event allocations and packed bag items', async () => { + await put('@ultraedge/gear-items', [{ id: 'water' }, { id: 'food' }]); + await put('@ultraedge/event-gear', [{ eventId: 'race', gearItemId: 'water' }]); + await put('@ultraedge/dropbags', [{ id: 'bag', items: [{ refId: 'water' }, { refId: 'food' }] }]); + await deleteLocalGear('water'); + expect(await get('@ultraedge/gear-items')).toEqual([{ id: 'food' }]); + expect(await get('@ultraedge/event-gear')).toEqual([]); + expect(await get('@ultraedge/dropbags')).toEqual([{ id: 'bag', items: [{ refId: 'food' }] }]); +}); + +it('recovers a checkpoint deletion interrupted after unassigning bags', async () => { + await put('ultraedge_checkpoints', { race: [{ id: 'cp', order_index: 0 }] }); + await put('@ultraedge/dropbags', [{ id: 'bag', eventId: 'race', checkpointId: 'cp', items: [] }]); + jest.spyOn(AsyncStorage, 'setItem').mockImplementation(async (key, value) => { + if (key === 'ultraedge_checkpoints') throw new Error('Disk full'); + return originalSet(key, value); + }); + await expect(deleteLocalCheckpoints('race', 'cp')).rejects.toThrow('Disk full'); + (AsyncStorage.setItem as jest.Mock).mockImplementation(originalSet); + await runLocalPlanOperation(async () => undefined); + expect(await get('ultraedge_checkpoints')).toEqual({ race: [] }); + expect((await get('@ultraedge/dropbags'))[0].checkpointId).toBeNull(); + expect(await AsyncStorage.getItem('@ultraedge/pending-plan-write')).toBeNull(); +}); diff --git a/src/lib/attachments.ts b/src/lib/attachments.ts new file mode 100644 index 0000000..30e690d --- /dev/null +++ b/src/lib/attachments.ts @@ -0,0 +1,69 @@ +import * as FS from 'expo-file-system/legacy'; +import * as Crypto from 'expo-crypto'; +export interface PlanAttachment { + path: string; + name: string; + mimeType: string; +} +const folder = () => { + if (!FS.documentDirectory) { + throw new Error('File storage is unavailable on this device.'); + } + return FS.documentDirectory + 'plan-attachments/'; +}; +export function attachmentUri(path: string): string { + if (!/^[a-zA-Z0-9-]+\.(jpg|png|webp|heic|pdf)$/.test(path)) { + throw new Error('Invalid attachment path.'); + } + return folder() + path; +} +export async function saveAttachment( + uri: string, + name: string, + mimeType: string, +): Promise { + const extension = + mimeType === 'application/pdf' + ? 'pdf' + : mimeType === 'image/png' + ? 'png' + : mimeType === 'image/webp' + ? 'webp' + : mimeType === 'image/heic' + ? 'heic' + : 'jpg'; + const info = await FS.getInfoAsync(uri); + if ( + !info.exists || + !('size' in info) || + info.size <= 0 || + info.size > 50 * 1024 * 1024 + ) { + throw new Error('Choose a non-empty file smaller than 50 MB.'); + } + if (extension === 'pdf') { + const header = await FS.readAsStringAsync(uri, { + encoding: FS.EncodingType.Base64, + position: 0, + length: 5, + }); + if (header !== 'JVBERi0=') { + throw new Error('This file is not a PDF.'); + } + } + await FS.makeDirectoryAsync(folder(), { intermediates: true }); + const path = `${Crypto.randomUUID()}.${extension}`; + await FS.copyAsync({ from: uri, to: attachmentUri(path) }); + return { path, name, mimeType }; +} + +/** Share a readable filename while keeping stored references stable and unique. */ +export async function attachmentShareUri(attachment: PlanAttachment): Promise { + if (!FS.cacheDirectory) throw new Error('File sharing storage is unavailable.'); + const name = attachment.name.replace(/[\\/\x00-\x1f]/g, '_').slice(0, 120).replace(/\.pdf$/i, '') || 'Race guide'; + const directory = `${FS.cacheDirectory}guide-share-${Crypto.randomUUID()}/`; + await FS.makeDirectoryAsync(directory, { intermediates: true }); + const uri = `${directory}${name}.pdf`; + await FS.copyAsync({ from: attachmentUri(attachment.path), to: uri }); + return uri; +} diff --git a/src/lib/authLinks.ts b/src/lib/authLinks.ts new file mode 100644 index 0000000..721fa4c --- /dev/null +++ b/src/lib/authLinks.ts @@ -0,0 +1,41 @@ +/** Only the registered app scheme and exact auth destinations can establish sessions. */ +export const AUTH_CALLBACK_URL = 'ultraedge://auth/callback'; +export const PASSWORD_RESET_URL = 'ultraedge://auth/reset-password'; + +export function parseAuthCallback(url: string, destination: 'callback' | 'reset-password'): URLSearchParams | null { + try { + const parsed = new URL(url); + if (parsed.protocol !== 'ultraedge:' || parsed.hostname !== 'auth' || + parsed.pathname !== `/${destination}` || parsed.port || parsed.username || parsed.password) return null; + const params = new URLSearchParams(parsed.hash.slice(1)); + // Reject duplicates and query/fragment ambiguity instead of choosing a token silently. + const seen = new Set(); + for (const [key] of params) { + if (seen.has(key)) return null; + seen.add(key); + } + for (const [key, value] of parsed.searchParams) { + if (seen.has(key)) return null; + seen.add(key); + params.set(key, value); + } + return params; + } catch { return null; } +} + +export function recoveryTokens(params: URLSearchParams) { + const access_token = params.get('access_token'); + const refresh_token = params.get('refresh_token'); + if (params.get('error') || params.get('error_code') || params.get('type') !== 'recovery' || + !access_token || access_token.length > 8192 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(access_token) || + !refresh_token || refresh_token.length < 8 || refresh_token.length > 512 || !/^[A-Za-z0-9._-]+$/.test(refresh_token)) return null; + return { access_token, refresh_token }; +} + +export function isSupabaseConfigured(url: string, key: string): boolean { + try { + const parsed = new URL(url); + return parsed.protocol === 'https:' && !parsed.username && !parsed.password && + !!parsed.hostname && key.length > 20 && !/placeholder|your[_-]|xxxxx/i.test(`${url} ${key}`); + } catch { return false; } +} diff --git a/src/lib/courseXml.ts b/src/lib/courseXml.ts new file mode 100644 index 0000000..21429ac --- /dev/null +++ b/src/lib/courseXml.ts @@ -0,0 +1,61 @@ +import { DOMParser, XMLSerializer } from '@xmldom/xmldom'; +import { haversineMiles } from './gpx'; + +/** Normalize supported KML course geometry into the GPX used by storage and preview. */ +export function normalizeCourseXml(xml: string): string { + const doc = new DOMParser({ onError: () => { throw new Error('The course file contains malformed XML.'); } }).parseFromString(xml, 'text/xml'); + if (doc.documentElement?.localName === 'gpx') return xml; + if (doc.documentElement?.localName !== 'kml') throw new Error('Choose a GPX or KML course file.'); + const ns = 'http://www.opengis.net/kml/2.2'; + const output = new DOMParser().parseFromString('', 'text/xml'); + const root = output.documentElement!; + const route = root.firstChild!; + const gp = 'http://www.topografix.com/GPX/1/1'; + const tuple = (raw: string) => { + const values = raw.split(','); + const lon = Number(values[0]?.trim() || NaN), lat = Number(values[1]?.trim() || NaN); + const ele = Number(values[2]?.trim() || NaN); + if (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180) throw new Error('The KML contains invalid course coordinates.'); + return { lat, lon, ele }; + }; + const addPoint = (raw: string, tag: string, name = '', description = '') => { + const p = tuple(raw), node = output.createElementNS(gp, tag); + node.setAttribute('lat', String(p.lat)); node.setAttribute('lon', String(p.lon)); + for (const [key, value] of [['ele', Number.isFinite(p.ele) ? String(p.ele) : ''], ['name', name], ['desc', description]]) { + if (!value) continue; + const child = output.createElementNS(gp, key); child.appendChild(output.createTextNode(value)); node.appendChild(child); + } + if (tag === 'wpt') root.insertBefore(node, route); else route.appendChild(node); + return p; + }; + const placemarks = doc.getElementsByTagNameNS(ns, 'Placemark'); + let courseCount = 0; + let previous: ReturnType | undefined; + for (let i = 0; i < placemarks.length; i++) { + const mark = placemarks[i]; + const field = (name: string) => { + for (let child = mark.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 1 && (child as typeof mark).localName === name) return child.textContent?.trim() || ''; + } + return ''; + }; + const points = mark.getElementsByTagNameNS(ns, 'Point'); + for (let j = 0; j < points.length; j++) { + const raw = points[j].getElementsByTagNameNS(ns, 'coordinates')[0]?.textContent?.trim(); + if (raw) addPoint(raw, 'wpt', field('name'), field('description')); + } + const lines = mark.getElementsByTagNameNS(ns, 'LineString'); + if (lines.length && ++courseCount > 1) throw new Error('This KML contains multiple courses. Export one course at a time.'); + for (let j = 0; j < lines.length; j++) { + const raw = lines[j].getElementsByTagNameNS(ns, 'coordinates')[0]?.textContent?.trim(); + if (!raw) continue; + const samples = raw.split(/\s+/), first = tuple(samples[0]); + if (previous && haversineMiles(previous.lat, previous.lon, first.lat, first.lon) * 1609.344 > 50) { + throw new Error('The KML course sections are disconnected. Export a continuous course.'); + } + for (const sample of samples) previous = addPoint(sample, 'rtept'); + } + } + if (!previous) throw new Error('This KML has no supported course line. Export a course with LineString geometry.'); + return new XMLSerializer().serializeToString(output); +} diff --git a/src/lib/database.types.ts b/src/lib/database.types.ts index f9d3e06..c5fd5cb 100644 --- a/src/lib/database.types.ts +++ b/src/lib/database.types.ts @@ -40,6 +40,8 @@ export interface Mover { } export interface Event { + mandatoryGear?: { id: string; name: string; quantity: number; gearIds: string[] }[]; + raceGuide?: import('./attachments').PlanAttachment | null; id: string; mover_id: string; name: string; diff --git a/src/lib/dateOnly.ts b/src/lib/dateOnly.ts new file mode 100644 index 0000000..15995e6 --- /dev/null +++ b/src/lib/dateOnly.ts @@ -0,0 +1,26 @@ +/** Calendar dates belong to the runner's selected day, not UTC midnight. */ +export function parseDateOnly(value: string | null | undefined): Date | null { + if (!value) return null; + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) return null; + const [, year, month, day] = match.map(Number); + // Noon avoids local midnight transitions in time zones that change clocks then. + const result = new Date(0); + result.setFullYear(year, month - 1, day); + result.setHours(12, 0, 0, 0); + return result.getFullYear() === year && result.getMonth() === month - 1 && result.getDate() === day + ? result : null; +} + +export function formatDateOnly(date: Date): string { + if (!Number.isFinite(date.getTime())) throw new Error('Choose a valid event date.'); + return `${String(date.getFullYear()).padStart(4, '0')}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; +} + +/** Count local calendar boundaries, independent of time of day and DST. */ +export function daysUntilDate(value: string | null | undefined, today = new Date()): number | null { + const date = parseDateOnly(value); + if (!date || !Number.isFinite(today.getTime())) return null; + const ordinal = (d: Date) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()); + return Math.round((ordinal(date) - ordinal(today)) / 86400000); +} diff --git a/src/lib/eventCrew.ts b/src/lib/eventCrew.ts new file mode 100644 index 0000000..d19c5b3 --- /dev/null +++ b/src/lib/eventCrew.ts @@ -0,0 +1,137 @@ +/** + * Per-event crew assignments — shared type, storage access, and the one-time + * migration that moved roles off crew member profiles onto event assignments. + */ + +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { readArray, runLocalPlanOperation, requireLocalEvent } from './localPlanStorage'; + +import type { CrewRole } from '../context/CrewContext'; + +export const EVENT_CREW_KEY = '@ultraedge/event-crew'; + +export interface EventCrewAssignment { + eventId: string; + crewMemberId: string; + /** A member can hold multiple roles for a single event. */ + roles: CrewRole[]; + /** Custom label used when `roles` includes 'other'. */ + customRole?: string | null; + notes?: string; +} + +export async function loadEventCrewAssignments(): Promise { + return runLocalPlanOperation(() => readArray(EVENT_CREW_KEY)); +} + +export async function saveEventCrewAssignments( + assignments: EventCrewAssignment[] +): Promise { + await AsyncStorage.setItem(EVENT_CREW_KEY, JSON.stringify(assignments)); +} + +// ============================================================================ +// LEGACY ROLE MIGRATION +// ============================================================================ + +/** + * Crew member records as they may exist on disk from older app versions, + * where `role`/`customRole` lived on the member profile. + */ +export interface StoredCrewMemberRecord { + id: string; + role?: CrewRole; + customRole?: string | null; + [key: string]: unknown; +} + +interface MigrationResult { + members: StoredCrewMemberRecord[]; + assignments: EventCrewAssignment[]; + changed: boolean; +} + +/** + * Copies each member's legacy profile role onto all of that member's event + * assignments (as a single-element `roles` array), then strips `role` and + * `customRole` from the member records. Idempotent: once members carry no + * legacy fields and assignments have `roles` arrays, this is a no-op. + */ +export function migrateLegacyCrewRoles( + members: StoredCrewMemberRecord[], + assignments: EventCrewAssignment[] +): MigrationResult { + let changed = false; + + const membersById = new Map(members.map(m => [m.id, m])); + + const migratedAssignments = assignments.map(assignment => { + if (Array.isArray(assignment.roles)) { + return assignment; + } + + changed = true; + const member = membersById.get(assignment.crewMemberId); + const legacyRole = member?.role; + + if (!legacyRole) { + return { ...assignment, roles: [] as CrewRole[] }; + } + + return { + ...assignment, + roles: [legacyRole], + customRole: + legacyRole === 'other' && member?.customRole ? member.customRole : null, + }; + }); + + const migratedMembers = members.map(member => { + if (!('role' in member) && !('customRole' in member)) { + return member; + } + changed = true; + const { role: _role, customRole: _customRole, ...rest } = member; + return rest as StoredCrewMemberRecord; + }); + + return { members: migratedMembers, assignments: migratedAssignments, changed }; +} + +/** Assignments must become durable before removing the only legacy role copy. */ +export async function loadAndMigrateCrew(): Promise { + return runLocalPlanOperation(async () => { + const members = await readArray('@ultraedge/crew'); + const assignments = await readArray(EVENT_CREW_KEY); + const result = migrateLegacyCrewRoles(members, assignments); + if (result.changed) { + await AsyncStorage.setItem(EVENT_CREW_KEY, JSON.stringify(result.assignments)); + await AsyncStorage.setItem('@ultraedge/crew', JSON.stringify(result.members)); + } + return result.members; + }); +} + +/** Upsert under one lock; a slow initial screen load cannot duplicate a member. */ +export async function upsertEventCrewAssignments(assignments: EventCrewAssignment[]): Promise { + await runLocalPlanOperation(async () => { + const current = await readArray(EVENT_CREW_KEY); + const byMember = new Map(current.map(item => [JSON.stringify([item.eventId, item.crewMemberId]), item])); + const members = await readArray<{ id: string }>('@ultraedge/crew'); + for (const assignment of assignments) { + await requireLocalEvent(assignment.eventId); + if (!members.some(member => member.id === assignment.crewMemberId)) throw new Error('This crew member was deleted. Refresh before assigning.'); + } + assignments.forEach(item => byMember.set(JSON.stringify([item.eventId, item.crewMemberId]), item)); + await saveEventCrewAssignments([...byMember.values()]); + }); +} + +export async function removeEventCrewAssignment(eventId: string, crewMemberId: string): Promise { + return runLocalPlanOperation(async () => { + const current = await readArray(EVENT_CREW_KEY); + const next = current.filter(item => item.eventId !== eventId || item.crewMemberId !== crewMemberId); + await saveEventCrewAssignments(next); + return next; + }); +} diff --git a/src/lib/eventGear.ts b/src/lib/eventGear.ts new file mode 100644 index 0000000..4d06d43 --- /dev/null +++ b/src/lib/eventGear.ts @@ -0,0 +1,53 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { readArray, runLocalPlanOperation } from './localPlanStorage'; + +export const EVENT_GEAR_KEY = '@ultraedge/event-gear'; +export interface EventGearAllocation { + eventId: string; + gearItemId: string; + isPacked?: boolean; + isWorn: boolean; + isCarried: boolean; + quantity: number; + notes?: string; +} + +export async function addEventGear(eventId: string, gearIds: string[]): Promise { + await runLocalPlanOperation(async () => { + const events = await readArray<{ id: string }>('@ultraedge/events'); + if (!events.some(event => event.id === eventId)) throw new Error('This event was deleted.'); + const inventory = await readArray<{ id: string; retired: boolean; isActive: boolean }>('@ultraedge/gear-items'); + const all = await readArray(EVENT_GEAR_KEY); + for (const gearItemId of new Set(gearIds)) { + if (!inventory.some(item => item.id === gearItemId && !item.retired && item.isActive)) { + throw new Error('Selected gear is no longer available. Refresh and try again.'); + } + if (!all.some(row => row.eventId === eventId && row.gearItemId === gearItemId)) { + all.push({ eventId, gearItemId, isWorn: false, isCarried: true, quantity: 1 }); + } + } + await AsyncStorage.setItem(EVENT_GEAR_KEY, JSON.stringify(all)); + }); +} + +export async function removeEventGear(eventId: string, gearItemId: string): Promise { + return runLocalPlanOperation(async () => { + const all = await readArray(EVENT_GEAR_KEY); + const updated = all.filter(row => !(row.eventId === eventId && row.gearItemId === gearItemId)); + await AsyncStorage.setItem(EVENT_GEAR_KEY, JSON.stringify(updated)); + return updated; + }); +} + +/** Edit only this race's packing list; shared inventory and other races stay intact. */ +export async function updateEventGear(eventId: string, gearItemId: string, updates: Partial>): Promise { + return runLocalPlanOperation(async () => { + const all = await readArray(EVENT_GEAR_KEY); + const index = all.findIndex(row => row.eventId === eventId && row.gearItemId === gearItemId); + if (index < 0) throw new Error('This gear assignment was removed. Refresh before editing.'); + if (updates.quantity !== undefined && (!Number.isInteger(updates.quantity) || updates.quantity < 1)) throw new Error('Quantity must be a positive whole number.'); + all[index] = { ...all[index], ...updates }; + await AsyncStorage.setItem(EVENT_GEAR_KEY, JSON.stringify(all)); + return all; + }); +} diff --git a/src/lib/gpx.ts b/src/lib/gpx.ts index 8c749ff..3eaff20 100644 --- a/src/lib/gpx.ts +++ b/src/lib/gpx.ts @@ -83,22 +83,27 @@ const ELEVATION_NOISE_THRESHOLD_M = 3; /** Extract track (or route) points from GPX XML. Invalid points are skipped. */ export function parseGpx(xml: string): GpxPoint[] { - const doc = new DOMParser().parseFromString(xml, 'text/xml'); - let nodes = doc.getElementsByTagName('trkpt'); - if (nodes.length === 0) nodes = doc.getElementsByTagName('rtept'); + const doc = new DOMParser({ + onError: () => { throw new Error('The GPX file contains malformed XML.'); }, + }).parseFromString(xml, 'text/xml'); + if (doc.documentElement?.localName !== 'gpx') { + throw new Error('Choose a GPX course file.'); + } + let nodes = doc.getElementsByTagNameNS('*', 'trkpt'); + if (nodes.length === 0) nodes = doc.getElementsByTagNameNS('*', 'rtept'); const points: GpxPoint[] = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; - const lat = parseFloat(node.getAttribute('lat') || ''); - const lon = parseFloat(node.getAttribute('lon') || ''); - if (isNaN(lat) || isNaN(lon)) continue; + const lat = Number(node.getAttribute('lat')?.trim() || NaN); + const lon = Number(node.getAttribute('lon')?.trim() || NaN); + if (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180) continue; let ele: number | undefined; - const eleNodes = node.getElementsByTagName('ele'); + const eleNodes = node.getElementsByTagNameNS('*', 'ele'); if (eleNodes.length > 0) { - const parsed = parseFloat(eleNodes[0].textContent || ''); - if (!isNaN(parsed)) ele = parsed; + const parsed = Number(eleNodes[0].textContent?.trim() || NaN); + if (Number.isFinite(parsed)) ele = parsed; } points.push({ lat, lon, ele }); } @@ -117,7 +122,7 @@ export function haversineMiles(lat1: number, lon1: number, lat2: number, lon2: n const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; - return 2 * EARTH_RADIUS_MILES * Math.asin(Math.sqrt(a)); + return 2 * EARTH_RADIUS_MILES * Math.asin(Math.sqrt(Math.min(1, Math.max(0, a)))); } // ============================================================================ @@ -362,6 +367,7 @@ export function elevationUnitForDistance(unit: DistanceUnit): ElevationUnit { /** The route figures needed to populate an event's course stats. */ export interface GpxRouteStats { + checkpoints?: GpxCheckpoint[]; totalDistanceMi: number; elevationGainFt: number | null; elevationLossFt: number | null; @@ -394,3 +400,47 @@ export function eventStatsFromRoute( : null, }; } + +/** Explicit GPX markers only: ordinary course samples are not aid stations. */ +export interface GpxCheckpoint extends GpxPoint { + name: string; + description: string | null; + distanceMi: number; +} + +export function parseGpxCheckpoints(xml: string, metrics: RouteMetrics): GpxCheckpoint[] { + const doc = new DOMParser({ onError: () => { throw new Error('The GPX file contains malformed XML.'); } }).parseFromString(xml, 'text/xml'); + const markers: GpxCheckpoint[] = []; + for (const tag of ['wpt', 'rtept', 'trkpt']) { + const nodes = doc.getElementsByTagNameNS('*', tag); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const field = (name: string) => node.getElementsByTagNameNS('*', name)[0]?.textContent?.trim() || ''; + const name = field('name'); + if (tag !== 'wpt' && !name) continue; + const lat = Number(node.getAttribute('lat')?.trim() || NaN); + const lon = Number(node.getAttribute('lon')?.trim() || NaN); + if (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180) continue; + // Project onto each course segment to avoid rounding to sparse track vertices. + let best = Infinity; + let distanceMi = 0; + for (let j = 1; j < metrics.points.length; j++) { + const a = metrics.points[j - 1], b = metrics.points[j]; + const scale = Math.cos(lat * Math.PI / 180); + const dx = (b.lon - a.lon) * scale, dy = b.lat - a.lat; + const length2 = dx * dx + dy * dy; + const t = length2 ? Math.max(0, Math.min(1, (((lon - a.lon) * scale) * dx + (lat - a.lat) * dy) / length2)) : 0; + const separation = haversineMiles(lat, lon, a.lat + t * dy, a.lon + t * (b.lon - a.lon)); + if (separation < best) { + best = separation; + distanceMi = a.distanceMi + t * (b.distanceMi - a.distanceMi); + } + } + const ele = Number(field('ele') || NaN); + const marker = { lat, lon, name: name || `Waypoint ${i + 1}`, description: field('desc') || field('cmt') || null, + ele: Number.isFinite(ele) ? ele : undefined, distanceMi }; + if (!markers.some(m => m.name === marker.name && m.lat === lat && m.lon === lon)) markers.push(marker); + } + } + return markers.sort((a, b) => a.distanceMi - b.distanceMi); +} diff --git a/src/lib/gpxStorage.ts b/src/lib/gpxStorage.ts index 05f58e8..b96161f 100644 --- a/src/lib/gpxStorage.ts +++ b/src/lib/gpxStorage.ts @@ -7,7 +7,7 @@ */ import { File } from 'expo-file-system'; -import { supabase } from './supabase'; +import { requireSupabase } from './supabase'; const BUCKET = 'gpx-files'; const SIGNED_URL_TTL_SECONDS = 300; @@ -21,7 +21,7 @@ export const gpxStoragePath = (userId: string, eventId: string): string => export async function uploadGpx(userId: string, eventId: string, localFile: File): Promise { const path = gpxStoragePath(userId, eventId); const body = await localFile.text(); - const { error } = await supabase.storage.from(BUCKET).upload(path, body, { + const { error } = await requireSupabase().storage.from(BUCKET).upload(path, body, { contentType: 'application/gpx+xml', upsert: true, }); @@ -30,7 +30,7 @@ export async function uploadGpx(userId: string, eventId: string, localFile: File } export async function downloadGpx(path: string, destination: File): Promise { - const { data, error } = await supabase.storage + const { data, error } = await requireSupabase().storage .from(BUCKET) .createSignedUrl(path, SIGNED_URL_TTL_SECONDS); if (error || !data?.signedUrl) throw error ?? new Error('Failed to create signed URL'); @@ -40,6 +40,6 @@ export async function downloadGpx(path: string, destination: File): Promise { - const { error } = await supabase.storage.from(BUCKET).remove([path]); + const { error } = await requireSupabase().storage.from(BUCKET).remove([path]); if (error) throw error; } diff --git a/src/lib/importGpx.ts b/src/lib/importGpx.ts new file mode 100644 index 0000000..3971cfa --- /dev/null +++ b/src/lib/importGpx.ts @@ -0,0 +1,28 @@ +import { normalizeCourseXml } from './courseXml'; +import { Directory, File, Paths } from 'expo-file-system'; +import { computeRouteMetrics, GpxRouteStats, parseGpx, parseGpxCheckpoints } from './gpx'; + +/** Stage a validated local route; publish its reference only after persistence succeeds. */ +export async function importLocalGpx( + sourceUri: string, + eventId: string, + save: (uri: string, stats: GpxRouteStats) => Promise, +): Promise { + const source = new File(sourceUri); + if (source.size > 10 * 1024 * 1024) throw new Error('Choose a GPX file smaller than 10 MB.'); + const xml = normalizeCourseXml(await source.text()); + const metrics = computeRouteMetrics(parseGpx(xml)); + if (!metrics) throw new Error('This GPX needs at least two valid course points.'); + const dir = new Directory(Paths.document, 'gpx'); + dir.create({ intermediates: true, idempotent: true }); + const dest = new File(dir, `${eventId}-${Date.now()}-${Math.random().toString(36).slice(2)}.gpx`); + dest.write(xml); + // Keep staged data recoverable if a journaled save fails midway. + await save(dest.uri, { + checkpoints: parseGpxCheckpoints(xml, metrics), + totalDistanceMi: metrics.totalDistanceMi, + elevationGainFt: metrics.elevationGainFt, + elevationLossFt: metrics.elevationLossFt, + }); + return dest.uri; +} diff --git a/src/lib/importGpxPlan.ts b/src/lib/importGpxPlan.ts new file mode 100644 index 0000000..dfbdf50 --- /dev/null +++ b/src/lib/importGpxPlan.ts @@ -0,0 +1,38 @@ +import type { Checkpoint, Event } from './database.types'; +import { eventStatsFromRoute, FEET_PER_METER, GpxRouteStats } from './gpx'; +import { commitPlanWrites, readArray, readCheckpointMap, runLocalPlanOperation } from './localPlanStorage'; + +/** Save route and extracted checkpoints together, preserving all existing planning details. */ +export async function saveGpxPlan(eventId: string, uri: string, stats: GpxRouteStats): Promise { + return runLocalPlanOperation(async () => { + const events = await readArray('@ultraedge/events'); + const event = events.find(e => e.id === eventId); + if (!event) throw new Error('This event was deleted. Refresh before importing.'); + const map = await readCheckpointMap(); + const existing = map[eventId] || []; + const next = [...existing]; + const now = new Date().toISOString(); + for (const marker of stats.checkpoints || []) { + if (next.some(cp => cp.name === marker.name && cp.latitude != null && cp.longitude != null && Math.abs(cp.latitude - marker.lat) < 0.00001 && Math.abs(cp.longitude - marker.lon) < 0.00001)) continue; + next.push({ + id: `cp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`, + event_id: eventId, name: marker.name, checkpoint_type: 'other', order_index: Math.max(-1, ...next.map(cp => cp.order_index)) + 1, + distance_from_start: marker.distanceMi, elevation: marker.ele == null ? null : marker.ele * FEET_PER_METER, + latitude: marker.lat, longitude: marker.lon, location_description: marker.description, + cutoff_time: null, cutoff_duration: null, estimated_arrival: null, estimated_duration: null, + has_crew_access: false, has_drop_bag: false, has_pacer_pickup: false, has_pacer_dropoff: false, + aid_supplies: [], notes: 'Imported from GPX. Course distance is estimated; verify the correct visit on loops or out-and-back routes. Confirm checkpoint type, access and cutoffs with the race organizer.', + created_at: now, updated_at: now, + }); + } + // Preserve established checkpoint ordering; new markers arrive in course order. + map[eventId] = next; + await commitPlanWrites([ + ['ultraedge_checkpoints', JSON.stringify(map)], + ['@ultraedge/events', JSON.stringify(events.map(e => e.id === eventId ? { + ...e, ...eventStatsFromRoute(stats, e.distance_unit, e.elevation_unit), gpx_file_url: uri, updated_at: now, + } : e))], + ]); + return next.length - existing.length; + }); +} diff --git a/src/lib/liveRaceModel.ts b/src/lib/liveRaceModel.ts new file mode 100644 index 0000000..6193d8a --- /dev/null +++ b/src/lib/liveRaceModel.ts @@ -0,0 +1,32 @@ +import { Checkpoint, Event } from './database.types'; +import { RaceOperations, TimeReport, projectRace, raceStart } from './raceOperations'; +export interface LiveSnapshot { event: Event; checkpoints: Checkpoint[]; operations: RaceOperations; startAt: string; logistics: string[]; } +export interface LiveReport { authorId: string; checkpointId: string; kind: 'in'|'out'; at: string|null; revision: number; receivedAt: string; } +export interface LiveRoom { id: string; ownerId: string; snapshot: LiveSnapshot; revision: number; invite: string|null; serverTime: string; members: {id:string;name:string}[]; reports: LiveReport[]; } +export interface PendingReport { requestId:string; roomId:string; authorId:string; checkpointId:string; kind:'in'|'out'; at:string|null; revision:number; } +export function liveOperations(room: LiveRoom): RaceOperations { + const reports: TimeReport[] = []; + for (const cp of room.snapshot.checkpoints) for (const kind of ['in','out'] as const) for (const source of ['runner','crew'] as const) { + const rows = room.reports.filter(r => r.checkpointId===cp.id && r.kind===kind && r.at && (source==='runner' ? r.authorId===room.ownerId : r.authorId!==room.ownerId)); + if (rows.length) reports.push({checkpointId:cp.id,kind,source,at:new Date(rows.reduce((n,r)=>n+Date.parse(r.at!),0)/rows.length).toISOString(),recordedAt:rows.map(r=>r.receivedAt).sort().pop()!}); + } + return {...room.snapshot.operations,startAt:room.snapshot.startAt,reports}; +} +export function liveProjection(room: LiveRoom) { + const ops=liveOperations(room);let prior=Date.parse(room.snapshot.startAt); + const projection=projectRace(room.snapshot.event,room.snapshot.checkpoints,ops); + // Keep conflicting evidence visible but do not present an impossible itinerary. + for(const row of projection?.rows??[]) for(const actual of [row.actualIn,row.actualOut]) if(actual!==null) { if(actual({id:cp.id,event_id:event.id,name:cp.name,order_index:cp.order_index,distance_from_start:cp.distance_from_start,estimated_duration:cp.estimated_duration,has_crew_access:cp.has_crew_access} as Checkpoint)); + return {event:safeEvent,checkpoints:safeCheckpoints,operations:{id:operations.id,variation:operations.variation,startAt:new Date(start).toISOString(),stops:{...operations.stops},duties:[],vehicles:[],cargo:[],reports:[]},startAt:new Date(start).toISOString(),logistics}; +} +export function nextReportRevision(room:LiveRoom,pending:PendingReport[],authorId:string,checkpointId:string,kind:'in'|'out'):number { + return Math.max(room.reports.find(r=>r.authorId===authorId&&r.checkpointId===checkpointId&&r.kind===kind)?.revision??0,...pending.filter(r=>r.roomId===room.id&&r.authorId===authorId&&r.checkpointId===checkpointId&&r.kind===kind).map(r=>r.revision+1)); +} diff --git a/src/lib/liveRaceSync.ts b/src/lib/liveRaceSync.ts new file mode 100644 index 0000000..48e9e6b --- /dev/null +++ b/src/lib/liveRaceSync.ts @@ -0,0 +1,72 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import * as Crypto from 'expo-crypto'; +import { createSecureStorageAdapter } from './supabase'; +import { runLocalPlanOperation } from './localPlanStorage'; +import { LiveRoom, LiveSnapshot, PendingReport, nextReportRevision } from './liveRaceModel'; +const KEY='@ultraedge/live-rooms-v1'; +export interface LiveCache { rooms:LiveRoom[]; pending:PendingReport[]; actorId:string|null; lastSync:Record; } +let client:SupabaseClient|undefined; +function connection(){ + const url=process.env.EXPO_PUBLIC_SUPABASE_URL||''; const key=process.env.EXPO_PUBLIC_SUPABASE_KEY||process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY||''; + if(!url||!key)throw new Error('Live collaboration is not configured in this build.'); + return client??=createClient(url,key,{global:{fetch:async(input,init)=>{const controller=new AbortController();const timer=setTimeout(()=>controller.abort(),15000);const abort=()=>controller.abort();init?.signal?.addEventListener('abort',abort);try{return await fetch(input,{...init,signal:controller.signal});}finally{clearTimeout(timer);init?.signal?.removeEventListener('abort',abort);}}},auth:{storage:createSecureStorageAdapter(),storageKey:'ultraedge-live-auth',persistSession:true,autoRefreshToken:true,detectSessionInUrl:false}}); +} +export async function readLiveCache():Promise{const raw=await AsyncStorage.getItem(KEY);return raw?JSON.parse(raw):{rooms:[],pending:[],actorId:null,lastSync:{}};} +async function editCache(change:(c:LiveCache)=>LiveCache){return runLocalPlanOperation(async()=>{const next=change(await readLiveCache());await AsyncStorage.setItem(KEY,JSON.stringify(next));return next;});} +async function identify(){ + const c=connection(); let {data:{session},error}=await c.auth.getSession();if(error)throw error; + if(!session){const result=await c.auth.signInAnonymously();if(result.error)throw result.error;session=result.data.session;} + if(!session)throw new Error('Could not establish a private device identity.'); + const cache=await readLiveCache(); + if(cache.actorId&&cache.actorId!==session.user.id)throw new Error('This device identity changed. Recover or discard saved reports before reconnecting.'); + await editCache(c=>({...c,actorId:session!.user.id}));return session.user.id; +} +async function rpc(action:string,args:object={}):Promise{const {data,error}=await connection().rpc('ue_live',{action,args});if(error)throw error;return data as T;} +export async function createLiveRoom(snapshot:LiveSnapshot,name:string){await identify();const {id}=await rpc<{id:string}>('create',{snapshot,localId:snapshot.event.id,name});return syncLiveRoom(id);} +export async function joinLiveRoom(code:string,name:string){await identify();const {id}=await rpc<{id:string}>('join',{code:code.trim(),name});return syncLiveRoom(id);} +let syncing:Promise|null=null; +let maintenance=false; +async function exclusive(work:()=>Promise):Promise{ + if(maintenance)throw new Error('Another sharing change is finishing. Please retry.'); + maintenance=true;try{await syncing?.catch(()=>{});return await work();}finally{maintenance=false;} +} +export async function syncLiveRoom(id:string):Promise{ + if(maintenance)throw new Error('Sharing access is being updated.'); + if(syncing){await syncing.catch(()=>{});return syncLiveRoom(id);} + const work=async()=>{ + const actor=await identify(); + // Check current membership before uploading. Revoked access leaves unsent reports intact locally. + let room=await rpc('read',{roomId:id}); + await editCache(c=>({...c,rooms:[...c.rooms.filter(r=>r.id!==id),room]})); + const pending=(await readLiveCache()).pending.filter(r=>r.roomId===id); + for(const report of pending){ + if(report.authorId!==actor)throw new Error('Pending report belongs to another device identity.'); + await rpc('report',report); + await editCache(c=>({...c,pending:c.pending.filter(r=>r.requestId!==report.requestId)})); + } + room=await rpc('read',{roomId:id}); + await editCache(c=>({...c,rooms:[...c.rooms.filter(r=>r.id!==id),room],lastSync:{...c.lastSync,[id]:new Date().toISOString()}}));return room; + }; + syncing=work();try{return await syncing;}finally{syncing=null;} +} +export async function queueLiveReport(room:LiveRoom,checkpointId:string,kind:'in'|'out',at:string|null){ + return editCache(c=>{ + if(!c.actorId)throw new Error('Join the race online before recording offline.'); + if(!room.snapshot.checkpoints.some(cp=>cp.id===checkpointId))throw new Error('Checkpoint no longer exists.'); + if(at&&(!Number.isFinite(Date.parse(at))||Date.parse(at)Date.now()+60000))throw new Error('Record a time between race start and now.'); + const fresh=c.rooms.find(r=>r.id===room.id)??room; + return {...c,pending:[...c.pending,{requestId:Crypto.randomUUID(),roomId:room.id,authorId:c.actorId,checkpointId,kind,at,revision:nextReportRevision(fresh,c.pending,c.actorId,checkpointId,kind)}]}; + }); +} +export async function publishLiveRoom(room:LiveRoom,snapshot:LiveSnapshot){await identify();await rpc('publish',{roomId:room.id,revision:room.revision,snapshot});return syncLiveRoom(room.id);} +export async function removeLiveAccess(room:LiveRoom,userId?:string){ + await exclusive(async()=>{await identify();const action=userId?'revoke':room.ownerId===(await readLiveCache()).actorId?'close':'leave';await rpc(action,{roomId:room.id,userId});if(!userId)await dropRoomCache(room.id);}); + return userId?syncLiveRoom(room.id):null; +} +async function dropRoomCache(id:string){await editCache(c=>({...c,rooms:c.rooms.filter(r=>r.id!==id),pending:c.pending.filter(r=>r.roomId!==id),lastSync:Object.fromEntries(Object.entries(c.lastSync).filter(([key])=>key!==id))}));} +export async function forgetLiveRoom(id:string){await exclusive(()=>dropRoomCache(id));} +export async function discardPendingReports(id:string){await editCache(c=>({...c,pending:c.pending.filter(r=>r.roomId!==id)}));} + +export async function recoverLiveRooms(){await identify();const list=await rpc<{id:string}[]>('list');for(const item of list)await syncLiveRoom(item.id);} +export async function deleteLiveIdentity(){await exclusive(async()=>{await identify();await rpc('delete_identity');try{await connection().auth.signOut({scope:'local'});}finally{await editCache(()=>({rooms:[],pending:[],actorId:null,lastSync:{}}));}});} diff --git a/src/lib/localGpxUri.ts b/src/lib/localGpxUri.ts new file mode 100644 index 0000000..517a51c --- /dev/null +++ b/src/lib/localGpxUri.ts @@ -0,0 +1,13 @@ +/** + * iOS can relocate an application's sandbox during an update or restore. + * Only rebase app-owned Documents/gpx files; never reinterpret arbitrary files. + */ +export function resolveLocalGpxUri(storedUri: string, documentUri: string): string { + const match = /^file:\/\/.*\/Documents\/gpx\/([^/?#]+)$/.exec(storedUri); + if (!match) return storedUri; + // Reject encoded traversal/separators before joining an untrusted saved name. + let filename: string; + try { filename = decodeURIComponent(match[1]); } catch { return storedUri; } + if (filename === '.' || filename === '..' || /[/\\\0]/.test(filename)) return storedUri; + return `${documentUri.replace(/\/$/, '')}/gpx/${match[1]}`; +} diff --git a/src/lib/localPlanStorage.ts b/src/lib/localPlanStorage.ts new file mode 100644 index 0000000..91163b5 --- /dev/null +++ b/src/lib/localPlanStorage.ts @@ -0,0 +1,151 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +const JOURNAL_KEY = '@ultraedge/pending-plan-write'; +type Entry = [string, string]; +let queue: Promise = Promise.resolve(); +const listeners = new Set<(keys: string[]) => void>(); + +/** Serialize read/modify/write operations, including recovery after partial writes. */ +export function runLocalPlanOperation(operation: () => Promise): Promise { + const result = queue.then(async () => { + await recoverPendingPlanWrite(); + return operation(); + }); + queue = result.catch(() => undefined); + return result; +} + +export function subscribePlanChanges(listener: (keys: string[]) => void): () => void { + listeners.add(listener); + return () => { listeners.delete(listener); }; +} + +export async function recoverPendingPlanWrite(): Promise { + const raw = await AsyncStorage.getItem(JOURNAL_KEY); + if (!raw) return; + const entries: Entry[] = JSON.parse(raw); + if (!Array.isArray(entries) || !entries.every(entry => Array.isArray(entry) && entry.length === 2 && entry.every(value => typeof value === 'string'))) { + throw new Error('Saved plan recovery data is invalid. Existing data has been preserved.'); + } + for (const [key, value] of entries) await AsyncStorage.setItem(key, value); + await AsyncStorage.removeItem(JOURNAL_KEY); + listeners.forEach(listener => listener(entries.map(([key]) => key))); +} + +/** Call inside runLocalPlanOperation. Journal remains until every write is durable. */ +export async function commitPlanWrites(entries: Entry[]): Promise { + await AsyncStorage.setItem(JOURNAL_KEY, JSON.stringify(entries)); + await recoverPendingPlanWrite(); +} + +export async function readArray(key: string): Promise { + const raw = await AsyncStorage.getItem(key); + if (raw === null) return []; + const value = JSON.parse(raw); + if (!Array.isArray(value)) throw new Error(`Invalid saved data: ${key}`); + return value; +} + +export async function readCheckpointMap(): Promise> { + const raw = await AsyncStorage.getItem('ultraedge_checkpoints'); + if (raw === null) return {}; + const value = JSON.parse(raw); + if (!value || Array.isArray(value) || typeof value !== 'object' || !Object.values(value).every(Array.isArray)) { + throw new Error('Invalid saved checkpoints. Existing data has been preserved.'); + } + return value; +} + +export async function deleteLocalEvent(eventId: string): Promise { + await runLocalPlanOperation(async () => { + const entries: Entry[] = []; + for (const key of ['@ultraedge/event-crew', '@ultraedge/event-gear', '@ultraedge/dropbags']) { + const rows = await readArray<{ eventId: string }>(key); + entries.push([key, JSON.stringify(rows.filter(row => row.eventId !== eventId))]); + } + const operations = await readArray<{ id: string }>('@ultraedge/race-operations'); + entries.push(['@ultraedge/race-operations', JSON.stringify(operations.filter(row => row.id !== eventId))]); + const checkpoints = await readCheckpointMap(); + delete checkpoints[eventId]; + entries.push(['ultraedge_checkpoints', JSON.stringify(checkpoints)]); + const events = await readArray<{ id: string }>('@ultraedge/events'); + // The event disappears only after all child writes have succeeded. + entries.push(['@ultraedge/events', JSON.stringify(events.filter(event => event.id !== eventId))]); + await commitPlanWrites(entries); + }); +} + +/** Merge this screen's changes into the latest disk state without erasing concurrent additions. */ +export async function saveArrayChanges(key: string, previous: T[], next: T[]): Promise { + return runLocalPlanOperation(async () => { + const current = await readArray(key); + const before = new Map(previous.map(row => [row.id, row])); + const after = new Map(next.map(row => [row.id, row])); + const merged = new Map(current.map(row => [row.id, row])); + for (const id of before.keys()) { + if (!after.has(id)) { + if (merged.has(id) && JSON.stringify(merged.get(id)) !== JSON.stringify(before.get(id))) throw new Error('This record changed. Refresh before deleting.'); + merged.delete(id); + } + } + for (const row of next) { + if (JSON.stringify(before.get(row.id)) === JSON.stringify(row)) continue; + if (before.has(row.id) && !merged.has(row.id)) throw new Error('This record was deleted. Refresh before editing.'); + if (before.has(row.id) && JSON.stringify(merged.get(row.id)) !== JSON.stringify(before.get(row.id))) throw new Error('This record changed. Refresh before editing.'); + if (key === '@ultraedge/dropbags') { + const bag = row as T & { eventId: string; checkpointId?: string | null }; + await requireLocalEvent(bag.eventId); + if (bag.checkpointId && !(await readCheckpointMap<{ id: string }>())[bag.eventId]?.some(cp => cp.id === bag.checkpointId)) throw new Error('This checkpoint was deleted. Choose another location.'); + } + merged.set(row.id, row); + } + const result = [...merged.values()]; + await AsyncStorage.setItem(key, JSON.stringify(result)); + return result; + }); +} + +/** Call within the operation lock before creating or changing a child record. */ +export async function requireLocalEvent(eventId: string): Promise { + const events = await readArray<{ id: string }>('@ultraedge/events'); + if (!events.some(event => event.id === eventId)) throw new Error('This event was deleted. Refresh before editing.'); +} + +export async function deleteLocalCrewMember(memberId: string): Promise { + await runLocalPlanOperation(async () => { + const members = await readArray<{ id: string }>('@ultraedge/crew'); + const assignments = await readArray<{ crewMemberId: string }>('@ultraedge/event-crew'); + await commitPlanWrites([ + ['@ultraedge/event-crew', JSON.stringify(assignments.filter(row => row.crewMemberId !== memberId))], + ['@ultraedge/crew', JSON.stringify(members.filter(row => row.id !== memberId))], + ]); + }); +} + +/** Keep packed bags, but remove their location when a checkpoint is deleted. */ +export async function deleteLocalCheckpoints(eventId: string, checkpointId?: string): Promise { + await runLocalPlanOperation(async () => { + const checkpoints = await readCheckpointMap<{ id: string; order_index: number }>(); + const bags = await readArray<{ eventId: string; checkpointId: string | null }>('@ultraedge/dropbags'); + if (checkpointId === undefined) delete checkpoints[eventId]; + else checkpoints[eventId] = (checkpoints[eventId] || []).filter(cp => cp.id !== checkpointId) + .sort((a, b) => a.order_index - b.order_index).map((cp, index) => ({ ...cp, order_index: index })); + await commitPlanWrites([ + ['@ultraedge/dropbags', JSON.stringify(bags.map(bag => bag.eventId === eventId && (checkpointId === undefined || bag.checkpointId === checkpointId) ? { ...bag, checkpointId: null } : bag))], + ['ultraedge_checkpoints', JSON.stringify(checkpoints)], + ]); + }); +} + +export async function deleteLocalGear(gearItemId: string): Promise { + await runLocalPlanOperation(async () => { + const gear = await readArray<{ id: string }>('@ultraedge/gear-items'); + const allocations = await readArray<{ gearItemId: string }>('@ultraedge/event-gear'); + const bags = await readArray<{ items: { refId: string }[] }>('@ultraedge/dropbags'); + await commitPlanWrites([ + ['@ultraedge/event-gear', JSON.stringify(allocations.filter(row => row.gearItemId !== gearItemId))], + ['@ultraedge/dropbags', JSON.stringify(bags.map(bag => ({ ...bag, items: bag.items.filter(item => item.refId !== gearItemId) })))], + ['@ultraedge/gear-items', JSON.stringify(gear.filter(row => row.id !== gearItemId))], + ]); + }); +} diff --git a/src/lib/mandatoryGear.ts b/src/lib/mandatoryGear.ts new file mode 100644 index 0000000..e01fcf6 --- /dev/null +++ b/src/lib/mandatoryGear.ts @@ -0,0 +1,25 @@ +import { Event } from './database.types'; +import { EventGearAllocation } from './eventGear'; +export type GearRequirement = NonNullable[number]; +export function requirementStatus( + requirement: GearRequirement, + allocations: EventGearAllocation[], + activeGearIds: string[], +) { + const selected = allocations.filter( + (a) => + requirement.gearIds.includes(a.gearItemId) && + activeGearIds.includes(a.gearItemId) && + (a.isCarried || a.isWorn), + ); + const assigned = selected.reduce((sum, a) => sum + a.quantity, 0); + const packed = selected + .filter((a) => a.isPacked) + .reduce((sum, a) => sum + a.quantity, 0); + return { + assigned, + packed, + missing: Math.max(0, requirement.quantity - packed), + ready: packed >= requirement.quantity, + }; +} diff --git a/src/lib/onboarding.ts b/src/lib/onboarding.ts new file mode 100644 index 0000000..a41cb12 --- /dev/null +++ b/src/lib/onboarding.ts @@ -0,0 +1,24 @@ +/** + * Onboarding persistence — first-launch flag stored locally. + */ + +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export const ONBOARDING_COMPLETE_KEY = '@ultraedge/onboarding-complete'; + +export async function isOnboardingComplete(): Promise { + try { + return (await AsyncStorage.getItem(ONBOARDING_COMPLETE_KEY)) === 'true'; + } catch { + // If storage is unreadable, don't trap the user in onboarding + return true; + } +} + +export async function setOnboardingComplete(): Promise { + try { + await AsyncStorage.setItem(ONBOARDING_COMPLETE_KEY, 'true'); + } catch (err) { + console.error('Failed to persist onboarding flag:', err); + } +} diff --git a/src/lib/raceOperations.ts b/src/lib/raceOperations.ts new file mode 100644 index 0000000..7e6ef93 --- /dev/null +++ b/src/lib/raceOperations.ts @@ -0,0 +1,179 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Checkpoint, Event } from './database.types'; +import { readArray, readCheckpointMap, requireLocalEvent, runLocalPlanOperation } from './localPlanStorage'; +import { parseDateOnly } from './dateOnly'; + +export const OPERATIONS_KEY = '@ultraedge/race-operations'; +export interface StationDuty { checkpointId: string; crewMemberId: string; role: 'support' | 'first_aid' | 'pacer'; } +export interface RaceVehicle { id: string; name: string; ownerId: string | null; crewIds: string[]; } +export interface CargoAllocation { key: string; label: string; vehicleId: string; } +export interface TimeReport { checkpointId: string; source: 'runner' | 'crew'; kind: 'in' | 'out'; at: string; recordedAt: string; } +export interface RaceOperations { + id: string; + variation: number; + startAt: string | null; + stops: Record; + duties: StationDuty[]; + vehicles: RaceVehicle[]; + cargo: CargoAllocation[]; + reports: TimeReport[]; +} +export const emptyOperations = (id: string): RaceOperations => ({ id, variation: 10, startAt: null, stops: {}, duties: [], vehicles: [], cargo: [], reports: [] }); +async function cleanOperations(ops: RaceOperations): Promise { + const cps = new Set(((await readCheckpointMap())[ops.id] ?? []).map(cp => cp.id)); + const crew = new Set((await readArray<{ eventId: string; crewMemberId: string }>('@ultraedge/event-crew')).filter(row => row.eventId === ops.id).map(row => row.crewMemberId)); + const bags = (await readArray<{ id: string; eventId: string; items: { id: string }[] }>('@ultraedge/dropbags')).filter(row => row.eventId === ops.id); + const gear = (await readArray<{ eventId: string; gearItemId: string }>('@ultraedge/event-gear')).filter(row => row.eventId === ops.id); + const keys = new Set([...bags.flatMap(bag => [`bag:${bag.id}`, ...bag.items.map(item => `item:${bag.id}:${item.id}`)]), ...gear.map(item => `gear:${item.gearItemId}`)]); + return { ...ops, stops: Object.fromEntries(Object.entries(ops.stops).filter(([id]) => cps.has(id))), + reports: ops.reports.filter(row => cps.has(row.checkpointId)), duties: ops.duties.filter(row => cps.has(row.checkpointId) && crew.has(row.crewMemberId)), + vehicles: ops.vehicles.map(v => ({ ...v, ownerId: v.ownerId && crew.has(v.ownerId) ? v.ownerId : null, crewIds: v.crewIds.filter(id => crew.has(id)) })), + cargo: ops.cargo.filter(row => keys.has(row.key) && ops.vehicles.some(v => v.id === row.vehicleId)) }; +} +export async function loadOperations(id: string): Promise { + return runLocalPlanOperation(async () => cleanOperations((await readArray(OPERATIONS_KEY)).find(row => row.id === id) ?? emptyOperations(id))); +} +export async function changeOperations(id: string, change: (current: RaceOperations) => RaceOperations): Promise { + return runLocalPlanOperation(async () => { + await requireLocalEvent(id); + const rows = await readArray(OPERATIONS_KEY); + const next = await cleanOperations(change(await cleanOperations(rows.find(row => row.id === id) ?? emptyOperations(id)))); + if (!Number.isFinite(next.variation) || next.variation < 0 || next.variation > 50) throw new Error('Choose a variation from 0 to 50%.'); + if (Object.values(next.stops).some(value => !Number.isFinite(value) || value < 0 || value > 1440)) throw new Error('Stop durations must be between 0 and 1440 minutes.'); + const event = (await readArray('@ultraedge/events')).find(event => event.id === id)!; + const start = raceStart(event, next); + if (next.startAt && (start === null || start > Date.now())) throw new Error('Actual start must be a valid time no later than now.'); + let previous = start; + for (const cp of [...((await readCheckpointMap())[id] ?? [])].sort((a,b) => a.order_index - b.order_index)) { + for (const kind of ['in', 'out'] as const) { + const average = reportAverage(next.reports, cp.id, kind); + if (average !== null) { + if (!Number.isFinite(average) || previous === null || average < previous) throw new Error('Time reports must follow the start and checkpoint order. Correct the conflicting reports first.'); + previous = average; + } + } + } + await AsyncStorage.setItem(OPERATIONS_KEY, JSON.stringify([...rows.filter(row => row.id !== id), next])); + return next; + }); +} +export function durationMinutes(value: string | null): number | null { + if (!value) return null; + const match = /^(\d+):([0-5]\d)(?::([0-5]\d))?$/.exec(value.trim()); + return match ? Number(match[1]) * 60 + Number(match[2]) + Number(match[3] || 0) / 60 : null; +} +export function raceStart(event: Event, ops: RaceOperations): number | null { + if (ops.startAt) return Number.isFinite(Date.parse(ops.startAt)) ? Date.parse(ops.startAt) : null; + const date = parseDateOnly(event.event_date); + const match = /^(\d{1,2}):([0-5]\d)(?::[0-5]\d)?$/.exec(event.event_time || ''); + if (!date || !match || Number(match[1]) > 23) return null; + date.setHours(Number(match[1]), Number(match[2]), 0, 0); + return date.getTime(); +} +export function reportAverage(reports: TimeReport[], checkpointId: string, kind: TimeReport['kind']): number | null { + const rows = reports.filter(row => row.checkpointId === checkpointId && row.kind === kind); + if (!rows.length) return null; + return rows.reduce((sum, row) => sum + Date.parse(row.at), 0) / rows.length; +} +export function putTimeReport(ops: RaceOperations, report: TimeReport, checkpoints: Checkpoint[], start: number | null): RaceOperations { + const at = Date.parse(report.at); + if (!Number.isFinite(at) || start === null || at < start || at > Date.now() + 60000) throw new Error('Record a time between race start and now.'); + if (!checkpoints.some(cp => cp.id === report.checkpointId)) throw new Error('Checkpoint no longer exists.'); + const reports = [...ops.reports.filter(row => !(row.checkpointId === report.checkpointId && row.source === report.source && row.kind === report.kind)), report]; + for (const source of ['runner', 'crew'] as const) { + const arrival = reports.find(row => row.checkpointId === report.checkpointId && row.source === source && row.kind === 'in'); + const departure = reports.find(row => row.checkpointId === report.checkpointId && row.source === source && row.kind === 'out'); + if (arrival && departure && Date.parse(departure.at) < Date.parse(arrival.at)) throw new Error('Departure cannot precede arrival for the same reporter.'); + } + let previous = start; + for (const cp of [...checkpoints].sort((a,b) => a.order_index - b.order_index)) { + for (const kind of ['in', 'out'] as const) { + const average = reportAverage(reports, cp.id, kind); + if (average !== null) { + if (average < previous) throw new Error('These times put a departure before arrival or a later station before an earlier one. Correct the report first.'); + previous = average; + } + } + } + return { ...ops, reports }; +} +export interface ProjectionRow { id: string; name: string; distance: number; arrival: number; departure: number; best: number; slow: number; actualIn: number | null; actualOut: number | null; stop: number; } +export interface Projection { rows: ProjectionRow[]; start: number; movingMinutes: number; finish: number; bestFinish: number; slowFinish: number; stopMinutes: number; lastReport: string | null; } +export function projectRace(event: Event, checkpoints: Checkpoint[], ops: RaceOperations): Projection | null { + const start = raceStart(event, ops); + const moving = durationMinutes(event.target_time); + const total = event.total_distance; + if (start === null || !moving || !total || !Number.isFinite(total) || total <= 0) return null; + const cps = [...checkpoints].sort((a,b) => a.order_index - b.order_index); + let lastDistance = 0; + if (cps.some(cp => { const distance = cp.distance_from_start; const invalid = distance === null || !Number.isFinite(distance) || distance < lastDistance || distance > total; lastDistance = distance ?? lastDistance; return invalid; })) return null; + // Anchor remaining travel at the latest observation, excluding station dwell from observed moving pace. + let anchorTime = start, anchorDistance = 0, observedStops = 0, cumulativeStops = 0, anchorStop = 0, anchorIndex = -1; + let scale = 1; + cps.forEach((cp, index) => { + const actualIn = reportAverage(ops.reports, cp.id, 'in'); + const actualOut = reportAverage(ops.reports, cp.id, 'out'); + const stop = ops.stops[cp.id] ?? durationMinutes(cp.estimated_duration) ?? 0; + const dwell = actualIn !== null && actualOut !== null ? (actualOut - actualIn) / 60000 : stop; + if (actualIn !== null || actualOut !== null) { + anchorIndex = index; + anchorDistance = cp.distance_from_start!; + anchorTime = actualOut ?? actualIn!; + anchorStop = actualOut !== null ? 0 : stop; + const baseline = moving * anchorDistance / total; + if (baseline > 0) scale = Math.max(0.01, ((anchorTime - start) / 60000 - observedStops - (actualOut !== null ? dwell : 0)) / baseline); + } + observedStops += dwell; + }); + let plannedElapsed = 0, previousDistance = 0; + let futureStops = anchorStop; + const rows = cps.map((cp, index) => { + const distance = cp.distance_from_start!; + const stop = ops.stops[cp.id] ?? durationMinutes(cp.estimated_duration) ?? 0; + const actualIn = reportAverage(ops.reports, cp.id, 'in'); + const actualOut = reportAverage(ops.reports, cp.id, 'out'); + plannedElapsed += moving * (distance - previousDistance) / total; + let arrival = start + plannedElapsed * 60000, best = start + (plannedElapsed - cumulativeStops) * (1 - ops.variation / 100) * 60000 + cumulativeStops * 60000; + let slow = start + (plannedElapsed - cumulativeStops) * (1 + ops.variation / 100) * 60000 + cumulativeStops * 60000; + if (index > anchorIndex) { + const travel = moving * (distance - anchorDistance) / total * scale; + arrival = anchorTime + (travel + futureStops) * 60000; + best = anchorTime + (travel * (1 - ops.variation / 100) + futureStops) * 60000; + slow = anchorTime + (travel * (1 + ops.variation / 100) + futureStops) * 60000; + futureStops += stop; + } + if (actualIn !== null) arrival = best = slow = actualIn; + const departure = actualOut ?? arrival + stop * 60000; + plannedElapsed += stop; cumulativeStops += stop; previousDistance = distance; + return { id: cp.id, name: cp.name, distance, arrival, departure, best, slow, actualIn, actualOut, stop }; + }); + const remainingTravel = moving * (total - anchorDistance) / total * scale; + const finish = anchorTime + (remainingTravel + futureStops) * 60000; + return { rows, start, movingMinutes: moving, finish, bestFinish: anchorTime + (remainingTravel * (1 - ops.variation / 100) + futureStops) * 60000, slowFinish: anchorTime + (remainingTravel * (1 + ops.variation / 100) + futureStops) * 60000, stopMinutes: cumulativeStops, lastReport: ops.reports.map(row => row.recordedAt).sort().pop() ?? null }; +} +export function clockLabel(time: number): string { + return new Date(time).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); +} + +/** Interpolate progress through travel segments; remain at the station during its dwell. */ +export function projectedDistance(projection: Projection, total: number, time: number, scenario: 'best' | 'arrival' | 'slow' = 'arrival'): number { + let previousTime = projection.start, previousDistance = 0; + for (const row of projection.rows) { + const arrival = row[scenario]; + if (time < arrival) return Math.max(0, previousDistance + (row.distance - previousDistance) * Math.max(0, time - previousTime) / Math.max(1, arrival - previousTime)); + const departure = row.actualOut ?? arrival + row.stop * 60000; + if (time <= departure) return row.distance; + previousTime = departure; previousDistance = row.distance; + } + const finish = scenario === 'best' ? projection.bestFinish : scenario === 'slow' ? projection.slowFinish : projection.finish; + return Math.min(total, previousDistance + (total - previousDistance) * Math.max(0, time - previousTime) / Math.max(1, finish - previousTime)); +} + +export function parseLocalRaceTime(value: string): string { + const match = /^(\d{4}-\d{2}-\d{2})T([01]\d|2[0-3]):([0-5]\d)$/.exec(value.trim()); + const date = match ? parseDateOnly(match[1]) : null; + if (!date || !match) throw new Error('Use a valid local time in YYYY-MM-DDTHH:MM format.'); + date.setHours(Number(match[2]), Number(match[3]), 0, 0); + if (date.getHours() !== Number(match[2]) || date.getMinutes() !== Number(match[3])) throw new Error('That local time does not exist due to a clock change.'); + return date.toISOString(); +} diff --git a/src/lib/racePlanPdf.ts b/src/lib/racePlanPdf.ts index 5346f6f..74c659f 100644 --- a/src/lib/racePlanPdf.ts +++ b/src/lib/racePlanPdf.ts @@ -1,3 +1,6 @@ +import { RaceOperations, projectRace, clockLabel } from './raceOperations'; +import { resolveLocalGpxUri } from './localGpxUri'; +import { parseDateOnly } from './dateOnly'; /** * Race Plan PDF generation. * @@ -46,6 +49,7 @@ export interface RacePlanGearItem { quantity: number; isWorn?: boolean; isCarried?: boolean; + isPacked?: boolean; notes?: string | null; } @@ -70,6 +74,8 @@ export interface RacePlanData { dropBags: RacePlanDropBag[]; /** Raw GPX XML for the course, when the event has one. */ gpxXml?: string | null; + operations?: RaceOperations; + operationsLogistics?: string[]; } // ============================================================================ @@ -119,9 +125,8 @@ function fmtStoredElevation(value: number, unit: ElevationUnit): string { function fmtEventDate(eventDate: string | null): string | null { if (!eventDate) {return null;} - const iso = /^\d{4}-\d{2}-\d{2}$/.test(eventDate) ? `${eventDate}T00:00:00` : eventDate; - const date = new Date(iso); - if (isNaN(date.getTime())) {return eventDate;} + const date = parseDateOnly(eventDate); + if (!date) {return eventDate;} return date.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', @@ -383,7 +388,11 @@ function buildCover(data: RacePlanData, metrics: RouteMetrics | null): string { } function buildRouteSection(data: RacePlanData, metrics: RouteMetrics | null): string { - if (!metrics) {return '';} + if (!metrics) { + return data.event.gpx_file_url || data.gpxXml + ? '

Course Route Unavailable

The saved course could not be loaded. This plan does not include its route map or elevation profile.

' + : ''; + } const { event } = data; const map = buildRouteMapSvg(metrics, data.checkpoints); const profile = buildElevationSvg(metrics, event.distance_unit, event.elevation_unit); @@ -400,7 +409,7 @@ function buildRouteSection(data: RacePlanData, metrics: RouteMetrics | null): st
${sectionHeader(ICONS.route, 'Course Route')} ${map}${map ? legend : ''} - ${profile ? `${sectionHeader(ICONS.elevation, 'Elevation Profile')}${profile}` : ''} + ${profile ? `` : ''}
`; } @@ -503,12 +512,14 @@ function buildGearSection(data: RacePlanData): string { .map(item => { const tags: string[] = []; if (item.isWorn) {tags.push('worn');} - if (item.isCarried) {tags.push('carried');} + if (item.isCarried) {tags.push('in vest');} + tags.push(item.isPacked ? 'packed' : 'not packed'); const qty = item.quantity > 1 ? ` ×${item.quantity}` : ''; return ( - '
' + + `
${item.isPacked ? '✓' : ''}` + `${escapeHtml(item.name)}${item.brand ? ` ${escapeHtml(item.brand)}` : ''}${qty}` + - `${tags.length > 0 ? ` ${tags.join(' · ')}` : ''}
` + `${tags.length > 0 ? ` ${tags.join(' · ')}` : ''}` + + `${item.notes ? `
${escapeHtml(item.notes)}` : ''}
` ); }) .join(''); @@ -525,7 +536,7 @@ function buildGearSection(data: RacePlanData): string { } function buildDropBagsSection(data: RacePlanData): string { - const bags = data.dropBags.filter(bag => bag.items.length > 0 || bag.notes); + const bags = data.dropBags; if (bags.length === 0) {return '';} const cards = bags @@ -540,14 +551,14 @@ function buildDropBagsSection(data: RacePlanData): string { return `
${escapeHtml(bag.name)}
${bag.checkpointName ? `
at ${escapeHtml(bag.checkpointName)}
` : ''} - ${items ? `
    ${items}
` : ''} + ${items ? `
    ${items}
` : '
No items listed.
'} ${bag.notes ? `
${escapeHtml(bag.notes)}
` : ''}
`; }) .join(''); return ` -
+ `; @@ -639,9 +650,14 @@ const STYLES = ` .map-legend span { display: flex; align-items: center; gap: 5px; } .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } + /* Inline blocks are atomic in the iOS print formatter, which may ignore + break-inside on flex boxes. Keep a chart title and its graphic together. */ + .print-block { display: inline-block; width: 100%; break-inside: avoid; page-break-inside: avoid; } .gear-grid { column-count: 2; column-gap: 24px; } .gear-group { break-inside: avoid; page-break-inside: avoid; margin-bottom: 14px; } - .gear-item { display: flex; align-items: flex-start; gap: 7px; padding: 3px 0; font-size: 11px; } + .gear-item { display: inline-block; width: 100%; padding: 3px 0; font-size: 11px; break-inside: avoid; page-break-inside: avoid; } + .gear-name { display: inline-block; width: calc(100% - 23px); vertical-align: top; } + .gear-item .checkbox { display: inline-block; margin-right: 7px; vertical-align: top; } .checkbox { width: 11px; height: 11px; flex: none; margin-top: 2px; border: 1.5px solid ${TRAIL}; border-radius: 3px; background: #FFFFFF; @@ -670,8 +686,24 @@ const STYLES = ` } `; +function buildOperationsSection(data: RacePlanData): string { + if (!data.operations) return ''; + const projection = projectRace(data.event, data.checkpoints, data.operations); + const rows = projection?.rows.map(row => `${escapeHtml(row.name)}${escapeHtml(clockLabel(row.best))}${escapeHtml(clockLabel(row.arrival))}${escapeHtml(clockLabel(row.slow))}${row.stop} min${escapeHtml(clockLabel(row.departure))}`).join('') ?? ''; + return `

Race timeline & logistics

Target is moving time; stops add to finish. Distance-based projections, ±${data.operations.variation}%. Times use the exporting device’s timezone. Offline reports on this device.

+ ${projection ? `

Expected finish: ${escapeHtml(clockLabel(projection.finish))}. Planned stops: ${projection.stopMinutes} minutes. ${projection.lastReport ? 'Last report: ' + escapeHtml(clockLabel(Date.parse(projection.lastReport))) : 'No actual reports yet.'}

${rows}
StationBestExpected inSlowStopOut
` : '

Complete race timing and checkpoint distances to calculate ETAs.

'} + ${(data.operationsLogistics ?? []).map(line => `

${escapeHtml(line)}

`).join('')} + ${data.operations.reports.map(report => `

${escapeHtml(data.checkpoints.find(cp => cp.id === report.checkpointId)?.name ?? 'Checkpoint')} — ${report.source} ${report.kind}: ${escapeHtml(clockLabel(Date.parse(report.at)))}

`).join('')} +
`; +} + /** Assemble the complete printable HTML document. */ export function buildRacePlanHtml(data: RacePlanData): string { + const projection = data.operations ? projectRace(data.event, data.checkpoints, data.operations) : null; + if (projection) data = { ...data, checkpoints: data.checkpoints.map(cp => { + const row = projection.rows.find(row => row.id === cp.id); + return row ? { ...cp, estimated_arrival: clockLabel(row.arrival) } : cp; + }) }; let metrics: RouteMetrics | null = null; if (data.gpxXml) { try { @@ -699,7 +731,10 @@ export function buildRacePlanHtml(data: RacePlanData): string { ${buildCover(data, metrics)} ${buildRouteSection(data, metrics)} ${buildCheckpointsSection(data)} +${buildOperationsSection(data)} ${buildCrewSection(data)} +${data.event.mandatoryGear?.length ? `

Mandatory race equipment

Requirements entered for this race. Verify against the official guide.

${data.event.mandatoryGear.map(r => `

□ ${escapeHtml(r.name)} ×${r.quantity}

`).join('')}
` : ''} +${data.event.raceGuide ? `

Official race guide

${escapeHtml(data.event.raceGuide.name)} — attached in UltraEdge. Share the original PDF separately with your crew.

` : ''} ${buildGearSection(data)} ${buildDropBagsSection(data)}