Merge branch 'main' into feature/open-mapping
This commit is contained in:
commit
dd4861458d
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
id: task-024
|
||||
title: 'Open Mapping: Collaborative Route Planning Module'
|
||||
status: To Do
|
||||
assignee: []
|
||||
created_date: '2025-12-04 14:30'
|
||||
labels:
|
||||
- feature
|
||||
- mapping
|
||||
dependencies: []
|
||||
priority: high
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||
Implement an open-source mapping and routing layer for the canvas that provides advanced route planning capabilities beyond Google Maps. Built on OpenStreetMap, OSRM/Valhalla, and MapLibre GL JS.
|
||||
<!-- SECTION:DESCRIPTION:END -->
|
||||
|
||||
## Acceptance Criteria
|
||||
<!-- AC:BEGIN -->
|
||||
- [ ] #1 MapLibre GL JS integrated with tldraw canvas
|
||||
- [ ] #2 OSRM routing backend deployed to Netcup
|
||||
- [ ] #3 Waypoint placement and route calculation working
|
||||
- [ ] #4 Multi-route comparison UI implemented
|
||||
- [ ] #5 Y.js collaboration for shared route editing
|
||||
- [ ] #6 Layer management panel with basemap switching
|
||||
- [ ] #7 Offline tile caching via Service Worker
|
||||
- [ ] #8 Budget tracking per waypoint/route
|
||||
<!-- AC:END -->
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
<!-- SECTION:PLAN:BEGIN -->
|
||||
Phase 1 - Foundation:
|
||||
- Integrate MapLibre GL JS with tldraw
|
||||
- Deploy OSRM to /opt/apps/open-mapping/
|
||||
- Basic waypoint and route UI
|
||||
|
||||
Phase 2 - Multi-Route:
|
||||
- Alternative routes visualization
|
||||
- Route comparison panel
|
||||
- Elevation profiles
|
||||
|
||||
Phase 3 - Collaboration:
|
||||
- Y.js integration
|
||||
- Real-time cursor presence
|
||||
- Share links
|
||||
|
||||
Phase 4 - Layers:
|
||||
- Layer panel UI
|
||||
- Multiple basemaps
|
||||
- Custom overlays
|
||||
|
||||
Phase 5 - Calendar/Budget:
|
||||
- Time windows on waypoints
|
||||
- Cost estimation
|
||||
- iCal export
|
||||
|
||||
Phase 6 - Optimization:
|
||||
- VROOM TSP/VRP
|
||||
- Offline PWA
|
||||
<!-- SECTION:PLAN:END -->
|
||||
|
|
@ -3,28 +3,45 @@ import * as Automerge from "@automerge/automerge"
|
|||
|
||||
// Helper function to validate if a string is a valid tldraw IndexKey
|
||||
// tldraw uses fractional indexing based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
|
||||
// Valid indices have an integer part (letter indicating length) followed by digits and optional alphanumeric fraction
|
||||
// Examples: "a0", "a1", "a1V", "a24sT", "a1V4rr"
|
||||
// Invalid: "b1" (old format), simple sequential numbers
|
||||
// The first letter encodes integer part length: a=1 digit, b=2 digits, c=3 digits, etc.
|
||||
// Examples: "a0"-"a9", "b10"-"b99", "c100"-"c999", with optional fraction "a1V4rr"
|
||||
// Invalid: "b1" (b expects 2 digits but has 1), simple sequential numbers
|
||||
function isValidIndexKey(index: string): boolean {
|
||||
if (!index || typeof index !== 'string' || index.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// tldraw uses fractional indexing where:
|
||||
// - First character is a lowercase letter indicating integer part length (a=1, b=2, c=3, etc.)
|
||||
// - Followed by alphanumeric characters for the value and optional jitter
|
||||
// Examples: "a0", "a1", "b10", "b99", "c100", "a1V4rr", "b10Lz"
|
||||
//
|
||||
// Also uppercase letters for negative indices (Z=1, Y=2, etc.)
|
||||
|
||||
// Valid fractional index: lowercase letter followed by alphanumeric characters
|
||||
if (/^[a-z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return true
|
||||
// Must start with a letter
|
||||
if (!/^[a-zA-Z]/.test(index)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Also allow uppercase prefix for negative/very high indices
|
||||
if (/^[A-Z][a-zA-Z0-9]+$/.test(index)) {
|
||||
const prefix = index[0]
|
||||
const rest = index.slice(1)
|
||||
|
||||
// For lowercase prefixes, validate digit count matches the prefix
|
||||
if (prefix >= 'a' && prefix <= 'z') {
|
||||
// Calculate expected minimum digit count: a=1, b=2, c=3, etc.
|
||||
const expectedDigits = prefix.charCodeAt(0) - 'a'.charCodeAt(0) + 1
|
||||
|
||||
// Extract the integer part (leading digits)
|
||||
const integerMatch = rest.match(/^(\d+)/)
|
||||
if (!integerMatch) {
|
||||
// No digits at all - invalid
|
||||
return false
|
||||
}
|
||||
|
||||
const integerPart = integerMatch[1]
|
||||
|
||||
// Check if integer part has correct number of digits for the prefix
|
||||
if (integerPart.length < expectedDigits) {
|
||||
// Invalid: "b1" has b (expects 2 digits) but only has 1 digit
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check overall format: letter followed by alphanumeric
|
||||
if (/^[a-zA-Z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,19 +21,36 @@ function minimalSanitizeRecord(record: any): any {
|
|||
if (typeof sanitized.opacity !== 'number') sanitized.opacity = 1
|
||||
if (!sanitized.meta || typeof sanitized.meta !== 'object') sanitized.meta = {}
|
||||
// NOTE: Index assignment is handled by assignSequentialIndices() during format conversion
|
||||
// Here we only ensure index exists with a valid format, not strictly validate
|
||||
// This preserves layer order that was established during conversion
|
||||
// tldraw uses fractional indexing: a0, a1, b10, c100, a1V4rr, etc.
|
||||
// - First letter (a-z) indicates integer part length (a=1 digit, b=2 digits, etc.)
|
||||
// - Uppercase (A-Z) for negative/special indices
|
||||
// Here we validate using tldraw's fractional indexing rules
|
||||
// The first letter encodes integer part length: a=1 digit, b=2 digits, c=3 digits, etc.
|
||||
// Examples: "a0"-"a9", "b10"-"b99", "c100"-"c999", with optional fraction "a1V4rr"
|
||||
// Invalid: "b1" (b expects 2 digits but has 1)
|
||||
if (!sanitized.index || typeof sanitized.index !== 'string' || sanitized.index.length === 0) {
|
||||
// Only assign default if truly missing
|
||||
sanitized.index = 'a1'
|
||||
} else if (!/^[a-zA-Z][a-zA-Z0-9]+$/.test(sanitized.index)) {
|
||||
// Accept any letter followed by alphanumeric characters
|
||||
// Only reset clearly invalid formats (e.g., numbers, empty, single char)
|
||||
console.warn(`⚠️ MinimalSanitization: Invalid index format "${sanitized.index}" for shape ${sanitized.id}`)
|
||||
sanitized.index = 'a1'
|
||||
} else {
|
||||
// Validate fractional indexing format
|
||||
let isValid = false
|
||||
const prefix = sanitized.index[0]
|
||||
const rest = sanitized.index.slice(1)
|
||||
|
||||
if (/^[a-zA-Z]/.test(sanitized.index) && /^[a-zA-Z][a-zA-Z0-9]+$/.test(sanitized.index)) {
|
||||
if (prefix >= 'a' && prefix <= 'z') {
|
||||
// Calculate expected minimum digit count: a=1, b=2, c=3, etc.
|
||||
const expectedDigits = prefix.charCodeAt(0) - 'a'.charCodeAt(0) + 1
|
||||
const integerMatch = rest.match(/^(\d+)/)
|
||||
if (integerMatch && integerMatch[1].length >= expectedDigits) {
|
||||
isValid = true
|
||||
}
|
||||
} else if (prefix >= 'A' && prefix <= 'Z') {
|
||||
// Uppercase for negative/special indices - allow
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
console.warn(`⚠️ MinimalSanitization: Invalid index format "${sanitized.index}" for shape ${sanitized.id}`)
|
||||
sanitized.index = 'a1'
|
||||
}
|
||||
}
|
||||
if (!sanitized.parentId) sanitized.parentId = 'page:page'
|
||||
|
||||
|
|
|
|||
|
|
@ -68,18 +68,50 @@ import "@/css/style.css"
|
|||
import "@/css/obsidian-browser.css"
|
||||
|
||||
// Helper to validate and fix tldraw IndexKey format
|
||||
// tldraw uses fractional indexing: a0, a1, b10, c100, a1V4rr, etc.
|
||||
// - First letter (a-z) indicates integer part length (a=1 digit, b=2 digits, etc.)
|
||||
// - Uppercase (A-Z) for negative/special indices
|
||||
// tldraw uses fractional indexing where the first letter encodes integer part length:
|
||||
// - 'a' = 1-digit integer (a0-a9), 'b' = 2-digit (b10-b99), 'c' = 3-digit (c100-c999), etc.
|
||||
// - Optional fractional part can follow (a1V, a1V4rr, etc.)
|
||||
// Common invalid formats from old data: "b1" (b expects 2 digits but has 1)
|
||||
function sanitizeIndex(index: any): IndexKey {
|
||||
if (!index || typeof index !== 'string' || index.length === 0) {
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
// Valid: letter followed by alphanumeric characters
|
||||
|
||||
// Must start with a letter
|
||||
if (!/^[a-zA-Z]/.test(index)) {
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
|
||||
// Check fractional indexing rules for lowercase prefixes
|
||||
const prefix = index[0]
|
||||
const rest = index.slice(1)
|
||||
|
||||
if (prefix >= 'a' && prefix <= 'z') {
|
||||
// Calculate expected minimum digit count: a=1, b=2, c=3, etc.
|
||||
const expectedDigits = prefix.charCodeAt(0) - 'a'.charCodeAt(0) + 1
|
||||
|
||||
// Extract the integer part (leading digits)
|
||||
const integerMatch = rest.match(/^(\d+)/)
|
||||
if (!integerMatch) {
|
||||
// No digits at all - invalid
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
|
||||
const integerPart = integerMatch[1]
|
||||
|
||||
// Check if integer part has correct number of digits for the prefix
|
||||
if (integerPart.length < expectedDigits) {
|
||||
// Invalid: "b1" has b (expects 2 digits) but only has 1 digit
|
||||
// Convert to safe format
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
}
|
||||
|
||||
// Check overall format: letter followed by alphanumeric
|
||||
if (/^[a-zA-Z][a-zA-Z0-9]+$/.test(index)) {
|
||||
return index as IndexKey
|
||||
}
|
||||
// Fallback for invalid formats
|
||||
|
||||
return 'a1' as IndexKey
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue