diff --git a/src/main/java/fr/u_paris/gla/project/idfm/CSVStreamProvider.java b/src/main/java/fr/u_paris/gla/project/idfm/CSVStreamProvider.java new file mode 100644 index 0000000..37f8202 --- /dev/null +++ b/src/main/java/fr/u_paris/gla/project/idfm/CSVStreamProvider.java @@ -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 currentTrace; + private Iterator> currentPath = Collections.emptyIterator(); + private Iterator currentSegmentStart = Collections.emptyIterator(); + private Iterator currentSegmentEnd = Collections.emptyIterator(); + Map> 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 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 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. + *

+ * 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); + } + +} diff --git a/src/main/java/fr/u_paris/gla/project/idfm/IDFMNetworkExtractor.java b/src/main/java/fr/u_paris/gla/project/idfm/IDFMNetworkExtractor.java index 50d4834..4d63584 100644 --- a/src/main/java/fr/u_paris/gla/project/idfm/IDFMNetworkExtractor.java +++ b/src/main/java/fr/u_paris/gla/project/idfm/IDFMNetworkExtractor.java @@ -3,51 +3,31 @@ */ package fr.u_paris.gla.project.idfm; -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.text.Format; import java.text.MessageFormat; -import java.text.NumberFormat; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -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; - -import fr.u_paris.gla.project.io.NetworkFormat; +import fr.u_paris.gla.project.utils.CSVTools; import fr.u_paris.gla.project.utils.GPS; /** Code of an extractor for the data from IDF mobilite. * * @author Emmanuel Bigeon */ public class IDFMNetworkExtractor { + /** The logger for information on the process */ private static final Logger LOGGER = Logger .getLogger(IDFMNetworkExtractor.class.getName()); @@ -68,83 +48,10 @@ public class IDFMNetworkExtractor { // Magically chosen values /** A number of stops on each line */ - 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; + private static final int GUESS_STOPS_BY_LINE = 5; // Well named constants - private static final double _250_METERS = .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 candidates = new ArrayList<>(); - - @Override - public String toString() { - return "UnidentifiedStop [candidates=" + this.candidates + "]"; - } - } - - private static final class TraceEntry { - String lname; - List> stops = new ArrayList<>(); - } - - public static void readCSVFromURL(String url, Consumer 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); - } - } + private static final double QUARTER_KILOMETER = .25; /** Main entry point for the extractor of IDF mobilite data into a network as * defined by this application. @@ -159,165 +66,103 @@ public class IDFMNetworkExtractor { Map traces = new HashMap<>(); try { - readCSVFromURL(TRACE_FILE_URL, (String[] line) -> { - TraceEntry entry = new TraceEntry(); - entry.lname = line[IDFM_TRACE_SNAME_INDEX]; - List> 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); - } - }); + CSVTools.readCSVFromURL(TRACE_FILE_URL, + (String[] line) -> addLine(line, traces)); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Error while reading the line paths", e); } List stops = new ArrayList<>(traces.size() * GUESS_STOPS_BY_LINE); try { - readCSVFromURL(STOPS_FILE_URL, (String[] line) -> { - 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]; - BiFunction func = ( - k, trace) -> { - for (List 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); - }); + CSVTools.readCSVFromURL(STOPS_FILE_URL, + (String[] line) -> addStop(line, traces, stops)); } catch (IOException 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 traces) { Set toRemove = new HashSet<>(); for (Entry traceEntry : traces.entrySet()) { TraceEntry trace = traceEntry.getValue(); - for (List path : trace.stops) { - for (int i = 0; i < path.size(); i++) { - StopEntry stop = path.get(i); - if (stop instanceof UnidentifiedStopEntry) - 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); - } + if (!cleanLine(trace.getPaths())) { + LOGGER.severe(() -> MessageFormat.format( + "Missing stop for line {0}. Line will be removed", trace.lname)); + toRemove.add(traceEntry.getKey()); } } for (String string : toRemove) { traces.remove(string); } + } - // Export content in required format - try (FileWriter writer = new FileWriter(args[0], StandardCharsets.UTF_8)) { - CSVWriterBuilder wBuilder = new CSVWriterBuilder(writer).withSeparator(';'); - try (ICSVWriter csv = wBuilder.build()) { - for (Entry traceEntry : traces.entrySet()) { - Map> lineSegments = new HashMap<>(); - String[] nextLine = new String[NetworkFormat.NUMBER_COLUMNS]; - nextLine[NetworkFormat.LINE_INDEX] = traceEntry.getValue().lname; - for (List path : traceEntry.getValue().stops) { - for (int i = 0; i < path.size() - 1; i++) { - StopEntry stop1 = path.get(i); - lineSegments.putIfAbsent(stop1, new HashSet<>()); - StopEntry stop2 = path.get(i + 1); - if (!lineSegments.get(stop1).contains(stop2)) { - fillStation(stop1, nextLine, NetworkFormat.START_INDEX); - fillStation(stop2, nextLine, NetworkFormat.STOP_INDEX); - double distance = GPS.distance(stop1.latitude, - 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); - } - } - } + /** @param path */ + private static boolean cleanLine(List> stops) { + for (List path : stops) { + for (int i = 0; i < path.size(); i++) { + StopEntry stop = path.get(i); + if (!(stop instanceof UnidentifiedStopEntry)) { + continue; + } + UnidentifiedStopEntry unidentified = (UnidentifiedStopEntry) stop; + StopEntry stopResolution = unidentified.resolve(); + if (stopResolution == null) { + return false; + } + path.set(i, stopResolution); + } + } + return true; + } + private static void addStop(String[] line, Map traces, + List 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 traces) { + TraceEntry entry = new TraceEntry(line[IDFM_TRACE_SNAME_INDEX]); + List> 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 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]); } - } - - /** @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. - *

- * 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) (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); + return trace; } private static List> buildPaths(String pathsJSON) { @@ -341,7 +186,8 @@ public class IDFMNetworkExtractor { } } catch (JSONException e) { // 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; } diff --git a/src/main/java/fr/u_paris/gla/project/idfm/StopEntry.java b/src/main/java/fr/u_paris/gla/project/idfm/StopEntry.java new file mode 100644 index 0000000..0b1685f --- /dev/null +++ b/src/main/java/fr/u_paris/gla/project/idfm/StopEntry.java @@ -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 { + 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); + } +} diff --git a/src/main/java/fr/u_paris/gla/project/idfm/TraceEntry.java b/src/main/java/fr/u_paris/gla/project/idfm/TraceEntry.java new file mode 100644 index 0000000..92ee5bf --- /dev/null +++ b/src/main/java/fr/u_paris/gla/project/idfm/TraceEntry.java @@ -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> 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> getPaths() { + return Collections.unmodifiableList(paths); + } + + public void addPath(List path) { + paths.add(new ArrayList<>(path)); + } +} diff --git a/src/main/java/fr/u_paris/gla/project/idfm/UnidentifiedStopEntry.java b/src/main/java/fr/u_paris/gla/project/idfm/UnidentifiedStopEntry.java new file mode 100644 index 0000000..fc4f8d0 --- /dev/null +++ b/src/main/java/fr/u_paris/gla/project/idfm/UnidentifiedStopEntry.java @@ -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 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) (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); + } + +} diff --git a/src/main/java/fr/u_paris/gla/project/utils/CSVTools.java b/src/main/java/fr/u_paris/gla/project/utils/CSVTools.java new file mode 100644 index 0000000..b3194f9 --- /dev/null +++ b/src/main/java/fr/u_paris/gla/project/utils/CSVTools.java @@ -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 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 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); + } + } + } + +}