/// import { Cell, computed, Default, handler, ifElse, NAME, pattern, UI, } from "commontools"; /** * Trip Planner Demo - Comprehensive ct-map component demonstration * * Demonstrates: * - Basic markers with title/description/icon * - Coverage circles for areas of interest * - Polylines for routes between stops * - Click to add markers * - Draggable markers with position updates * - Fit to bounds button * - Bidirectional center/zoom tracking */ interface LatLng { lat: number; lng: number; } interface TripStop { position: LatLng; title: string; description: Default; icon: Default; draggable: Default; } interface AreaOfInterest { center: LatLng; radius: number; // meters title: string; description: Default; color: Default; } interface Input { tripName: Default; stops: Cell>; areasOfInterest: Cell>; showRoute: Cell>; center: Cell>; zoom: Cell>; selectedStopIndex: Cell>; fitBoundsTrigger: Cell>; initialized: Cell>; } interface Output { tripName: string; stops: TripStop[]; areasOfInterest: AreaOfInterest[]; showRoute: boolean; center: LatLng; zoom: number; selectedStopIndex: number | null; fitBoundsTrigger: number; initialized: boolean; } // Runtime values for the defaults (for use in handlers like Reset) const DEFAULT_STOPS: TripStop[] = [ { position: { lat: 37.7749, lng: -122.4194 }, title: "San Francisco", description: "The City by the Bay", icon: "bridge", draggable: true, }, { position: { lat: 37.8716, lng: -122.2727 }, title: "Berkeley", description: "Home of UC Berkeley", icon: "graduation", draggable: true, }, { position: { lat: 37.5485, lng: -122.059 }, title: "Fremont", description: "Gateway to Silicon Valley", icon: "computer", draggable: true, }, { position: { lat: 37.3382, lng: -121.8863 }, title: "San Jose", description: "Capital of Silicon Valley", icon: "building", draggable: true, }, ]; const DEFAULT_AREAS: AreaOfInterest[] = [ { center: { lat: 37.8199, lng: -122.4783 }, radius: 3000, title: "Golden Gate Area", description: "Iconic bridge and park", color: "#ef4444", }, { center: { lat: 37.4419, lng: -122.143 }, radius: 5000, title: "Stanford Area", description: "University and research hub", color: "#22c55e", }, ]; // Handler for adding a new stop when map is clicked const addStopHandler = handler< { detail: { lat: number; lng: number } }, { stops: Cell; stopCount: number } >(({ detail: { lat, lng } }, { stops, stopCount }) => { stops.push({ position: { lat, lng }, title: `Stop ${stopCount + 1}`, description: "Click marker to see details", icon: "pin", draggable: true, }); }); // Handler for updating marker position after drag const markerDragHandler = handler< { detail: { index: number; position: LatLng } }, { stops: Cell } >(({ detail: { index, position } }, { stops }) => { const currentStops = stops.get(); if (index >= 0 && index < currentStops.length) { const updated = currentStops.map((stop, i) => i === index ? { ...stop, position } : stop ); stops.set(updated); } }); // Handler for selecting a marker on click const markerClickHandler = handler< { detail: { index: number } }, { selectedStopIndex: Cell } >(({ detail }, { selectedStopIndex }) => { selectedStopIndex.set(detail?.index ?? null); }); // Handler for removing a stop const removeStopHandler = handler< void, { stops: Cell; index: number } >((_event, { stops, index }) => { const current = stops.get(); if (index >= 0 && index < current.length) { stops.set(current.toSpliced(index, 1)); } }); // Handler for adding an area of interest const addAreaHandler = handler< void, { areas: Cell; center: Cell } >((_event, { areas, center }) => { const colors = ["#3b82f6", "#ef4444", "#22c55e", "#f59e0b", "#8b5cf6"]; const colorIndex = areas.get().length % colors.length; const currentCenter = center.get() ?? { lat: 37.6, lng: -122.2 }; areas.push({ center: currentCenter, radius: 2000, title: `Area ${areas.get().length + 1}`, description: "New area of interest", color: colors[colorIndex], }); }); // Format coordinates for display (handles undefined during reactive updates) const formatCoord = ( val: number | undefined | null, decimals = 4, ): string => val != null ? val.toFixed(decimals) : "N/A"; // Handler to initialize demo data const initHandler = handler< void, { stops: Cell; areasOfInterest: Cell; center: Cell; initialized: Cell; } >((_event, { stops, areasOfInterest, center, initialized }) => { if (!initialized.get()) { stops.set(DEFAULT_STOPS); areasOfInterest.set(DEFAULT_AREAS); center.set({ lat: 37.6, lng: -122.2 }); initialized.set(true); } }); export default pattern( ({ tripName, stops, areasOfInterest, showRoute, center, zoom, selectedStopIndex, fitBoundsTrigger, initialized, }) => { // Computed values const stopCount = computed(() => stops.get().length); const areaCount = computed(() => areasOfInterest.get().length); // Bound handler for initialization const initializeDemo = initHandler({ stops, areasOfInterest, center, initialized, }); // Build map value from stops and areas const mapValue = computed(() => { const currentStops = stops.get(); const currentAreas = areasOfInterest.get(); const currentShowRoute = showRoute.get(); // Markers from stops const markers = currentStops.map((stop) => ({ position: stop.position, title: stop.title, description: stop.description || "", icon: stop.icon || "pin", draggable: stop.draggable ?? true, })); // Circles from areas of interest const circles = currentAreas.map((area) => ({ center: area.center, radius: area.radius, color: area.color || "#3b82f6", fillOpacity: 0.2, strokeWidth: 2, title: area.title, description: area.description || "", })); // Polyline connecting all stops (if enabled and more than 1 stop) const polylines = currentShowRoute && currentStops.length > 1 ? [ { points: currentStops.map((stop) => stop.position), color: "#6366f1", strokeWidth: 3, dashArray: "10, 5", }, ] : []; return { markers, circles, polylines }; }); return { [NAME]: computed(() => `Trip Planner: ${tripName}`), [UI]: ( Trip Planner {stopCount} stops | {areaCount} areas {/* Initialization prompt for empty state */} {ifElse( computed(() => !initialized.get() && stops.get().length === 0), Welcome to Trip Planner! Get started with demo data or click on the map to add your first stop. Load Demo Data , null, )} {/* Map container */} fitBoundsTrigger.get() > 0 && stops.get().length > 0, )} onct-click={addStopHandler({ stops, stopCount: stopCount, })} onct-marker-drag-end={markerDragHandler({ stops })} onct-marker-click={markerClickHandler({ selectedStopIndex })} /> {/* Map controls */} Map Controls { fitBoundsTrigger.set(fitBoundsTrigger.get() + 1); }} > Fit to All Stops { center.set({ lat: 37.6, lng: -122.2 }); zoom.set(9); }} > Reset View Show Route Center: {computed(() => formatCoord( (center.get() ?? { lat: 37.6, lng: -122.2 }).lat, ) )}, {computed(() => formatCoord( (center.get() ?? { lat: 37.6, lng: -122.2 }).lng, ) )} Zoom: {computed(() => zoom.get().toFixed(1))} {/* Stops list */} Trip Stops Click map to add {stops.map((stop, index) => ( selectedStopIndex.get() === index), "var(--ct-color-blue-100)", "var(--ct-color-gray-50)", ), }} > {index + 1} {computed( () => `${formatCoord(stop.position.lat)}, ${ formatCoord(stop.position.lng) }`, )} { center.set(stop.position); zoom.set(14); }} > Go x ))} {ifElse( computed(() => stops.get().length === 0), Click on the map to add your first stop! , null, )} {/* Areas of interest */} Areas of Interest + Add Area {areasOfInterest.map((area) => ( Radius: {area.radius}m { center.set(area.center); zoom.set(13); }} > Go { const current = areasOfInterest.get(); const idx = current.findIndex((a) => Cell.equals(area, a) ); if (idx >= 0) { areasOfInterest.set(current.toSpliced(idx, 1)); } }} > x ))} {ifElse( computed(() => areasOfInterest.get().length === 0), No areas defined. Click "Add Area" to highlight a region. , null, )} {/* Route summary */} {ifElse( computed(() => showRoute.get() && stops.get().length > 1), Route Overview {stops.map((stop, index) => ( {stop.title} {ifElse( computed(() => index < stops.get().length - 1 ), {" "} ->{" "} , null, )} ))} {stopCount} stops connected by dashed route line , null, )} {/* Instructions */} How to Use Click anywhere on the map to add a new stop Drag markers to reposition stops Use "Fit to All Stops" to zoom to see all markers Toggle "Show Route" to display/hide the route line Add areas of interest to highlight regions on the map Edit stop names inline in the list below { stops.set([]); areasOfInterest.set([]); selectedStopIndex.set(null); }} > Clear All { stops.set(DEFAULT_STOPS); areasOfInterest.set(DEFAULT_AREAS); center.set({ lat: 37.6, lng: -122.2 }); zoom.set(9); }} > Reset Demo ), tripName, stops, areasOfInterest, showRoute, center: computed(() => center.get() ?? { lat: 37.6, lng: -122.2 }), zoom: computed(() => zoom.get()), selectedStopIndex: computed(() => selectedStopIndex.get()), fitBoundsTrigger: computed(() => fitBoundsTrigger.get()), initialized: computed(() => initialized.get()), }; }, );
Get started with demo data or click on the map to add your first stop.