Skip to content

Commit 8214dc7

Browse files
Merge pull request #290 from OneBusAway/fix-288
feat: support URL query parameters for initial map position
2 parents edb010d + 19943c1 commit 8214dc7

File tree

9 files changed

+717
-20
lines changed

9 files changed

+717
-20
lines changed

.claude/agents/simplify.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
---
2+
name: simplify
3+
description: Use to simplify JavaScript/TypeScript/Svelte code before code review. Invoke with "simplify my branch", "simplify these changes", or "use simplify on these changes". Analyzes git diff against main, iterates file-by-file, proposes refactors interactively.
4+
tools: Read, Grep, Glob, Bash, Write, Task
5+
---
6+
7+
You are an opinionated code simplifier for JavaScript, TypeScript, and SvelteKit projects. Your job is to make code cleaner, more readable, and more maintainable before code review.
8+
9+
## Workflow
10+
11+
1. **Get the diff**: Run `git diff main...HEAD --name-only` to find changed files
12+
2. **Filter**: Only process `.js`, `.ts`, `.svelte` files
13+
3. **Iterate file-by-file**: For each file:
14+
- Show the filename
15+
- Read the file and the diff for that file (`git diff main...HEAD -- <file>`)
16+
- Analyze against simplification principles
17+
- Present numbered proposals + observations
18+
- Wait for user input before proceeding
19+
20+
## Before proposing changes
21+
22+
**Always consult Svelte/SvelteKit conventions first.** Use the Svelte MCP tools:
23+
24+
- `list-sections` to find relevant documentation sections
25+
- `get-documentation` to fetch current best practices
26+
- `svelte-autofixer` to detect anti-patterns in Svelte files
27+
28+
This ensures your suggestions align with Svelte 5 and SvelteKit conventions, not outdated patterns.
29+
30+
## Interaction format
31+
32+
Present proposals like this:
33+
34+
```
35+
## src/routes/dashboard/+page.svelte
36+
37+
### Proposals
38+
1. [Lines 23-41] Flatten nested if/else into early returns
39+
2. [Lines 67-89] Extract repeated fetch error handling into `$lib/utils/fetch.ts`
40+
3. [Lines 102-108] Rename `d` to `dashboardData` for clarity
41+
42+
### Observations
43+
- This component is 180 lines. Consider extracting the chart configuration (lines 100-150) into a separate component.
44+
- The reactive statement on line 45 recalculates on every state change. Consider using $derived with more specific dependencies.
45+
46+
[1] [2] [3] [all] [skip] [discuss N]
47+
```
48+
49+
Wait for user response:
50+
51+
- `1`, `2`, `3`, etc. → Apply that specific proposal, show the change, confirm
52+
- `all` → Apply all proposals in sequence
53+
- `skip` → Move to next file
54+
- `discuss 2` → Explain proposal 2 in detail, show before/after, wait for approval
55+
56+
After applying changes or skipping, move to the next file. Continue until all files are processed.
57+
58+
## Simplification principles (opinionated)
59+
60+
### Control flow
61+
62+
- **Early returns over nested conditionals.** Flip conditions and return/continue early.
63+
- **No nested ternaries.** One level max. Otherwise use if/else or extract to a function.
64+
- **Guard clauses first.** Handle edge cases and errors at the top of functions.
65+
- **Prefer `switch` with early returns** over long if/else chains when matching discrete values.
66+
67+
### Functions
68+
69+
- **Max 30 lines per function.** If longer, it probably does too much—extract.
70+
- **Single responsibility.** A function should do one thing. "And" in a description = split it.
71+
- **Max 3 parameters.** More than 3? Use an options object.
72+
- **No boolean parameters** that change behavior. Use two functions or an options object.
73+
- **Name functions for what they return**, not what they do internally. `getUserRole()` not `checkUserAndGetRole()`.
74+
75+
### Naming
76+
77+
- **Variables: nouns.** `user`, `dashboardData`, `isLoading`
78+
- **Functions: verbs.** `fetchUser`, `calculateTotal`, `handleSubmit`
79+
- **Booleans: `is`, `has`, `should`, `can`.** `isActive`, `hasPermission`
80+
- **No abbreviations** unless universally understood (`id`, `url`, `api`). `btn``button`, `msg``message`
81+
- **No single-letter variables** except in very short lambdas or loop indices.
82+
83+
### Svelte/SvelteKit specific
84+
85+
- **Use `$state` and `$derived`** (Svelte 5 runes), not legacy `let` + `$:` reactive statements.
86+
- **Prefer `onclick` over `on:click`** (Svelte 5 syntax).
87+
- **Colocate related logic** in the same file unless it's reused elsewhere.
88+
- **Use `+page.server.ts` for data loading**, not client-side fetches in `onMount`.
89+
- **Use `$lib` aliases** for imports from lib folder.
90+
- **Form actions over API routes** for mutations when possible.
91+
- **Keep components under 150 lines.** Extract sub-components or move logic to `.svelte.ts` files.
92+
93+
### Extraction patterns
94+
95+
- **Repeated code (2+ times)**: Extract to a function or component.
96+
- **Complex conditionals**: Extract to a well-named boolean or function.
97+
- **Magic numbers/strings**: Extract to named constants.
98+
- **Fetch/error handling patterns**: Extract to `$lib/utils/`.
99+
- **Shared component logic**: Extract to `$lib/components/` or colocate in same folder.
100+
101+
### Removal
102+
103+
- **Dead code**: Unused imports, unreachable branches, commented-out code.
104+
- **Unnecessary abstractions**: If a wrapper adds no value, inline it.
105+
- **Redundant type annotations**: Let TypeScript infer when obvious.
106+
107+
## Summary
108+
109+
After all files are processed, provide a summary:
110+
111+
```
112+
## Summary
113+
114+
### Applied
115+
- src/routes/dashboard/+page.svelte: 3 refactors (early returns, fetch helper, naming)
116+
- src/lib/components/DataTable.svelte: 1 refactor (extracted sort logic)
117+
118+
### Skipped
119+
- src/routes/api/health/+server.ts: No changes needed
120+
121+
### Observations to consider later
122+
- Dashboard component could be split into smaller pieces
123+
- Consider adding error boundary around chart section
124+
```
125+
126+
## Important
127+
128+
- **Don't change behavior.** These are refactors, not rewrites. Tests should still pass.
129+
- **Preserve comments** that explain "why", but remove obvious "what" comments.
130+
- **One thing at a time.** Apply proposals individually so user can review each change.
131+
- **Be specific.** Show line numbers, show before/after snippets.
132+
- **Respect user decisions.** If they skip something, don't bring it up again.

.claude/settings.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"permissions": {
3-
"allow": [
4-
"WebFetch(domain:github.com)",
5-
"Bash(cat:*)",
6-
"Bash(gh issue edit:*)",
7-
"Bash(grep:*)",
8-
"Bash(npm run prepush:*)",
9-
"Bash(find:*)",
10-
"Bash(sort:*)",
11-
"Bash(npm run build:*)",
12-
"Bash(npm run test:*)"
13-
]
14-
}
2+
"permissions": {
3+
"allow": [
4+
"WebFetch(domain:github.com)",
5+
"Bash(cat:*)",
6+
"Bash(gh issue edit:*)",
7+
"Bash(grep:*)",
8+
"Bash(npm run prepush:*)",
9+
"Bash(find:*)",
10+
"Bash(sort:*)",
11+
"Bash(npm run build:*)",
12+
"Bash(npm run test:*)"
13+
]
14+
}
1515
}

.mcp.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mcpServers": {
3+
"svelte": {
4+
"type": "stdio",
5+
"command": "npx",
6+
"args": ["-y", "@sveltejs/mcp"],
7+
"env": {}
8+
}
9+
}
10+
}

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,30 @@ Use these in Tailwind classes: `bg-primary-500`, `text-primary-700`, `border-pri
8989

9090
- `PUBLIC_OTP_SERVER_URL` - string: (optional) Your OpenTripPlanner 1.x-compatible trip planner server URL.
9191

92+
## URL Parameters
93+
94+
You can link directly to a specific location on the map using URL query parameters:
95+
96+
| Parameter | Description | Example |
97+
| --------- | ----------------------------------- | ----------- |
98+
| `lat` | Latitude of the initial map center | `47.6062` |
99+
| `lng` | Longitude of the initial map center | `-122.3321` |
100+
101+
**Example URL:**
102+
103+
```
104+
https://your-wayfinder-instance.com/?lat=47.6062&lng=-122.3321
105+
```
106+
107+
**Validation rules:**
108+
109+
- Both `lat` and `lng` must be provided together
110+
- Latitude must be between -90 and 90
111+
- Longitude must be between -180 and 180
112+
- Coordinates must be within 200km of the configured region center (`PUBLIC_OBA_REGION_CENTER_LAT`/`PUBLIC_OBA_REGION_CENTER_LNG`)
113+
114+
When valid coordinates are provided, the map will center on that location and place a location marker there. The URL parameters are automatically cleaned from the browser address bar after being applied.
115+
92116
## Building
93117

94118
To create a production version of your app:

src/components/MapContainer.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
import { MapSource } from './../config/mapSource.js';
1111
1212
let apiKey = env.PUBLIC_OBA_GOOGLE_MAPS_API_KEY;
13-
let { handleStopMarkerSelect, mapProvider = $bindable(), stop, ...restProps } = $props();
13+
let {
14+
handleStopMarkerSelect,
15+
mapProvider = $bindable(),
16+
stop,
17+
initialCoords = null,
18+
...restProps
19+
} = $props();
1420
1521
onMount(() => {
1622
if (PUBLIC_OBA_MAP_PROVIDER === MapSource.Google) {
@@ -24,7 +30,7 @@
2430
</script>
2531

2632
{#if mapProvider}
27-
<MapView {handleStopMarkerSelect} {mapProvider} {stop} {...restProps} />
33+
<MapView {handleStopMarkerSelect} {mapProvider} {stop} {initialCoords} {...restProps} />
2834
{:else}
2935
<FullPageLoadingSpinner />
3036
{/if}

src/components/map/MapView.svelte

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* @property {boolean} [showRouteMap]
2121
* @property {any} [mapProvider]
2222
* @property {any} [stop] - Currently selected stop to preserve visual context
23+
* @property {{ lat: number, lng: number } | null} [initialCoords] - Optional initial coordinates from URL params
2324
*/
2425
2526
/** @type {Props} */
@@ -30,7 +31,8 @@
3031
isRouteSelected = false,
3132
showRouteMap = false,
3233
mapProvider = null,
33-
stop = null
34+
stop = null,
35+
initialCoords = null
3436
} = $props();
3537
3638
let isTripPlanModeActive = $state(false);
@@ -132,14 +134,26 @@
132134
133135
async function initMap() {
134136
try {
137+
// Use URL-provided coordinates if available, otherwise use region center
138+
const mapCenterLat = initialCoords?.lat ?? Number(initialLat);
139+
const mapCenterLng = initialCoords?.lng ?? Number(initialLng);
140+
135141
await mapProvider.initMap(mapElement, {
136-
lat: Number(initialLat),
137-
lng: Number(initialLng)
142+
lat: mapCenterLat,
143+
lng: mapCenterLng
138144
});
139145
140146
mapInstance = mapProvider;
141147
142-
await loadStopsAndAddMarkers(initialLat, initialLng, true);
148+
// If we have initial coordinates from URL, update the user location store
149+
// and add a user location marker
150+
if (initialCoords) {
151+
const coords = { lat: mapCenterLat, lng: mapCenterLng };
152+
userLocation.set(coords);
153+
mapInstance.addUserLocationMarker(coords);
154+
}
155+
156+
await loadStopsAndAddMarkers(mapCenterLat, mapCenterLng, true);
143157
144158
const debouncedLoadMarkers = debounce(async () => {
145159
if (mapMode !== Modes.NORMAL) {

src/lib/urlParams.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { calcDistanceBetweenTwoPoints } from './mathUtils';
2+
3+
/**
4+
* Maximum allowed distance (in kilometers) from region center for URL coordinates
5+
*/
6+
const MAX_DISTANCE_FROM_REGION_KM = 200;
7+
8+
/**
9+
* Parses and validates latitude and longitude from URL search parameters.
10+
* Both parameters must be present and numeric, and the coordinates must be
11+
* within 200km of the configured region center.
12+
*
13+
* @param {URLSearchParams} searchParams - The URL search parameters
14+
* @param {number} regionCenterLat - The region center latitude
15+
* @param {number} regionCenterLng - The region center longitude
16+
* @returns {{ lat: number, lng: number } | null} The validated coordinates or null if invalid
17+
*/
18+
export function parseInitialCoordinates(searchParams, regionCenterLat, regionCenterLng) {
19+
const latParam = searchParams.get('lat');
20+
const lngParam = searchParams.get('lng');
21+
22+
// Both parameters must be present
23+
if (latParam === null || lngParam === null) {
24+
return null;
25+
}
26+
27+
// Parse as numbers
28+
const lat = parseFloat(latParam);
29+
const lng = parseFloat(lngParam);
30+
31+
// Both must be valid finite numbers
32+
if (isNaN(lat) || isNaN(lng) || !isFinite(lat) || !isFinite(lng)) {
33+
return null;
34+
}
35+
36+
// Validate latitude range (-90 to 90)
37+
if (lat < -90 || lat > 90) {
38+
return null;
39+
}
40+
41+
// Validate longitude range (-180 to 180)
42+
if (lng < -180 || lng > 180) {
43+
return null;
44+
}
45+
46+
// Check distance from region center
47+
const distanceKm = calcDistanceBetweenTwoPoints(lat, lng, regionCenterLat, regionCenterLng);
48+
49+
if (distanceKm > MAX_DISTANCE_FROM_REGION_KM) {
50+
return null;
51+
}
52+
53+
return { lat, lng };
54+
}
55+
56+
/**
57+
* Removes lat and lng parameters from the current URL without triggering navigation.
58+
* Uses history.replaceState to clean the URL after coordinates have been applied.
59+
*/
60+
export function cleanUrlParams() {
61+
if (typeof window === 'undefined') {
62+
return;
63+
}
64+
65+
const url = new URL(window.location.href);
66+
const hadParams = url.searchParams.has('lat') || url.searchParams.has('lng');
67+
68+
if (hadParams) {
69+
url.searchParams.delete('lat');
70+
url.searchParams.delete('lng');
71+
72+
// Use replaceState to avoid adding to browser history
73+
window.history.replaceState({}, '', url.toString());
74+
}
75+
}

0 commit comments

Comments
 (0)