Compare commits
No commits in common. "76a187bbb6d8b4e440dc7fe3d7a68332f187a6f3" and "476a0787a1972ebcd9534235cabddb8e2ed913c2" have entirely different histories.
76a187bbb6
...
476a0787a1
|
@ -1,3 +0,0 @@
|
||||||
https://wiki.openstreetmap.org/wiki/Montpellier/Transports_en_commun
|
|
||||||
|
|
||||||
http://data.montpellier3m.fr/dataset/offre-de-transport-tam-en-temps-reel
|
|
|
@ -23,31 +23,15 @@ h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw`;
|
||||||
|
|
||||||
const network = require('../tam/network.json');
|
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 makeDataSources = async () =>
|
||||||
{
|
{
|
||||||
const segmentsSource = new VectorSource();
|
const segmentsSource = new VectorSource();
|
||||||
const stopsSource = new VectorSource();
|
const stopsSource = new VectorSource();
|
||||||
|
|
||||||
segmentsSource.addFeatures(
|
segmentsSource.addFeatures(
|
||||||
Object.values(network.segments).map(({routes, points}) =>
|
Object.values(network.segments).map(({lines, points}) =>
|
||||||
new Feature({
|
new Feature({
|
||||||
colors: getRouteColors(routes),
|
colors: lines.map(line => network.lines[line].color),
|
||||||
geometry: new LineString(points.map(
|
geometry: new LineString(points.map(
|
||||||
({lat, lon}) => proj.fromLonLat([lon, lat])
|
({lat, lon}) => proj.fromLonLat([lon, lat])
|
||||||
)),
|
)),
|
||||||
|
@ -56,9 +40,9 @@ const makeDataSources = async () =>
|
||||||
);
|
);
|
||||||
|
|
||||||
stopsSource.addFeatures(
|
stopsSource.addFeatures(
|
||||||
Object.values(network.stops).map(({routes, lon, lat}) =>
|
Object.values(network.stops).map(({lines, lon, lat}) =>
|
||||||
new Feature({
|
new Feature({
|
||||||
colors: getRouteColors(routes),
|
colors: lines.map(line => network.lines[line].color),
|
||||||
geometry: new Point(proj.fromLonLat([lon, lat])),
|
geometry: new Point(proj.fromLonLat([lon, lat])),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,6 +17,111 @@ const util = require('../util');
|
||||||
const osm = require('./sources/osm');
|
const osm = require('./sources/osm');
|
||||||
const tam = require('./sources/tam');
|
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 don’t 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.
|
* Fetch stops and lines of the network.
|
||||||
*
|
*
|
||||||
|
@ -37,6 +142,9 @@ relation[network="TaM"][type="route_master"][ref~"^(${lineRefs.join('|')})$"];
|
||||||
out body qt;
|
out body qt;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Retrieve stop associations from TaM
|
||||||
|
const associations = await fetchStopsRefAssociations();
|
||||||
|
|
||||||
// List of retrieved objects
|
// List of retrieved objects
|
||||||
const elementsList = rawData.elements;
|
const elementsList = rawData.elements;
|
||||||
|
|
||||||
|
@ -65,15 +173,11 @@ out body qt;
|
||||||
const color = routeMaster.tags.colour || '#000000';
|
const color = routeMaster.tags.colour || '#000000';
|
||||||
|
|
||||||
// Extract all routes for the given line
|
// Extract all routes for the given line
|
||||||
const routes = [];
|
const rawRoutes = routeMaster.members.map(({ref}) => elements[ref]);
|
||||||
|
|
||||||
for (let [routeRef, {ref: routeId}] of routeMaster.members.entries())
|
// Add missing stops to the result object
|
||||||
|
for (let route of rawRoutes)
|
||||||
{
|
{
|
||||||
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)
|
for (let {ref, role} of route.members)
|
||||||
{
|
{
|
||||||
if (role === 'stop')
|
if (role === 'stop')
|
||||||
|
@ -82,9 +186,69 @@ out body qt;
|
||||||
|
|
||||||
if (!('ref' in stop.tags))
|
if (!('ref' in stop.tags))
|
||||||
{
|
{
|
||||||
throw new Error(`Stop ${stop.id}
|
console.warn(`Stop ${stop.id} is missing a “ref” tag
|
||||||
(${osm.viewNode(stop.id)}) on line ${route.tags.name} is missing
|
Name: ${stop.tags.name}
|
||||||
a “ref” tag`);
|
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('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(stop.tags.ref in stops))
|
if (!(stop.tags.ref in stops))
|
||||||
|
@ -93,15 +257,23 @@ a “ref” tag`);
|
||||||
lat: stop.lat,
|
lat: stop.lat,
|
||||||
lon: stop.lon,
|
lon: stop.lon,
|
||||||
name: stop.tags.name,
|
name: stop.tags.name,
|
||||||
routes: [[lineRef, routeRef]],
|
lines: new Set([lineRef]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
stops[stop.tags.ref].routes.push([lineRef, routeRef]);
|
stops[stop.tags.ref].lines.add(lineRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the line’s 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
|
// Check that the route consists of a block of stops and platforms
|
||||||
// followed by a block of routes as dictated by PTv2
|
// followed by a block of routes as dictated by PTv2
|
||||||
|
@ -128,7 +300,7 @@ of ${name}`);
|
||||||
// List of stops in the route, expected to be in the timetable
|
// 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
|
// order as per PTv2 and to be traversed in order by the sequence
|
||||||
// of ways extracted below
|
// of ways extracted below
|
||||||
const lineStops = route.members.slice(0, relationPivot)
|
const stops = route.members.slice(0, relationPivot)
|
||||||
.filter(({role}) => role === 'stop')
|
.filter(({role}) => role === 'stop')
|
||||||
.map(({ref}) => ref);
|
.map(({ref}) => ref);
|
||||||
|
|
||||||
|
@ -139,7 +311,7 @@ of ${name}`);
|
||||||
|
|
||||||
// Merge all used ways in a single path
|
// Merge all used ways in a single path
|
||||||
let path = [];
|
let path = [];
|
||||||
let currentNode = lineStops[0];
|
let currentNode = stops[0];
|
||||||
|
|
||||||
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
|
for (let wayIndex = 0; wayIndex < ways.length; wayIndex += 1)
|
||||||
{
|
{
|
||||||
|
@ -200,15 +372,15 @@ ${name} is one-way and cannot be used in reverse.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the path into segments between stops
|
// Split the path into segments between stops
|
||||||
for (let stopIdx = 0; stopIdx + 1 < lineStops.length; ++stopIdx)
|
for (let stopIndex = 0; stopIndex + 1 < stops.length; ++stopIndex)
|
||||||
{
|
{
|
||||||
const begin = elements[lineStops[stopIdx]].tags.ref;
|
const begin = elements[stops[stopIndex]].tags.ref;
|
||||||
const end = elements[lineStops[stopIdx + 1]].tags.ref;
|
const end = elements[stops[stopIndex + 1]].tags.ref;
|
||||||
|
|
||||||
const id = `${begin}-${end}`;
|
const id = `${begin}-${end}`;
|
||||||
const nodes = path.slice(
|
const nodes = path.slice(
|
||||||
path.indexOf(lineStops[stopIdx]),
|
path.indexOf(stops[stopIndex]),
|
||||||
path.indexOf(lineStops[stopIdx + 1]) + 1,
|
path.indexOf(stops[stopIndex + 1]) + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (id in segments)
|
if (id in segments)
|
||||||
|
@ -219,7 +391,7 @@ ${name} is one-way and cannot be used in reverse.`);
|
||||||
different sequence of nodes in two or more lines.`);
|
different sequence of nodes in two or more lines.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
segments[id].routes.push([lineRef, routeRef]);
|
segments[id].lines.add(lineRef);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -246,17 +418,15 @@ different sequence of nodes in two or more lines.`);
|
||||||
// Keep track of the original sequence of nodes to
|
// Keep track of the original sequence of nodes to
|
||||||
// compare with duplicates
|
// compare with duplicates
|
||||||
nodes,
|
nodes,
|
||||||
|
|
||||||
points,
|
points,
|
||||||
routes: [[lineRef, routeRef]],
|
lines: new Set([lineRef]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
from, to,
|
from, to, name,
|
||||||
name, state,
|
stops: stops.map(id => elements[id].tags.ref),
|
||||||
stops: lineStops.map(id => elements[id].tags.ref),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
6680
src/tam/network.json
6680
src/tam/network.json
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue