Compare commits

..

4 Commits

4 changed files with 5169 additions and 1758 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
https://wiki.openstreetmap.org/wiki/Montpellier/Transports_en_commun
http://data.montpellier3m.fr/dataset/offre-de-transport-tam-en-temps-reel

View File

@ -23,15 +23,31 @@ h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw`;
const network = require('../tam/network.json');
const getRouteColors = routes =>
{
const colors = routes.filter(
// Only consider normal routes (excluding alternate routes)
([lineRef, routeRef]) =>
network.lines[lineRef].routes[routeRef].state === 'normal'
).map(([lineRef]) => network.lines[lineRef].color);
if (colors.length >= 1)
{
return colors;
}
return ['#FFFFFF'];
};
const makeDataSources = async () =>
{
const segmentsSource = new VectorSource();
const stopsSource = new VectorSource();
segmentsSource.addFeatures(
Object.values(network.segments).map(({lines, points}) =>
Object.values(network.segments).map(({routes, points}) =>
new Feature({
colors: lines.map(line => network.lines[line].color),
colors: getRouteColors(routes),
geometry: new LineString(points.map(
({lat, lon}) => proj.fromLonLat([lon, lat])
)),
@ -40,9 +56,9 @@ const makeDataSources = async () =>
);
stopsSource.addFeatures(
Object.values(network.stops).map(({lines, lon, lat}) =>
Object.values(network.stops).map(({routes, lon, lat}) =>
new Feature({
colors: lines.map(line => network.lines[line].color),
colors: getRouteColors(routes),
geometry: new Point(proj.fromLonLat([lon, lat])),
})
)

View File

@ -17,111 +17,6 @@ const util = require('../util');
const osm = require('./sources/osm');
const tam = require('./sources/tam');
/**
* Use theoretical passings data to guess which lines use which stops in which
* direction.
*
* This is used for suggesting possible stop IDs for stops that dont have
* one in OSM.
*
* @return Map containing for each stop its abbreviated name, the lines that
* use it and in which directions it is used.
*/
const fetchStopsRefAssociations = () => new Promise((res, rej) =>
{
const stops = {};
tam.fetchTheoretical((err, row) =>
{
if (err)
{
rej(err);
return;
}
if (!row)
{
res(stops);
return;
}
let line = row.routeShortName;
if (line === '4')
{
line += row.directionId === '0' ? 'A' : 'B';
}
if (!(row.stopId in stops))
{
stops[row.stopId] = {
name: row.stopName,
lines: new Set([line]),
directions: new Set([row.tripHeadsign]),
};
}
else
{
const stop = stops[row.stopId];
if (stop.name !== row.stopName)
{
console.warn(`Stop ${row.stopId} has multiple names: \
${row.stopName} and ${stop.name}. Only the first one will be considered.`);
}
stop.lines.add(line);
stop.directions.add(row.tripHeadsign);
}
});
});
// Mapping for abbreviations used in stop names
const stopAbbreviations = {
st: 'saint',
};
/**
* Convert a stop name to a canonical representation suitable for
* comparing two names.
*
* @param stopName Original stop name.
* @return List of normalized tokens in the name.
*/
const canonicalizeStopName = stopName => stopName
.toLowerCase()
// Remove diacritics
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
// Only keep alpha-numeric characters
.replace(/[^a-z0-9]/g, ' ')
// Split in tokens longer than two characters
.split(/\s+/g).filter(part => part.length >= 2)
// Resolve well-known abbreviations
.map(part => part in stopAbbreviations ? stopAbbreviations[part] : part);
/**
* Compute a matching score between two stop names.
*
* @param fullName Stop name in full.
* @param abbrName Abbreviated stop name.
* @return Matching score (number of common tokens).
*/
const matchStopNames = (fullName, abbrName) =>
{
const canonicalFullName = canonicalizeStopName(fullName);
const canonicalAbbrName = canonicalizeStopName(abbrName);
return canonicalFullName.filter(part =>
canonicalAbbrName.findIndex(abbrPart =>
part.startsWith(abbrPart)
) !== -1
).length;
};
/**
* Fetch stops and lines of the network.
*
@ -142,9 +37,6 @@ relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join('|')})$"];
out body qt;
`);
// Retrieve stop associations from TaM
const associations = await fetchStopsRefAssociations();
// List of retrieved objects
const elementsList = rawData.elements;
@ -173,11 +65,15 @@ out body qt;
const color = routeMaster.tags.colour || '#000000';
// Extract all routes for the given line
const rawRoutes = routeMaster.members.map(({ref}) => elements[ref]);
const routes = [];
// Add missing stops to the result object
for (let route of rawRoutes)
for (let [routeRef, {ref: routeId}] of routeMaster.members.entries())
{
const route = elements[routeId];
const {from, to, name} = route.tags;
const state = route.tags.state || 'normal';
// Add missing stops to the global stops object
for (let {ref, role} of route.members)
{
if (role === 'stop')
@ -186,69 +82,9 @@ out body qt;
if (!('ref' in stop.tags))
{
console.warn(`Stop ${stop.id} is missing a “ref” tag
Name: ${stop.tags.name}
Part of line: ${route.tags.name}
URI: ${osm.viewNode(stop.id)}
`);
// Try to identify stops matching this stop in the
// TaM-provided data, using the stop name, line number
// and trip direction
const candidates = Object.entries(associations).filter(
([, {lines}]) => lines.has(route.tags.ref)
).map(([stopRef, {name, lines, directions}]) => ({
stopRef,
lines,
name,
nameScore: matchStopNames(stop.tags.name, name),
directions,
directionScore: Math.max(
...Array.from(directions).map(direction =>
matchStopNames(route.tags.to, direction)
)
),
}))
// Only keep non-zero scores for both criteria
.filter(({nameScore, directionScore}) =>
nameScore && directionScore
)
// Sort by best name score then best direction
.sort(({
nameScore: nameScore1,
directionScore: directionScore1,
}, {
nameScore: nameScore2,
directionScore: directionScore2,
}) =>
(nameScore2 - nameScore1)
|| (directionScore2 - directionScore1)
)
.slice(0, 4);
if (candidates.length === 0)
{
console.warn('No candidate found in TaM data.');
}
else
{
console.warn('Candidates:');
for (let candidate of candidates)
{
console.warn(`\
Stop ${candidate.stopRef} with name ${candidate.name} used by \
${util.choosePlural(candidate.lines.length, 'line', '.s')} \
${util.joinSentence(Array.from(candidate.lines), ', ', ' and ')} going to \
${util.joinSentence(Array.from(candidate.directions), ', ', ' or ')}
Apply in JOSM: ${osm.addTagsToNode(stop.id, ['ref=' + candidate.stopRef])}
`);
}
}
console.warn('');
throw new Error(`Stop ${stop.id}
(${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing
a ref tag`);
}
if (!(stop.tags.ref in stops))
@ -257,23 +93,15 @@ ${util.joinSentence(Array.from(candidate.directions), ', ', ' or ')}
lat: stop.lat,
lon: stop.lon,
name: stop.tags.name,
lines: new Set([lineRef]),
routes: [[lineRef, routeRef]],
};
}
else
{
stops[stop.tags.ref].lines.add(lineRef);
stops[stop.tags.ref].routes.push([lineRef, routeRef]);
}
}
}
}
// Reconstruct the lines route from stop to stop
const routes = [];
for (let route of rawRoutes)
{
const {from, to, name} = route.tags;
// Check that the route consists of a block of stops and platforms
// followed by a block of routes as dictated by PTv2
@ -300,7 +128,7 @@ of ${name}`);
// List of stops in the route, expected to be in the timetable
// order as per PTv2 and to be traversed in order by the sequence
// of ways extracted below
const stops = route.members.slice(0, relationPivot)
const lineStops = route.members.slice(0, relationPivot)
.filter(({role}) => role === 'stop')
.map(({ref}) => ref);
@ -311,7 +139,7 @@ of ${name}`);
// Merge all used ways in a single path
let path = [];
let currentNode = stops[0];
let currentNode = lineStops[0];
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
{
@ -372,15 +200,15 @@ ${name} is one-way and cannot be used in reverse.`);
}
// Split the path into segments between stops
for (let stopIndex = 0; stopIndex + 1 < stops.length; ++stopIndex)
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx)
{
const begin = elements[stops[stopIndex]].tags.ref;
const end = elements[stops[stopIndex + 1]].tags.ref;
const begin = elements[lineStops[stopIdx]].tags.ref;
const end = elements[lineStops[stopIdx + 1]].tags.ref;
const id = `${begin}-${end}`;
const nodes = path.slice(
path.indexOf(stops[stopIndex]),
path.indexOf(stops[stopIndex + 1]) + 1,
path.indexOf(lineStops[stopIdx]),
path.indexOf(lineStops[stopIdx + 1]) + 1,
);
if (id in segments)
@ -391,7 +219,7 @@ ${name} is one-way and cannot be used in reverse.`);
different sequence of nodes in two or more lines.`);
}
segments[id].lines.add(lineRef);
segments[id].routes.push([lineRef, routeRef]);
}
else
{
@ -418,15 +246,17 @@ different sequence of nodes in two or more lines.`);
// Keep track of the original sequence of nodes to
// compare with duplicates
nodes,
points,
lines: new Set([lineRef]),
routes: [[lineRef, routeRef]],
};
}
}
routes.push({
from, to, name,
stops: stops.map(id => elements[id].tags.ref),
from, to,
name, state,
stops: lineStops.map(id => elements[id].tags.ref),
});
}

File diff suppressed because it is too large Load Diff