Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ students and an orientation and mobility instructor is planned in
| `src/geojson/` | Campus data layers ([README](src/geojson/README.md)) |
| `src/basemap/` | Self-hosted basemap, terrain, fonts, and icons |
| `src/vendor/` | MapLibre GL JS and PMTiles ([README](src/vendor/README.md)) |
| `src/js/routing/` | Routing graph, route finder, slopes, and step-by-step instructions, shared with `scripts/routing/` ([README](scripts/routing/README.md)) |
| `src/js/routing/` | Routing graph, route finder, slopes, shuttle rides, and step-by-step instructions, shared with `scripts/routing/` ([README](scripts/routing/README.md)) |
| `scripts/` | Local preview server and data/basemap tooling. Not deployed. |
| `tests/accessibility/` | Automated accessibility checks (axe-core and Playwright, used by CI only). Not deployed. |
| `docs/` | Data collection sheets and the usability testing plan |

See [updatingdata.md](updatingdata.md) for editing map data.
See [updatingdata.md](updatingdata.md) for editing map data. See [ROADMAP.md](ROADMAP.md) for what's planned next.

## Deployment

Expand Down
54 changes: 54 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Access BC roadmap

What's planned next, and what each item needs. Roughly in priority order within each section.

## Conversations to have

| Who | About |
|---|---|
| **BC Transportation** | **Live shuttle tracker.** Permission to use the live tracker (bostoncollege.transloc.com, run by TransLoc) for arrival times ("next shuttle in 4 min"), ideally through an official GTFS and GTFS-realtime feed from TransLoc (the tracker data already carries GTFS ids). Today Access BC uses a snapshot of the routes and BC's published schedules ([scripts/shuttles.mjs](scripts/shuttles.mjs)). |
| BC Transportation | **Shuttle accessibility.** Which shuttles have wheelchair lifts or ramps, and which stops are accessible (curb, shelter, level boarding). Until then, routes say shuttle accessibility isn't confirmed. Also: is there an accessible van or escort service, and how is it booked? |
| BC Facilities | **Problem reports.** Where reports should go so things get fixed (UpKeep), and whether Access BC's reports can reach them. What data Facilities already has, and in what formats: floor plans, snow priority routes, construction and closures, accessible parking. |
| Disability Services | Which data and features matter most to students; usability testing with students ([docs/usability-testing/](docs/usability-testing/)). |
| Building managers, Auxiliary Services | Building hours, and card-access hours for each entrance. |

## Live problem reports

- A Google Form for reports (bc.edu accounts only), feeding a Google Sheet. Set `REPORT_FORM` in
[src/js/reports.js](src/js/reports.js) and the report form opens filled in.
- A small Apps Script on the Sheet serving recent reports as JSON. The site fetches it when it loads, so everyone's
routes avoid what's reported (today, your own reports affect only your device). No rebuild needed.
- Reports show as unconfirmed ("reported 2 h ago") and expire after a day or two unless staff confirm them in the Sheet.
- The same Sheet-and-script approach for other data staff keep up to date: building hours, closures, entrance details.

## Getting around

- **Shuttles:** live arrival times (see BC Transportation above); routes that change by day (holidays, football).
- **MBTA Green Line:** Boston College, Chestnut Hill Ave, Reservoir and Newton Centre stations, with elevator
outages from the MBTA's public feeds.
- **City sidewalks:** 1,432 crossings around campus have no curb information in OpenStreetMap. Tagging curb ramps
there (`kerb=lowered` on a crossing's end nodes) improves routes after `node scripts/osm-sidewalks.mjs`.
- The Brookline campus has no BC entrances mapped yet.

## Inside buildings

- Floor levels on indoor paths, and elevators as links between floors ("take the elevator to floor 3").
- Rooms as destinations: single-stall restrooms and lactation rooms with their floor, offices, classrooms.
- Door details: automatic openers (and where the button is), card-swipe doors, heavy doors.
- Source: Facilities' floor plans (CAD or Revit), converted to IMDF. Publish routes and points of interest, not full plans.
- Indoor location: start with steps and landmarks; QR or NFC tags at entrances later.

## Outdoor data

1. Accessible parking and drop-off points.
2. Surface (brick, cobblestone) and cross slope.
3. Crossings: marked crosswalks, accessible pedestrian signals, tactile paving, curb ramp condition.
4. Path width, pinch points, and lighting at night.
5. Snow priority routes.
6. Elevator details: floors served, key needed, hours.
7. Building hours, so routes don't go through buildings that are closed.

## Before relying on live directions

- Test with students on real phones (iPhone and Android, with VoiceOver and TalkBack).
- Confirm the accessibility checks run on GitHub's servers ([.github/workflows/accessibility.yml](.github/workflows/accessibility.yml)).
10 changes: 8 additions & 2 deletions help.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,20 @@ <h2 id="where-title">Where am I?</h2>

<section id="between-campuses" aria-labelledby="between-campuses-title">
<h2 id="between-campuses-title">Between campuses, and from off campus</h2>
<p>Directions between the Chestnut Hill, Brighton and Newton campuses, and from places near campus, use city sidewalks and street crossings from <a href="https://www.openstreetmap.org/">OpenStreetMap</a> where BC's own paths don't reach. On campus, BC's checked paths are always used first.</p>
<p>Walking directions between the Chestnut Hill, Brighton and Newton campuses, and from places near campus, use city sidewalks and street crossings from <a href="https://www.openstreetmap.org/">OpenStreetMap</a> where BC's own paths don't reach. On campus, BC's checked paths are always used first.</p>
<ul>
<li>City sidewalks <strong>haven't been checked for accessibility</strong>. The route says how much of it is on them.</li>
<li>Each street crossing is a step of its own: <q>Cross Beacon Street at the traffic lights. There are curb ramps on both sides.</q> When curb ramps aren't recorded, the step says so, and the route says how many crossings have confirmed curb ramps. Crossings known to have a raised curb are avoided.</li>
<li>Off campus, directions name the street you're walking along, since there are no BC buildings to steer by.</li>
<li>Avoid steep slopes applies here too. Newton is hilly, so a step-free, gentle route can be much longer: the route says when a shorter, steeper one exists.</li>
</ul>
<p>The BC shuttle connects the campuses as well. Shuttle times aren't in Access BC's directions yet.</p>
<h3>BC shuttles</h3>
<p>When a BC shuttle is running and makes the trip clearly quicker, directions include it: walk to the stop, <q>take the Newton Eagle Direct shuttle to Stuart</q>, and walk on from there. The route says how long walking all the way would take instead. If a shuttle would help but none is running, the route says when the next one starts.</p>
<ul>
<li>Times come from BC's published schedule, with an average wait. They aren't live: check the <a href="https://bostoncollege.transloc.com/">live shuttle tracker</a>.</li>
<li><strong>Whether the shuttles have a wheelchair lift or ramp isn't confirmed yet.</strong> Check with BC Transportation before relying on one.</li>
<li>To walk instead, turn off <strong>Use BC shuttles when quicker</strong> in Edit trip.</li>
</ul>
</section>

<section id="blue-light" aria-labelledby="blue-light-title">
Expand Down
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ <h3>Route options</h3>
<option value="0.05" data-label="1:20 (5%)">1:20 (5%), the steepest an accessible walkway should be</option>
</select>
</div>
<div class="route-check">
<input type="checkbox" id="opt-shuttle" checked aria-describedby="opt-shuttle-help">
<label for="opt-shuttle">Use BC shuttles when quicker</label>
<p id="opt-shuttle-help" class="route-note">Only shuttles running at the time. Whether they have a wheelchair lift or ramp isn't confirmed yet.</p>
</div>
<div class="route-check"><input type="checkbox" id="route-focus" checked><label for="route-focus">Only show map features along the route</label></div>
<h3>Route line colours</h3>
<ul class="route-legend" aria-label="Route line colours">
Expand All @@ -201,6 +206,7 @@ <h3>Route line colours</h3>
<li><span class="route-swatch very-steep"></span>Steeper than 1:12</li>
<li><span class="route-swatch unreliable"></span>Slope unknown</li>
<li><span class="route-swatch stairs"></span>Stairs</li>
<li><span class="route-swatch shuttle"></span>Shuttle ride</li>
</ul>
<p class="route-note">Slopes are estimated from elevation data and can miss short ramps and curbs.</p>
<p id="route-coverage" class="route-note"></p>
Expand Down
134 changes: 134 additions & 0 deletions scripts/shuttles.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Builds src/geojson/shuttles.json: BC's regular shuttle routes, so directions can include a shuttle ride.
//
// Usage: node scripts/shuttles.mjs
//
// Routes, stops (in order), ride times between stops, and route lines come from a snapshot of BC's public live
// shuttle tracker (bostoncollege.transloc.com, run by TransLoc). The days, hours, and frequencies aren't in the
// tracker data, so they're typed in below from BC's shuttle page:
// https://www.bc.edu/bc-web/offices/aux-services/sites/transportation-parking/shuttles.html
// Re-run this, and check SERVICE against that page, whenever the routes or schedules change (each semester).
// Using the live tracker directly (arrival times) needs BC Transportation's agreement: see ROADMAP.md.
//
// Whether the shuttles have lifts or ramps isn't published, so the app says it isn't confirmed.
import fs from 'node:fs';
import path from 'node:path';

const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
const OUT = path.join(ROOT, 'src/geojson/shuttles.json');
const TRACKER = 'https://bostoncollege.transloc.com/Services/JSONPRelay.svc/';
const SCHEDULE_PAGE = 'https://www.bc.edu/bc-web/offices/aux-services/sites/transportation-parking/shuttles.html';

// When each route runs, from BC's shuttle page (checked 2026-10-16). Keyed by tracker route id.
// days: 0 = Sunday … 6 = Saturday. Times are 24-hour "HH:MM"; past midnight continues the day ("26:00" = 2 a.m.).
// every: minutes between shuttles (the middle of a range like "8–10 minutes"). loop: the last stop runs on to the first.
// Event routes (football, holidays) aren't included.
const WEEKDAYS = [1, 2, 3, 4, 5], WEEKEND = [0, 6];
const SERVICE = {
6: { name: 'Comm Ave Direct', loop: true, service: [{ days: WEEKDAYS, from: '07:00', to: '17:00', every: 9 }] },
5: { name: 'Comm Ave All Stops', loop: true, service: [
{ days: WEEKDAYS, from: '17:00', to: '26:00', every: 12 },
{ days: WEEKEND, from: '08:00', to: '11:30', every: 30 },
{ days: WEEKEND, from: '11:30', to: '26:00', every: 12 },
] },
14: { name: 'Newton Eagle Direct', loop: true, service: [{ days: WEEKDAYS, from: '07:00', to: '17:00', every: 9 }] },
15: { name: 'Newton Limited', loop: true, service: [
{ days: WEEKDAYS, from: '17:00', to: '18:00', every: 10 },
{ days: WEEKEND, from: '08:15', to: '11:00', every: 30 },
] },
13: { name: 'Newton All Stops', loop: true, service: [
{ days: WEEKDAYS, from: '18:00', to: '26:00', every: 12 },
{ days: WEEKEND, from: '11:00', to: '13:45', every: 30 },
{ days: WEEKEND, from: '13:45', to: '26:00', every: 12 },
] },
45: { name: 'Brookline Campus', loop: true, service: [
{ days: [0, 1, 2, 3, 4], from: '07:00', to: '22:00', every: 20 },
{ days: [0, 1, 2, 3, 4], from: '22:00', to: '26:00', every: 40 },
{ days: [5, 6], from: '07:00', to: '26:00', every: 20 },
] },
// Fixed departures (7:00, 7:40, 8:35, 9:20 from Reservoir; 3:10 and 5:00 p.m. from Newton): an average wait
12: { name: 'Law School Express (morning)', loop: false, service: [{ days: WEEKDAYS, from: '06:55', to: '09:25', every: 45 }] },
21: { name: 'Law School Express (afternoon)', loop: false, service: [{ days: WEEKDAYS, from: '15:05', to: '17:05', every: 110 }] },
9: { name: 'Mall and Grocery Shuttle (Saturday)', loop: false, service: [{ days: [6], from: '11:00', to: '15:05', every: 60 }] },
25: { name: 'Mall and Grocery Shuttle (Sunday)', loop: false, service: [{ days: [0], from: '11:00', to: '16:05', every: 60 }] },
24: { name: 'Early Morning Shuttle (Newton)', loop: false, service: [
{ days: WEEKDAYS, from: '05:55', to: '06:35', every: 30 },
{ days: WEEKEND, from: '06:55', to: '07:35', every: 30 },
] },
};

async function tracker(method, params = {}) {
const url = new URL(method, TRACKER);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { 'User-Agent': 'AccessBC/1.0 (Boston College accessibility map)' } });
if (!res.ok) throw new Error(`${method}: ${res.status}`);
return res.json();
}

// Google's encoded polyline format (precision 5) to [[lon, lat]]
function decodePolyline(str) {
const out = [];
let i = 0, lat = 0, lon = 0;
const next = () => {
let result = 0, shift = 0, b;
do { b = str.charCodeAt(i++) - 63; result |= (b & 0x1f) << shift; shift += 5; } while (b >= 0x20);
return result & 1 ? ~(result >> 1) : result >> 1;
};
while (i < str.length) {
lat += next();
lon += next();
out.push([+(lon / 1e5).toFixed(5), +(lat / 1e5).toFixed(5)]);
}
return out;
}

const metres = ([lon1, lat1], [lon2, lat2]) => Math.hypot((lon2 - lon1) * 111320 * Math.cos((lat1 * Math.PI) / 180), (lat2 - lat1) * 110540);

// The index of the route line's point nearest each stop, in order along the line (a loop may pass a place twice)
function stopIndexes(line, stops) {
const out = [];
let from = 0;
for (const stop of stops) {
let best = { i: from, d: Infinity };
for (let i = from; i < line.length; i++) {
const d = metres(line[i], stop.lonlat);
if (d < best.d) best = { i, d };
}
out.push(best.i);
from = best.i;
}
return out;
}

async function main() {
console.log('Reading BC’s public shuttle tracker…');
const config = await tracker('GetMapConfig');
const routes = await tracker('GetRoutesForMapWithScheduleWithEncodedLine', { apiKey: config.ApiKey, isDispatch: 'false' });
const out = [];
for (const [id, info] of Object.entries(SERVICE)) {
const r = routes.find((x) => String(x.RouteID) === id);
if (!r) { console.warn(` route ${id} (${info.name}) isn't in the tracker any more: check SERVICE`); continue; }
const line = decodePolyline(r.EncodedPolyline || '');
let stops = r.Stops.map((s) => ({
name: s.Description.replace(/\s*\(\d+\)\s*$/, '').trim(),
lonlat: [+s.Longitude.toFixed(6), +s.Latitude.toFixed(6)],
rideToNext: s.SecondsToNextStop,
dwell: s.SecondsAtStop,
}));
// A loop listed with its first stop again at the end: keep it once
if (stops.length > 1 && metres(stops[0].lonlat, stops.at(-1).lonlat) < 30 && stops[0].name === stops.at(-1).name) stops = stops.slice(0, -1);
const at = stopIndexes(line, stops);
stops.forEach((s, k) => { s.at = at[k]; });
out.push({ id: Number(id), name: info.name, tracker: r.Description, color: r.MapLineColor, loop: info.loop, service: info.service, stops, line });
console.log(` ${info.name}: ${stops.map((s) => s.name).join(' → ')}${info.loop ? ' → (back to start)' : ''}`);
}
const data = {
source: 'Snapshot of BC’s public shuttle tracker (bostoncollege.transloc.com); schedules from BC’s shuttle page',
schedulePage: SCHEDULE_PAGE,
snapshot: new Date().toISOString().slice(0, 10),
routes: out,
};
fs.writeFileSync(OUT, JSON.stringify(data) + '\n');
console.log(`Wrote ${path.relative(ROOT, OUT)}: ${out.length} routes, ${(fs.statSync(OUT).size / 1e3).toFixed(0)} kB`);
}

main().catch((err) => { console.error(err); process.exit(1); });
9 changes: 9 additions & 0 deletions src/geojson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ unpaved ways, ways inside a BC campus boundary, and bridges and tunnels. None of
accessibility, and the app says so on every route that uses it. Many curb ramps aren't tagged in OpenStreetMap
yet; adding them there (`kerb=lowered` on the crossing's end nodes) improves the routes after the next refresh.

## BC shuttles (`shuttles.json`)

BC's regular shuttle routes, built by [scripts/shuttles.mjs](../../scripts/shuttles.mjs) from a snapshot of BC's
public live shuttle tracker (bostoncollege.transloc.com): each route's stops in order, the ride time between them,
and the route line. The days, hours and frequencies aren't in the tracker data; they're typed into the script from
[BC's shuttle page](https://www.bc.edu/bc-web/offices/aux-services/sites/transportation-parking/shuttles.html).
Re-run the script and check the schedules each semester. Event routes (football, holidays) aren't included.
Whether the shuttles have wheelchair lifts or ramps isn't published, so the app says it isn't confirmed.

## Known quirks

- Field names were cut to 10 characters by the shapefile export (`Descriptio`, `Food_dinin`,
Expand Down
1 change: 1 addition & 0 deletions src/geojson/shuttles.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ hr {
border-top-style: dotted;
}

.route-swatch.shuttle {
border-top-color: #6b2fa3;
border-top-style: dashed;
}

.route-options {
margin: 0.75rem 0 0.25rem;
border: 0.1rem solid #ddd;
Expand Down
Loading
Loading