[refactor] Reduce complexity by refactoring
- Extracted csv related operations - Divided in smaller functions - Externalized classes
This commit is contained in:
parent
d4e04eaef6
commit
5406582d34
6 changed files with 490 additions and 234 deletions
168
src/main/java/fr/u_paris/gla/project/idfm/CSVStreamProvider.java
Normal file
168
src/main/java/fr/u_paris/gla/project/idfm/CSVStreamProvider.java
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package fr.u_paris.gla.project.idfm;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import fr.u_paris.gla.project.io.NetworkFormat;
|
||||||
|
import fr.u_paris.gla.project.utils.GPS;
|
||||||
|
|
||||||
|
public final class CSVStreamProvider {
|
||||||
|
private static final NumberFormat GPS_FORMATTER = NetworkFormat
|
||||||
|
.getGPSFormatter();
|
||||||
|
private static final NumberFormat MINUTES_SECOND_FORMATTER = NumberFormat
|
||||||
|
.getInstance(Locale.ENGLISH);
|
||||||
|
static {
|
||||||
|
MINUTES_SECOND_FORMATTER.setMinimumIntegerDigits(2);
|
||||||
|
}
|
||||||
|
/** Number of seconds in a minute. */
|
||||||
|
private static final int SECONDS_IN_MINUTES = 60;
|
||||||
|
private static final long SECONDS_IN_HOURS = 3_600;
|
||||||
|
// Magically chosen values
|
||||||
|
/** Maximal speed in km/h */
|
||||||
|
private static final double MAX_SPEED = 5;
|
||||||
|
/** Distance to reach maximal speed in km */
|
||||||
|
private static final double TWO_ACCELERATION_DISTANCE = 0.2;
|
||||||
|
|
||||||
|
private String[] line = new String[NetworkFormat.NUMBER_COLUMNS];
|
||||||
|
|
||||||
|
private Iterator<TraceEntry> currentTrace;
|
||||||
|
private Iterator<List<StopEntry>> currentPath = Collections.emptyIterator();
|
||||||
|
private Iterator<StopEntry> currentSegmentStart = Collections.emptyIterator();
|
||||||
|
private Iterator<StopEntry> currentSegmentEnd = Collections.emptyIterator();
|
||||||
|
Map<StopEntry, Set<StopEntry>> lineSegments = new HashMap<>();
|
||||||
|
|
||||||
|
private StopEntry start = null;
|
||||||
|
private StopEntry end = null;
|
||||||
|
|
||||||
|
private boolean hasNext = false;
|
||||||
|
private boolean onNext = false;
|
||||||
|
|
||||||
|
/** Create the stream provider */
|
||||||
|
public CSVStreamProvider(Iterator<TraceEntry> traces) {
|
||||||
|
this.currentTrace = traces;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
if (!this.onNext) {
|
||||||
|
skipToNext();
|
||||||
|
}
|
||||||
|
return this.hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skipToNext() {
|
||||||
|
if (this.onNext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (!this.onNext) {
|
||||||
|
if (!this.currentSegmentEnd.hasNext()) {
|
||||||
|
skipToNextCandidatePath();
|
||||||
|
}
|
||||||
|
if (this.onNext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skipToNextNewSegment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skipToNextNewSegment() {
|
||||||
|
do {
|
||||||
|
this.start = this.currentSegmentStart.next();
|
||||||
|
this.lineSegments.putIfAbsent(this.start, new HashSet<>());
|
||||||
|
this.end = this.currentSegmentEnd.next();
|
||||||
|
} while (this.lineSegments.get(this.start).contains(this.end)
|
||||||
|
&& this.currentSegmentEnd.hasNext());
|
||||||
|
if (!this.lineSegments.get(this.start).contains(this.end)) {
|
||||||
|
this.lineSegments.get(this.start).add(this.end);
|
||||||
|
this.onNext = true;
|
||||||
|
this.hasNext = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move the reading head of path to the next one that has at least two
|
||||||
|
* elements */
|
||||||
|
private void skipToNextCandidatePath() {
|
||||||
|
currentSegmentStart = null;
|
||||||
|
do {
|
||||||
|
while (!this.currentPath.hasNext()) {
|
||||||
|
if (!this.currentTrace.hasNext()) {
|
||||||
|
this.hasNext = false;
|
||||||
|
this.onNext = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TraceEntry trace = this.currentTrace.next();
|
||||||
|
this.currentPath = trace.getPaths().iterator();
|
||||||
|
this.line[NetworkFormat.LINE_INDEX] = trace.lname;
|
||||||
|
this.lineSegments.clear();
|
||||||
|
}
|
||||||
|
List<StopEntry> path = this.currentPath.next();
|
||||||
|
this.currentSegmentEnd = path.iterator();
|
||||||
|
if (this.currentSegmentEnd.hasNext()) {
|
||||||
|
this.currentSegmentEnd.next();
|
||||||
|
this.currentSegmentStart = path.iterator();
|
||||||
|
}
|
||||||
|
} while (currentSegmentStart == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] next() {
|
||||||
|
if (!this.onNext) {
|
||||||
|
skipToNext();
|
||||||
|
}
|
||||||
|
this.onNext = false;
|
||||||
|
|
||||||
|
fillStation(this.start, this.line, NetworkFormat.START_INDEX);
|
||||||
|
fillStation(this.end, this.line, NetworkFormat.STOP_INDEX);
|
||||||
|
double distance = GPS.distance(this.start.latitude, this.start.longitude,
|
||||||
|
this.end.latitude, this.end.longitude);
|
||||||
|
this.line[NetworkFormat.DISTANCE_INDEX] = NumberFormat.getInstance(Locale.ENGLISH)
|
||||||
|
.format(distance);
|
||||||
|
this.line[NetworkFormat.DURATION_INDEX] = formatTime(
|
||||||
|
(long) Math.ceil(distanceToTime(distance) * SECONDS_IN_HOURS));
|
||||||
|
this.line[NetworkFormat.VARIANT_INDEX] = Integer
|
||||||
|
.toString(this.lineSegments.get(this.start).size() - 1);
|
||||||
|
|
||||||
|
return Arrays.copyOf(this.line, this.line.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param stop1
|
||||||
|
* @param nextLine
|
||||||
|
* @param i */
|
||||||
|
private static void fillStation(StopEntry stop, String[] nextLine, int index) {
|
||||||
|
nextLine[index] = stop.lname;
|
||||||
|
nextLine[index + 1] = MessageFormat.format("{0}, {1}", //$NON-NLS-1$
|
||||||
|
GPS_FORMATTER.format(stop.latitude),
|
||||||
|
GPS_FORMATTER.format(stop.longitude));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param distanceToTime
|
||||||
|
* @return */
|
||||||
|
private static String formatTime(long time) {
|
||||||
|
return MessageFormat.format("{0}:{1}", //$NON-NLS-1$
|
||||||
|
MINUTES_SECOND_FORMATTER.format(time / SECONDS_IN_MINUTES), MINUTES_SECOND_FORMATTER.format(time % SECONDS_IN_MINUTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A tool method to give a delay to go through a certain distance.
|
||||||
|
* <p>
|
||||||
|
* This is a model with an linear acceleration and deceleration periods and a
|
||||||
|
* constant speed in between.
|
||||||
|
*
|
||||||
|
* @param distance the distance (in km)
|
||||||
|
* @return the duration of the trip (in hours) */
|
||||||
|
private static double distanceToTime(double distance) {
|
||||||
|
return Math.max(0, distance - TWO_ACCELERATION_DISTANCE) / MAX_SPEED
|
||||||
|
+ Math.pow(Math.min(distance, TWO_ACCELERATION_DISTANCE) / MAX_SPEED, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,51 +3,31 @@
|
||||||
*/
|
*/
|
||||||
package fr.u_paris.gla.project.idfm;
|
package fr.u_paris.gla.project.idfm;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.Reader;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.text.Format;
|
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.text.NumberFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import com.opencsv.CSVParserBuilder;
|
import fr.u_paris.gla.project.utils.CSVTools;
|
||||||
import com.opencsv.CSVReader;
|
|
||||||
import com.opencsv.CSVReaderBuilder;
|
|
||||||
import com.opencsv.CSVWriterBuilder;
|
|
||||||
import com.opencsv.ICSVParser;
|
|
||||||
import com.opencsv.ICSVWriter;
|
|
||||||
import com.opencsv.exceptions.CsvValidationException;
|
|
||||||
|
|
||||||
import fr.u_paris.gla.project.io.NetworkFormat;
|
|
||||||
import fr.u_paris.gla.project.utils.GPS;
|
import fr.u_paris.gla.project.utils.GPS;
|
||||||
|
|
||||||
/** Code of an extractor for the data from IDF mobilite.
|
/** Code of an extractor for the data from IDF mobilite.
|
||||||
*
|
*
|
||||||
* @author Emmanuel Bigeon */
|
* @author Emmanuel Bigeon */
|
||||||
public class IDFMNetworkExtractor {
|
public class IDFMNetworkExtractor {
|
||||||
|
|
||||||
/** The logger for information on the process */
|
/** The logger for information on the process */
|
||||||
private static final Logger LOGGER = Logger
|
private static final Logger LOGGER = Logger
|
||||||
.getLogger(IDFMNetworkExtractor.class.getName());
|
.getLogger(IDFMNetworkExtractor.class.getName());
|
||||||
|
@ -68,83 +48,10 @@ public class IDFMNetworkExtractor {
|
||||||
|
|
||||||
// Magically chosen values
|
// Magically chosen values
|
||||||
/** A number of stops on each line */
|
/** A number of stops on each line */
|
||||||
private static final int GUESS_STOPS_BY_LINE = 5;
|
private static final int GUESS_STOPS_BY_LINE = 5;
|
||||||
/** Maximal speed in km/h */
|
|
||||||
private static final double MAX_SPEED = 5;
|
|
||||||
/** Distance to reach maximal speed in km */
|
|
||||||
private static final double ACCELERATION_DISTANCE = 0.1;
|
|
||||||
|
|
||||||
// Well named constants
|
// Well named constants
|
||||||
private static final double _250_METERS = .25;
|
private static final double QUARTER_KILOMETER = .25;
|
||||||
private static final long SECONDS_IN_HOURS = 3_600;
|
|
||||||
|
|
||||||
private static final Format GPS_FORMATTER = NetworkFormat.getGPSFormatter();
|
|
||||||
|
|
||||||
public static class StopEntry {
|
|
||||||
private String lname;
|
|
||||||
public final double longitude;
|
|
||||||
public final double latitude;
|
|
||||||
|
|
||||||
/** Create the stop
|
|
||||||
*
|
|
||||||
* @param lname
|
|
||||||
* @param longitude
|
|
||||||
* @param latitude */
|
|
||||||
public StopEntry(String lname, double longitude, double latitude) {
|
|
||||||
super();
|
|
||||||
this.lname = lname;
|
|
||||||
this.longitude = longitude;
|
|
||||||
this.latitude = latitude;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return MessageFormat.format("{0} [{1}, {2}]", this.lname, this.longitude,
|
|
||||||
this.latitude);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class UnidentifiedStopEntry extends StopEntry {
|
|
||||||
/** Create the stop
|
|
||||||
*
|
|
||||||
* @param longitude
|
|
||||||
* @param latitude */
|
|
||||||
public UnidentifiedStopEntry(double longitude, double latitude) {
|
|
||||||
super("Unidentified", longitude, latitude);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<StopEntry> candidates = new ArrayList<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "UnidentifiedStop [candidates=" + this.candidates + "]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TraceEntry {
|
|
||||||
String lname;
|
|
||||||
List<List<StopEntry>> stops = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void readCSVFromURL(String url, Consumer<String[]> contentLineConsumer)
|
|
||||||
throws IOException {
|
|
||||||
ICSVParser parser = new CSVParserBuilder().withSeparator(';').build();
|
|
||||||
try (InputStream is = new URL(url).openStream();
|
|
||||||
Reader reader = new BufferedReader(
|
|
||||||
new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
|
||||||
CSVReaderBuilder csvBuilder = new CSVReaderBuilder(reader)
|
|
||||||
.withCSVParser(parser);
|
|
||||||
try (CSVReader csv = csvBuilder.build()) {
|
|
||||||
String[] line = csv.readNextSilently(); // Eliminate header
|
|
||||||
while (csv.peek() != null) {
|
|
||||||
line = csv.readNext();
|
|
||||||
contentLineConsumer.accept(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (CsvValidationException e) {
|
|
||||||
throw new IOException("Invalid csv file for lines", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Main entry point for the extractor of IDF mobilite data into a network as
|
/** Main entry point for the extractor of IDF mobilite data into a network as
|
||||||
* defined by this application.
|
* defined by this application.
|
||||||
|
@ -159,165 +66,103 @@ public class IDFMNetworkExtractor {
|
||||||
|
|
||||||
Map<String, TraceEntry> traces = new HashMap<>();
|
Map<String, TraceEntry> traces = new HashMap<>();
|
||||||
try {
|
try {
|
||||||
readCSVFromURL(TRACE_FILE_URL, (String[] line) -> {
|
CSVTools.readCSVFromURL(TRACE_FILE_URL,
|
||||||
TraceEntry entry = new TraceEntry();
|
(String[] line) -> addLine(line, traces));
|
||||||
entry.lname = line[IDFM_TRACE_SNAME_INDEX];
|
|
||||||
List<List<StopEntry>> buildPaths = buildPaths(
|
|
||||||
line[IDFM_TRACE_SHAPE_INDEX]);
|
|
||||||
entry.stops.addAll(buildPaths);
|
|
||||||
if (buildPaths.isEmpty()) {
|
|
||||||
LOGGER.severe(() -> MessageFormat.format(
|
|
||||||
"Line {0} has no provided itinerary and was ignored",
|
|
||||||
entry.lname));
|
|
||||||
} else {
|
|
||||||
traces.put(line[IDFM_TRACE_ID_INDEX], entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.log(Level.SEVERE, "Error while reading the line paths", e);
|
LOGGER.log(Level.SEVERE, "Error while reading the line paths", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<StopEntry> stops = new ArrayList<>(traces.size() * GUESS_STOPS_BY_LINE);
|
List<StopEntry> stops = new ArrayList<>(traces.size() * GUESS_STOPS_BY_LINE);
|
||||||
try {
|
try {
|
||||||
readCSVFromURL(STOPS_FILE_URL, (String[] line) -> {
|
CSVTools.readCSVFromURL(STOPS_FILE_URL,
|
||||||
StopEntry entry = new StopEntry(line[IDFM_STOPS_NAME_INDEX],
|
(String[] line) -> addStop(line, traces, stops));
|
||||||
Double.parseDouble(line[IDFM_STOPS_LON_INDEX]),
|
|
||||||
Double.parseDouble(line[IDFM_STOPS_LAT_INDEX]));
|
|
||||||
String rid = line[IDFM_STOPS_RID_INDEX];
|
|
||||||
BiFunction<? super String, ? super TraceEntry, ? extends TraceEntry> func = (
|
|
||||||
k, trace) -> {
|
|
||||||
for (List<StopEntry> path : trace.stops) {
|
|
||||||
for (StopEntry stopEntry : path) {
|
|
||||||
if (stopEntry instanceof UnidentifiedStopEntry
|
|
||||||
&& GPS.distance(entry.latitude, entry.longitude,
|
|
||||||
stopEntry.latitude,
|
|
||||||
stopEntry.longitude) < _250_METERS) {
|
|
||||||
((UnidentifiedStopEntry) stopEntry).candidates.add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trace;
|
|
||||||
};
|
|
||||||
traces.computeIfPresent(rid, func);
|
|
||||||
stops.add(entry);
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.log(Level.SEVERE, "Error while reading the stops", e);
|
LOGGER.log(Level.SEVERE, "Error while reading the stops", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanTraces(traces);
|
||||||
|
|
||||||
|
CSVStreamProvider provider = new CSVStreamProvider(traces.values().iterator());
|
||||||
|
|
||||||
|
try {
|
||||||
|
CSVTools.writeCSVToFile(args[0], Stream.iterate(provider.next(),
|
||||||
|
t -> provider.hasNext(), t -> provider.next()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.log(Level.SEVERE, e,
|
||||||
|
() -> MessageFormat.format("Could not write in file {0}", args[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cleanTraces(Map<String, TraceEntry> traces) {
|
||||||
Set<String> toRemove = new HashSet<>();
|
Set<String> toRemove = new HashSet<>();
|
||||||
for (Entry<String, TraceEntry> traceEntry : traces.entrySet()) {
|
for (Entry<String, TraceEntry> traceEntry : traces.entrySet()) {
|
||||||
TraceEntry trace = traceEntry.getValue();
|
TraceEntry trace = traceEntry.getValue();
|
||||||
for (List<StopEntry> path : trace.stops) {
|
if (!cleanLine(trace.getPaths())) {
|
||||||
for (int i = 0; i < path.size(); i++) {
|
LOGGER.severe(() -> MessageFormat.format(
|
||||||
StopEntry stop = path.get(i);
|
"Missing stop for line {0}. Line will be removed", trace.lname));
|
||||||
if (stop instanceof UnidentifiedStopEntry)
|
toRemove.add(traceEntry.getKey());
|
||||||
stop = resolve((UnidentifiedStopEntry) stop);
|
|
||||||
if (stop instanceof UnidentifiedStopEntry
|
|
||||||
&& ((UnidentifiedStopEntry) stop).candidates.isEmpty()) {
|
|
||||||
LOGGER.severe("Missing stop for line " + trace.lname
|
|
||||||
+ ". Line will be removed");
|
|
||||||
toRemove.add(traceEntry.getKey());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
path.set(i, stop);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String string : toRemove) {
|
for (String string : toRemove) {
|
||||||
traces.remove(string);
|
traces.remove(string);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export content in required format
|
/** @param path */
|
||||||
try (FileWriter writer = new FileWriter(args[0], StandardCharsets.UTF_8)) {
|
private static boolean cleanLine(List<List<StopEntry>> stops) {
|
||||||
CSVWriterBuilder wBuilder = new CSVWriterBuilder(writer).withSeparator(';');
|
for (List<StopEntry> path : stops) {
|
||||||
try (ICSVWriter csv = wBuilder.build()) {
|
for (int i = 0; i < path.size(); i++) {
|
||||||
for (Entry<String, TraceEntry> traceEntry : traces.entrySet()) {
|
StopEntry stop = path.get(i);
|
||||||
Map<StopEntry, Set<StopEntry>> lineSegments = new HashMap<>();
|
if (!(stop instanceof UnidentifiedStopEntry)) {
|
||||||
String[] nextLine = new String[NetworkFormat.NUMBER_COLUMNS];
|
continue;
|
||||||
nextLine[NetworkFormat.LINE_INDEX] = traceEntry.getValue().lname;
|
}
|
||||||
for (List<StopEntry> path : traceEntry.getValue().stops) {
|
UnidentifiedStopEntry unidentified = (UnidentifiedStopEntry) stop;
|
||||||
for (int i = 0; i < path.size() - 1; i++) {
|
StopEntry stopResolution = unidentified.resolve();
|
||||||
StopEntry stop1 = path.get(i);
|
if (stopResolution == null) {
|
||||||
lineSegments.putIfAbsent(stop1, new HashSet<>());
|
return false;
|
||||||
StopEntry stop2 = path.get(i + 1);
|
}
|
||||||
if (!lineSegments.get(stop1).contains(stop2)) {
|
path.set(i, stopResolution);
|
||||||
fillStation(stop1, nextLine, NetworkFormat.START_INDEX);
|
}
|
||||||
fillStation(stop2, nextLine, NetworkFormat.STOP_INDEX);
|
}
|
||||||
double distance = GPS.distance(stop1.latitude,
|
return true;
|
||||||
stop1.longitude, stop2.latitude, stop2.longitude);
|
}
|
||||||
nextLine[NetworkFormat.DISTANCE_INDEX] = NumberFormat
|
|
||||||
.getInstance(Locale.ENGLISH).format(distance);
|
|
||||||
nextLine[NetworkFormat.DURATION_INDEX] = formatTime(
|
|
||||||
(long) Math.ceil(distanceToTime(distance)
|
|
||||||
* SECONDS_IN_HOURS));
|
|
||||||
nextLine[NetworkFormat.VARIANT_INDEX] = Integer
|
|
||||||
.toString(lineSegments.get(stop1).size());
|
|
||||||
csv.writeNext(nextLine);
|
|
||||||
lineSegments.get(stop1).add(stop2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private static void addStop(String[] line, Map<String, TraceEntry> traces,
|
||||||
|
List<StopEntry> stops) {
|
||||||
|
StopEntry entry = new StopEntry(line[IDFM_STOPS_NAME_INDEX],
|
||||||
|
Double.parseDouble(line[IDFM_STOPS_LON_INDEX]),
|
||||||
|
Double.parseDouble(line[IDFM_STOPS_LAT_INDEX]));
|
||||||
|
String rid = line[IDFM_STOPS_RID_INDEX];
|
||||||
|
traces.computeIfPresent(rid,
|
||||||
|
(String k, TraceEntry trace) -> addCandidate(trace, entry));
|
||||||
|
stops.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addLine(String[] line, Map<String, TraceEntry> traces) {
|
||||||
|
TraceEntry entry = new TraceEntry(line[IDFM_TRACE_SNAME_INDEX]);
|
||||||
|
List<List<StopEntry>> buildPaths = buildPaths(line[IDFM_TRACE_SHAPE_INDEX]);
|
||||||
|
entry.getPaths().addAll(buildPaths);
|
||||||
|
if (buildPaths.isEmpty()) {
|
||||||
|
LOGGER.severe(() -> MessageFormat.format(
|
||||||
|
"Line {0} has no provided itinerary and was ignored", entry.lname));
|
||||||
|
} else {
|
||||||
|
traces.put(line[IDFM_TRACE_ID_INDEX], entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TraceEntry addCandidate(TraceEntry trace, StopEntry entry) {
|
||||||
|
for (List<StopEntry> path : trace.getPaths()) {
|
||||||
|
for (StopEntry stopEntry : path) {
|
||||||
|
if (stopEntry instanceof UnidentifiedStopEntry unidentified
|
||||||
|
&& GPS.distance(entry.latitude, entry.longitude,
|
||||||
|
stopEntry.latitude,
|
||||||
|
stopEntry.longitude) < QUARTER_KILOMETER) {
|
||||||
|
unidentified.addCandidate(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.log(Level.SEVERE, e, () -> "Could not write in file " + args[1]);
|
|
||||||
}
|
}
|
||||||
}
|
return trace;
|
||||||
|
|
||||||
/** @param distanceToTime
|
|
||||||
* @return */
|
|
||||||
private static String formatTime(long time) {
|
|
||||||
NumberFormat format = NumberFormat.getInstance(Locale.ENGLISH);
|
|
||||||
format.setMinimumIntegerDigits(2);
|
|
||||||
return MessageFormat.format("{0}:{1}", format.format(time / 60),
|
|
||||||
format.format(time % 60));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A tool method to give a delay to go through a certain distance.
|
|
||||||
* <p>
|
|
||||||
* This is a model with an linear acceleration and deceleration periods and a
|
|
||||||
* constant speed in between.
|
|
||||||
*
|
|
||||||
* @param distance the distance (in km)
|
|
||||||
* @return the duration of the trip (in hours) */
|
|
||||||
private static double distanceToTime(double distance) {
|
|
||||||
return Math.max(0, distance - 2 * ACCELERATION_DISTANCE) / MAX_SPEED
|
|
||||||
+ Math.pow(Math.min(distance, 2 * ACCELERATION_DISTANCE) / MAX_SPEED, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param stop1
|
|
||||||
* @param nextLine
|
|
||||||
* @param i */
|
|
||||||
private static void fillStation(StopEntry stop, String[] nextLine, int index) {
|
|
||||||
nextLine[index] = stop.lname;
|
|
||||||
nextLine[index + 1] = MessageFormat.format("{0}, {1}",
|
|
||||||
GPS_FORMATTER.format(stop.latitude),
|
|
||||||
GPS_FORMATTER.format(stop.longitude));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param stop
|
|
||||||
* @return */
|
|
||||||
private static StopEntry resolve(UnidentifiedStopEntry stop) {
|
|
||||||
|
|
||||||
if (stop.candidates.isEmpty()) {
|
|
||||||
LOGGER.severe("Unable to find stop name, will use a placeholder");
|
|
||||||
return stop;
|
|
||||||
}
|
|
||||||
if (stop.candidates.size() == 1) {
|
|
||||||
return stop.candidates.get(0);
|
|
||||||
}
|
|
||||||
Collections.sort(stop.candidates,
|
|
||||||
(Comparator<? super StopEntry>) (StopEntry s1,
|
|
||||||
StopEntry s2) -> (int) Math.signum((GPS.distance(stop.latitude,
|
|
||||||
stop.longitude, s1.latitude, s1.longitude)
|
|
||||||
- GPS.distance(stop.latitude, stop.longitude, s2.latitude,
|
|
||||||
s2.longitude))));
|
|
||||||
|
|
||||||
return stop.candidates.get(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<List<StopEntry>> buildPaths(String pathsJSON) {
|
private static List<List<StopEntry>> buildPaths(String pathsJSON) {
|
||||||
|
@ -341,7 +186,8 @@ public class IDFMNetworkExtractor {
|
||||||
}
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
// Ignoring invalid element!
|
// Ignoring invalid element!
|
||||||
LOGGER.log(Level.FINE, "Invalid json element " + pathsJSON, e); //$NON-NLS-1$
|
LOGGER.log(Level.FINE, e,
|
||||||
|
() -> MessageFormat.format("Invalid json element {0}", pathsJSON)); //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
71
src/main/java/fr/u_paris/gla/project/idfm/StopEntry.java
Normal file
71
src/main/java/fr/u_paris/gla/project/idfm/StopEntry.java
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package fr.u_paris.gla.project.idfm;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/** A transport stop data.
|
||||||
|
*
|
||||||
|
* @author Emmanuel Bigeon */
|
||||||
|
public class StopEntry implements Comparable<StopEntry> {
|
||||||
|
public final String lname;
|
||||||
|
public final double longitude;
|
||||||
|
public final double latitude;
|
||||||
|
|
||||||
|
/** Create the stop
|
||||||
|
*
|
||||||
|
* @param lname
|
||||||
|
* @param longitude
|
||||||
|
* @param latitude */
|
||||||
|
public StopEntry(String lname, double longitude, double latitude) {
|
||||||
|
super();
|
||||||
|
this.lname = lname;
|
||||||
|
this.longitude = longitude;
|
||||||
|
this.latitude = latitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MessageFormat.format("{0} [{1}, {2}]", this.lname, this.longitude, //$NON-NLS-1$
|
||||||
|
this.latitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(StopEntry o) {
|
||||||
|
if (latitude < o.latitude) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (latitude > o.latitude) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (longitude < o.longitude) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (longitude > o.longitude) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return lname.compareTo(o.lname);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(latitude, lname, longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
StopEntry other = (StopEntry) obj;
|
||||||
|
return Double.doubleToLongBits(latitude) == Double
|
||||||
|
.doubleToLongBits(other.latitude) && Objects.equals(lname, other.lname)
|
||||||
|
&& Double.doubleToLongBits(longitude) == Double
|
||||||
|
.doubleToLongBits(other.longitude);
|
||||||
|
}
|
||||||
|
}
|
34
src/main/java/fr/u_paris/gla/project/idfm/TraceEntry.java
Normal file
34
src/main/java/fr/u_paris/gla/project/idfm/TraceEntry.java
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package fr.u_paris.gla.project.idfm;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Representation of a transport line
|
||||||
|
*
|
||||||
|
* @author Emmanuel Bigeon */
|
||||||
|
public final class TraceEntry {
|
||||||
|
public final String lname;
|
||||||
|
private List<List<StopEntry>> paths = new ArrayList<>();
|
||||||
|
|
||||||
|
/** Create a transport line.
|
||||||
|
*
|
||||||
|
* @param lname the name of the line */
|
||||||
|
public TraceEntry(String lname) {
|
||||||
|
super();
|
||||||
|
this.lname = lname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME list of lists are bad practice in direct access...
|
||||||
|
/** @return the list of paths */
|
||||||
|
public List<List<StopEntry>> getPaths() {
|
||||||
|
return Collections.unmodifiableList(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPath(List<StopEntry> path) {
|
||||||
|
paths.add(new ArrayList<>(path));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package fr.u_paris.gla.project.idfm;
|
||||||
|
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import fr.u_paris.gla.project.utils.GPS;
|
||||||
|
|
||||||
|
/** A transport stop with unidentified name and potential candidates as to their
|
||||||
|
* real value.
|
||||||
|
*
|
||||||
|
* @author Emmanuel Bigeon */
|
||||||
|
public final class UnidentifiedStopEntry extends StopEntry {
|
||||||
|
private List<StopEntry> candidates = new ArrayList<>();
|
||||||
|
|
||||||
|
/** Create the stop
|
||||||
|
*
|
||||||
|
* @param longitude
|
||||||
|
* @param latitude */
|
||||||
|
public UnidentifiedStopEntry(double longitude, double latitude) {
|
||||||
|
super("Unidentified", longitude, latitude); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MessageFormat.format("UnidentifiedStop [candidates={0}]", this.candidates); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the most likely choice for the stop
|
||||||
|
*
|
||||||
|
* @return the most likely candidate */
|
||||||
|
public StopEntry resolve() {
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (candidates.size() == 1) {
|
||||||
|
return candidates.get(0);
|
||||||
|
}
|
||||||
|
Collections.sort(candidates, (Comparator<? super StopEntry>) (StopEntry s1,
|
||||||
|
StopEntry s2) -> (int) Math.signum((GPS.distance(latitude, longitude,
|
||||||
|
s1.latitude, s1.longitude)
|
||||||
|
- GPS.distance(latitude, longitude, s2.latitude, s2.longitude))));
|
||||||
|
|
||||||
|
return candidates.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a candidate.
|
||||||
|
*
|
||||||
|
* @param entry the candidate */
|
||||||
|
public void addCandidate(StopEntry entry) {
|
||||||
|
candidates.add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (!super.equals(obj))
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
UnidentifiedStopEntry other = (UnidentifiedStopEntry) obj;
|
||||||
|
return Objects.equals(candidates, other.candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
65
src/main/java/fr/u_paris/gla/project/utils/CSVTools.java
Normal file
65
src/main/java/fr/u_paris/gla/project/utils/CSVTools.java
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package fr.u_paris.gla.project.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.opencsv.CSVParserBuilder;
|
||||||
|
import com.opencsv.CSVReader;
|
||||||
|
import com.opencsv.CSVReaderBuilder;
|
||||||
|
import com.opencsv.CSVWriterBuilder;
|
||||||
|
import com.opencsv.ICSVParser;
|
||||||
|
import com.opencsv.ICSVWriter;
|
||||||
|
import com.opencsv.exceptions.CsvValidationException;
|
||||||
|
|
||||||
|
/** A CSV tool class.
|
||||||
|
*
|
||||||
|
* @author Emmanuel Bigeon */
|
||||||
|
public final class CSVTools {
|
||||||
|
|
||||||
|
/** Hidden constructor of tool class */
|
||||||
|
private CSVTools() {
|
||||||
|
// Tool class
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readCSVFromURL(String url, Consumer<String[]> contentLineConsumer)
|
||||||
|
throws IOException {
|
||||||
|
ICSVParser parser = new CSVParserBuilder().withSeparator(';').build();
|
||||||
|
try (InputStream is = new URL(url).openStream();
|
||||||
|
Reader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||||
|
CSVReaderBuilder csvBuilder = new CSVReaderBuilder(reader)
|
||||||
|
.withCSVParser(parser);
|
||||||
|
try (CSVReader csv = csvBuilder.build()) {
|
||||||
|
String[] line = csv.readNextSilently(); // Eliminate header
|
||||||
|
while (csv.peek() != null) {
|
||||||
|
line = csv.readNext();
|
||||||
|
contentLineConsumer.accept(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (CsvValidationException e) {
|
||||||
|
throw new IOException("Invalid csv file", e); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeCSVToFile(String filename,
|
||||||
|
Stream<String[]> contentLineConsumer) throws IOException {
|
||||||
|
try (FileWriter writer = new FileWriter(filename, StandardCharsets.UTF_8)) {
|
||||||
|
CSVWriterBuilder wBuilder = new CSVWriterBuilder(writer).withSeparator(';');
|
||||||
|
try (ICSVWriter csv = wBuilder.build()) {
|
||||||
|
contentLineConsumer.forEachOrdered(csv::writeNext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in a new issue