Implements hardcoded subway line plotting on the interactive map in WorkingSubwayMap.tsx. Use when adding a new subway line, fixing station data for an existing line, updating transfer information, or debugging map rendering issues. Covers station coordinates, GeoJSON line paths, marker creation, glassmorphic tooltips, and the toggleLine cleanup lifecycle.
Plot NYC subway lines accurately on the interactive MapLibre GL map.
NYC subway stations sharing the same name can be completely different physical locations. The same name does NOT mean the same station.
Example — "Chambers St":
[-74.009266, 40.713243][-74.003739, 40.713065][-74.009390, 40.715478]These are separate facilities with different coordinates. Each line's station array must use the correct platform's coordinates, not just any "Chambers St."
Example — "14 St":
ALWAYS extract coordinates from /data/stations-normalized.json. NEVER fabricate coordinates.
node -e "
const data = require('./data/stations-normalized.json');
const stations = data.filter(s => s.lines.includes('LINE_ID'));
stations.forEach((s, i) => console.log(\`\${i+1}. \${s.name} [\${s.longitude}, \${s.latitude}] lines: \${s.lines.join(',')}\`));
"
Cross-reference with the line's *_LINE_IMPLEMENTATION.md file in the project root for station ordering, branch structure, and transfer details.
All lines are implemented in /components/WorkingSubwayMap.tsx inside the toggleLine function.
const HARDCODED_LINES: Record<string, { stationCount: number }> = {
// ...existing lines
'X': { stationCount: N }, // exact count matters for cleanup
};
const xLineStations = [
{
name: "Station Name",
coordinates: [-73.xxx, 40.xxx] as [number, number],
lines: ['X', 'Y', 'Z'] // ONLY lines at THIS platform
},
];
Transfer lines rules:
stations-normalized.json platforms array and the *_LINE_IMPLEMENTATION.md for accuracyconst xLineCoords: [number, number][] = xLineStations.map(s => s.coordinates);
const lineGeoJSON = {
type: 'Feature' as const,
properties: {},
geometry: { type: 'LineString' as const, coordinates: xLineCoords },
};
if (!map.current!.getSource(`line-${lineId}`)) {
map.current!.addSource(`line-${lineId}`, { type: 'geojson', data: lineGeoJSON });
}
if (!map.current!.getLayer(`line-${lineId}`)) {
map.current!.addLayer({
id: `line-${lineId}`,
type: 'line',
source: `line-${lineId}`,
paint: { 'line-color': MTA_COLORS[lineId] || '#000000', 'line-width': 4, 'line-opacity': 0.8 },
});
}
Branching lines (A, 2, 5, etc.): Use multiple LineString segments or a MultiLineString. See references/branching.md.
For each station, create a GeoJSON point source, a circle layer, and mouse event handlers:
xLineStations.forEach((station, index) => {
const stationId = `station-${lineId}-${index}`;
// Source
if (!map.current!.getSource(stationId)) {
map.current!.addSource(stationId, {
type: 'geojson',
data: {
type: 'Feature',
properties: { name: station.name },
geometry: { type: 'Point', coordinates: station.coordinates },
},
});
}
// Layer
if (!map.current!.getLayer(stationId)) {
map.current!.addLayer({
id: stationId,
type: 'circle',
source: stationId,
paint: {
'circle-radius': 8,
'circle-color': MTA_COLORS[lineId] || '#000000',
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff',
},
});
}
// Tooltip (mouseenter)
map.current!.on('mouseenter', stationId, (e) => {
if (e.features && e.features[0]) {
const coordinates = (e.features[0].geometry as any).coordinates.slice();
const allLines = station.lines;
hoverPopup = new maplibregl.Popup({
closeButton: false, closeOnClick: false,
className: 'glassmorphic-tooltip', offset: 25, maxWidth: '280px'
})
.setLngLat(coordinates)
.setHTML(`
<div class="glassmorphic-tooltip-content">
<div class="font-semibold text-sm mb-2" style="color: white;">${station.name}</div>
<div class="flex gap-1.5 flex-wrap">
${allLines.map(line => `
<span class="inline-block w-6 h-6 rounded-full text-xs font-bold text-center leading-6"
style="background-color: ${MTA_COLORS[line] || '#000000'}; color: white;">
${line}
</span>
`).join('')}
</div>
</div>
`)
.addTo(map.current!);
}
});
map.current!.on('mouseleave', stationId, () => {
if (hoverPopup) { hoverPopup.remove(); hoverPopup = null; }
});
});
The HARDCODED_LINES[lineId].stationCount drives cleanup. If stationCount is wrong, orphan layers remain on the map.
// Remove line layer + source
if (map.current.getLayer(`line-${lineId}`)) map.current.removeLayer(`line-${lineId}`);
if (map.current.getSource(`line-${lineId}`)) map.current.removeSource(`line-${lineId}`);
// Remove markers
if (lineMarkers[lineId]) {
lineMarkers[lineId].forEach(m => m.remove());
setLineMarkers(prev => { const u = { ...prev }; delete u[lineId]; return u; });
}
// Remove station layers + sources
for (let i = 0; i < HARDCODED_LINES[lineId].stationCount; i++) {
const sid = `station-${lineId}-${i}`;
if (map.current.getLayer(sid)) map.current.removeLayer(sid);
if (map.current.getSource(sid)) map.current.removeSource(sid);
}
glassmorphic-tooltip, gap-1.5, badge sizes)After implementing a line:
references/mta-line-maps.md)stations-normalized.json filtered by line (not fabricated)HARDCODED_LINES matches array length exactly*_LINE_IMPLEMENTATION.md files in project root for implementation details