From de7b2a76cbf3f71def93d5ce8e4cbd5d83c16ad5 Mon Sep 17 00:00:00 2001 From: rakow Date: Tue, 28 Nov 2023 11:51:14 +0100 Subject: [PATCH] Counts v2 format (#2840) * created classes for multi mode counts * started writer class, added element names * Renamed Volumes.java to Measurable.java to make the class more flexible. Updated MultiModeCount class to new functionality * finished writer class, adapted count classes for better writing performance. * added reader, cleaned up writer class. checkstyle fixing * test class for general handling, writer and reader * added null value filter to writer * added new multi mode counts * extended test * bug fixing, code improvements * started cleanup of new multi mode counts * improved API * update reader and writer and tests * start to provide old api in the new format * use id class in v2 reader * separated v1 and v2, updated tests * update tests * fix type error with new counts * fix counts v1 tests * add more backwards compatible API * change count resolution to seconds instead of minutes * update warning for backwards compatibility * counts v2 is now zero based, deprecated old methods * mode is now networkMode * fix hour offsets in compatibility class * remove old uncheckedioexception * fix backwards compatibility * make refType optional for now * update traffic counts dashboard for new counts format * update count dashboard to new format --------- Co-authored-by: hzoerner --- .../traffic/CountComparisonAnalysis.java | 111 +- .../counts/CreateCountsFromBAStData.java | 162 +- .../counts/CreateCountsFromBAStDataTest.java | 171 +- .../cadyts/general/CadytsBuilderImpl.java | 6 +- .../dashboard/TrafficCountsDashboard.java | 2 +- .../dashboard/TrafficCountsDashboardTest.java | 51 +- .../simwrapper/dashboard/dummy_counts.xml | 8568 +++++++++++------ matsim/pom.xml | 5 + .../matsim/api/core/v01/TransportMode.java | 4 +- .../main/java/org/matsim/counts/Count.java | 121 +- .../main/java/org/matsim/counts/Counts.java | 173 +- .../matsim/counts/CountsReaderMatsimV2.java | 104 + .../java/org/matsim/counts/CountsWriter.java | 99 +- .../org/matsim/counts/CountsWriterV1.java | 138 + .../org/matsim/counts/CountsWriterV2.java | 177 + .../org/matsim/counts/MatsimCountsReader.java | 28 +- .../java/org/matsim/counts/Measurable.java | 204 + .../matsim/counts/MeasurementLocation.java | 178 + .../main/java/org/matsim/counts/Volume.java | 13 +- matsim/src/main/resources/dtd/counts_v2.xsd | 68 + .../java/org/matsim/counts/CountTest.java | 13 +- .../matsim/counts/CountsParserWriterTest.java | 2 +- .../java/org/matsim/counts/CountsV2Test.java | 180 + .../org/matsim/counts/MatsimCountsIOTest.java | 4 +- 24 files changed, 7247 insertions(+), 3335 deletions(-) create mode 100644 matsim/src/main/java/org/matsim/counts/CountsReaderMatsimV2.java create mode 100644 matsim/src/main/java/org/matsim/counts/CountsWriterV1.java create mode 100644 matsim/src/main/java/org/matsim/counts/CountsWriterV2.java create mode 100644 matsim/src/main/java/org/matsim/counts/Measurable.java create mode 100644 matsim/src/main/java/org/matsim/counts/MeasurementLocation.java create mode 100644 matsim/src/main/resources/dtd/counts_v2.xsd create mode 100644 matsim/src/test/java/org/matsim/counts/CountsV2Test.java diff --git a/contribs/application/src/main/java/org/matsim/application/analysis/traffic/CountComparisonAnalysis.java b/contribs/application/src/main/java/org/matsim/application/analysis/traffic/CountComparisonAnalysis.java index 42985e22beb..adc9a86f435 100644 --- a/contribs/application/src/main/java/org/matsim/application/analysis/traffic/CountComparisonAnalysis.java +++ b/contribs/application/src/main/java/org/matsim/application/analysis/traffic/CountComparisonAnalysis.java @@ -1,5 +1,7 @@ package org.matsim.application.analysis.traffic; +import it.unimi.dsi.fastutil.ints.Int2DoubleMap; +import it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap; import org.matsim.analysis.VolumesAnalyzer; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.TransportMode; @@ -13,10 +15,7 @@ import org.matsim.core.api.experimental.events.EventsManager; import org.matsim.core.events.EventsUtils; import org.matsim.core.network.NetworkUtils; -import org.matsim.counts.Count; -import org.matsim.counts.Counts; -import org.matsim.counts.MatsimCountsReader; -import org.matsim.counts.Volume; +import org.matsim.counts.*; import picocli.CommandLine; import tech.tablesaw.api.*; import tech.tablesaw.selection.Selection; @@ -29,7 +28,7 @@ @CommandLine.Command(name = "count-comparison", description = "Produces comparisons of observed and simulated counts.") @CommandSpec(requireEvents = true, requireCounts = true, requireNetwork = true, - produces = {"count_comparison_by_hour.csv", "count_comparison_daily.csv", "count_comparison_quality.csv", "count_error_by_hour.csv"}) + produces = {"count_comparison_by_hour.csv", "count_comparison_daily.csv", "count_comparison_quality.csv", "count_error_by_hour.csv"}) public class CountComparisonAnalysis implements MATSimAppCommand { @CommandLine.Mixin @@ -47,7 +46,7 @@ public class CountComparisonAnalysis implements MATSimAppCommand { @CommandLine.Option(names = "--labels", split = ",", description = "Labels for quality categories", defaultValue = "major under,under,ok,over,major over") private List labels; - @CommandLine.Option(names = "--transport-mode", description = "Mode to analyze", split = ",", defaultValue = TransportMode.car) + @CommandLine.Option(names = {"--network-mode", "--transport-mode"}, description = "Mode(s) to analyze", split = ",", defaultValue = TransportMode.car) private Set modes; public static void main(String[] args) { @@ -65,6 +64,9 @@ private static String cut(double relError, List errorGroups, List counts, Network network, VolumesAnalyzer Map, ? extends Link> links = network.getLinks(); Table byHour = Table.create( - StringColumn.create("link_id"), - StringColumn.create("name"), - StringColumn.create("road_type"), - IntColumn.create("hour"), - DoubleColumn.create("observed_traffic_volume"), - DoubleColumn.create("simulated_traffic_volume") + StringColumn.create("link_id"), + StringColumn.create("name"), + StringColumn.create("road_type"), + IntColumn.create("hour"), + DoubleColumn.create("observed_traffic_volume"), + DoubleColumn.create("simulated_traffic_volume") ); Table dailyTrafficVolume = Table.create(StringColumn.create("link_id"), - StringColumn.create("name"), - StringColumn.create("road_type"), - DoubleColumn.create("observed_traffic_volume"), - DoubleColumn.create("simulated_traffic_volume") + StringColumn.create("name"), + StringColumn.create("road_type"), + DoubleColumn.create("observed_traffic_volume"), + DoubleColumn.create("simulated_traffic_volume") ); - for (Map.Entry, Count> entry : counts.getCounts().entrySet()) { + for (Map.Entry, MeasurementLocation> entry : counts.getMeasureLocations().entrySet()) { Id key = entry.getKey(); - Map countVolume = entry.getValue().getVolumes(); - String name = entry.getValue().getCsLabel(); + + Int2DoubleMap countVolume = aggregateObserved(entry.getValue(), modes); + + String name = entry.getValue().getDisplayName(); Link link = links.get(key); String type = NetworkUtils.getHighwayType(link); @@ -134,9 +138,9 @@ private Table writeOutput(Counts counts, Network network, VolumesAnalyzer continue; Optional opt = modes.stream() - .map(mode -> volumes.getVolumesForLink(key, mode)) - .filter(Objects::nonNull) - .reduce(CountComparisonAnalysis::sum); + .map(mode -> volumes.getVolumesForLink(key, mode)) + .filter(Objects::nonNull) + .reduce(CountComparisonAnalysis::sum); int[] volumesForLink; if (countVolume.isEmpty() || opt.isEmpty()) { @@ -148,12 +152,13 @@ private Table writeOutput(Counts counts, Network network, VolumesAnalyzer double simulatedTrafficVolumeByDay = 0; double observedTrafficVolumeByDay = 0; - if (countVolume.size() == 24) { + // Hourly values are present + if (countVolume.size() > 1 || !countVolume.containsKey(24)) { - for (int hour = 1; hour < 25; hour++) { + for (int hour = 0; hour < 24; hour++) { - double observedTrafficVolumeAtHour = countVolume.get(hour).getValue(); - double simulatedTrafficVolumeAtHour = (double) volumesForLink[hour - 1] / this.sample.getSample(); + double observedTrafficVolumeAtHour = countVolume.get(hour); + double simulatedTrafficVolumeAtHour = (double) volumesForLink[hour] / this.sample.getSample(); simulatedTrafficVolumeByDay += simulatedTrafficVolumeAtHour; observedTrafficVolumeByDay += observedTrafficVolumeAtHour; @@ -162,12 +167,15 @@ private Table writeOutput(Counts counts, Network network, VolumesAnalyzer row.setString("link_id", key.toString()); row.setString("name", name); row.setString("road_type", type); - row.setInt("hour", hour - 1); + row.setInt("hour", hour); row.setDouble("observed_traffic_volume", observedTrafficVolumeAtHour); row.setDouble("simulated_traffic_volume", simulatedTrafficVolumeAtHour); } - } else - observedTrafficVolumeByDay = countVolume.values().stream().mapToDouble(Volume::getValue).sum(); + } else { + // Get the daily values + observedTrafficVolumeByDay = countVolume.get(24); + simulatedTrafficVolumeByDay = Arrays.stream(volumesForLink).sum() / this.sample.getSample(); + } Row row = dailyTrafficVolume.appendRow(); row.setString("link_id", key.toString()); @@ -178,12 +186,12 @@ private Table writeOutput(Counts counts, Network network, VolumesAnalyzer } DoubleColumn relError = dailyTrafficVolume.doubleColumn("simulated_traffic_volume") - .divide(dailyTrafficVolume.doubleColumn("observed_traffic_volume")) - .setName("rel_error"); + .divide(dailyTrafficVolume.doubleColumn("observed_traffic_volume")) + .setName("rel_error"); StringColumn qualityLabel = relError.copy() - .map(err -> cut(err, limits, labels), ColumnType.STRING::create) - .setName("quality"); + .map(err -> cut(err, limits, labels), ColumnType.STRING::create) + .setName("quality"); dailyTrafficVolume.addColumns(relError, qualityLabel); @@ -220,20 +228,47 @@ private Table writeOutput(Counts counts, Network network, VolumesAnalyzer return byHour; } + /** + * Aggregate observed counts by hour. Starting at 0. + */ + private static Int2DoubleMap aggregateObserved(MeasurementLocation m, Set modes) { + + Int2DoubleOpenHashMap map = new Int2DoubleOpenHashMap(); + + for (String mode : modes) { + Measurable volumes = m.getVolumesForMode(mode); + if (volumes == null) + continue; + + if (volumes.supportsHourlyAggregate()) { + for (int i = 0; i < 24; i++) { + OptionalDouble v = volumes.aggregateAtHour(i); + if (v.isPresent()) + map.mergeDouble(i, v.getAsDouble(), Double::sum); + } + } else { + // Daily value is stored under separate key + map.mergeDouble(24, volumes.aggregateDaily(), Double::sum); + } + } + + return map; + } + private void writeErrorMetrics(Table byHour, Path path) { byHour.addColumns( - byHour.doubleColumn("simulated_traffic_volume").subtract(byHour.doubleColumn("observed_traffic_volume")).setName("error") + byHour.doubleColumn("simulated_traffic_volume").subtract(byHour.doubleColumn("observed_traffic_volume")).setName("error") ); byHour.addColumns( - byHour.doubleColumn("error").abs().setName("abs_error") + byHour.doubleColumn("error").abs().setName("abs_error") ); DoubleColumn relError = byHour.doubleColumn("abs_error") - .multiply(100) - .divide(byHour.doubleColumn("observed_traffic_volume")) - .setName("rel_error"); + .multiply(100) + .divide(byHour.doubleColumn("observed_traffic_volume")) + .setName("rel_error"); // Cut-off at Max error relError = relError.set(relError.isMissing(), 1000d); diff --git a/contribs/application/src/main/java/org/matsim/application/prepare/counts/CreateCountsFromBAStData.java b/contribs/application/src/main/java/org/matsim/application/prepare/counts/CreateCountsFromBAStData.java index 1018c9e0696..dd02f6572ee 100644 --- a/contribs/application/src/main/java/org/matsim/application/prepare/counts/CreateCountsFromBAStData.java +++ b/contribs/application/src/main/java/org/matsim/application/prepare/counts/CreateCountsFromBAStData.java @@ -21,9 +21,10 @@ import org.matsim.core.utils.geometry.geotools.MGC; import org.matsim.core.utils.geometry.transformations.TransformationFactory; import org.matsim.core.utils.io.IOUtils; -import org.matsim.counts.Count; import org.matsim.counts.Counts; import org.matsim.counts.CountsWriter; +import org.matsim.counts.Measurable; +import org.matsim.counts.MeasurementLocation; import picocli.CommandLine; import java.io.BufferedReader; @@ -56,43 +57,29 @@ @CommandLine.Command(name = "counts-from-bast", description = "Creates MATSim from BASt Stundenwerte.txt") public class CreateCountsFromBAStData implements MATSimAppCommand { - @CommandLine.Option(names = "--network", description = "path to MATSim network", required = true) - private String network; - + private static final Logger log = LogManager.getLogger(CreateCountsFromBAStData.class); @CommandLine.Option(names = "--road-types", description = "Define on which roads counts are created") private final List roadTypes = List.of("motorway", "primary", "trunk"); - + @CommandLine.Mixin + private final ShpOptions shp = new ShpOptions(); + @CommandLine.Mixin + private final CountsOption counts = new CountsOption(); + @CommandLine.Mixin + private final CrsOptions crs = new CrsOptions("EPSG:25832"); + @CommandLine.Option(names = "--network", description = "path to MATSim network", required = true) + private String network; @CommandLine.Option(names = "--primary-data", description = "path to BASt Bundesstraßen-'Stundenwerte'-.txt file", required = true) private Path primaryData; - @CommandLine.Option(names = "--motorway-data", description = "path to BASt Bundesautobahnen-'Stundenwerte'-.txt file", required = true) private Path motorwayData; - @CommandLine.Option(names = "--station-data", description = "path to default BASt count station .csv", required = true) private Path stationData; - @CommandLine.Option(names = "--search-range", description = "range for the buffer around count stations, in which links are queried", defaultValue = "50") private double searchRange; - @CommandLine.Option(names = "--year", description = "Year of counts", required = true) private int year; - - @CommandLine.Option(names = "--car-output", description = "output car counts path", defaultValue = "car-counts-from-bast.xml.gz") - private Path carOutput; - - @CommandLine.Option(names = "--freight-output", description = "output freight counts path", defaultValue = "freight-counts-from-bast.xml.gz") - private Path freightOutput; - - @CommandLine.Mixin - private final ShpOptions shp = new ShpOptions(); - - @CommandLine.Mixin - private final CountsOption counts = new CountsOption(); - - @CommandLine.Mixin - private final CrsOptions crs = new CrsOptions("EPSG:25832"); - - private static final Logger log = LogManager.getLogger(CreateCountsFromBAStData.class); + @CommandLine.Option(names = "--output", description = "Output counts path", defaultValue = "counts-from-bast.xml.gz") + private Path output; public static void main(String[] args) { new CreateCountsFromBAStData().execute(args); @@ -111,21 +98,23 @@ public Integer call() { readHourlyTrafficVolume(motorwayData, stations); log.info("+++++++ Map aggregated traffic volumes to count stations +++++++"); - Counts miv = new Counts<>(); - Counts freight = new Counts<>(); - stations.values().forEach(station -> mapTrafficVolumeToCount(station, miv, freight)); - miv.setYear(year); - freight.setYear(year); + Counts mmCounts = new Counts<>(); + mmCounts.setYear(year); + mmCounts.setName("BASt Counts"); + mmCounts.setSource("Bundesanstalt für Straßenwesen"); + mmCounts.setDescription("Aggregated daily traffic volumes for car and freight traffic."); + + stations.values().forEach(station -> mapTrafficVolumeToCount(station, mmCounts)); + + log.info("+++++++ Write MATSim counts to {} +++++++", output); + new CountsWriter(mmCounts).write(output.toString()); - log.info("+++++++ Write MATSim counts to {} and {} +++++++", carOutput, freightOutput); - new CountsWriter(miv).write(carOutput.toString()); - new CountsWriter(freight).write(freightOutput.toString()); return 0; } - private void mapTrafficVolumeToCount(BAStCountStation station, Counts miv, Counts freight) { + private void mapTrafficVolumeToCount(BAStCountStation station, Counts counts) { if (!station.hasMatchedLink()) return; @@ -135,18 +124,21 @@ private void mapTrafficVolumeToCount(BAStCountStation station, Counts miv, return; } - Count mivCount = miv.createAndAddCount(station.getMatchedLink().getId(), station.getName() + "_" + station.getDirection()); - Count freightCount = freight.createAndAddCount(station.getMatchedLink().getId(), station.getName() + "_" + station.getDirection()); + MeasurementLocation multiModeCount = counts.createAndAddMeasureLocation(station.getMatchedLink().getId(), station.getName() + "_" + station.getDirection()); + + Measurable carVolume = multiModeCount.createVolume(TransportMode.car); + Measurable freightVolume = multiModeCount.createVolume(TransportMode.truck); var mivTrafficVolumes = station.getMivTrafficVolume(); var freightTrafficVolumes = station.getFreightTrafficVolume(); for (String hour : mivTrafficVolumes.keySet()) { - - if (hour.startsWith("0")) hour.replace("0", ""); int h = Integer.parseInt(hour); - mivCount.createVolume(h, mivTrafficVolumes.get(hour)); - freightCount.createVolume(h, freightTrafficVolumes.get(hour)); + Double mivAtHour = mivTrafficVolumes.get(hour); + Double freightAtHour = freightTrafficVolumes.get(hour); + + carVolume.setAtHour(h, mivAtHour); + freightVolume.setAtHour(h, freightAtHour); } } @@ -194,31 +186,31 @@ private void readHourlyTrafficVolume(Path pathToDisaggregatedData, Map keys = stations.keySet().stream() - .map(key -> key.replaceAll("_1", "").replaceAll("_2", "")) - .collect(Collectors.toSet()); + .map(key -> key.replaceAll("_1", "").replaceAll("_2", "")) + .collect(Collectors.toSet()); // Filter for weekday Tuesday-Wednesday preFilteredRecords = StreamSupport.stream(records.spliterator(), false) - .filter(row -> keys.contains(row.get("Zst"))) - .filter(row -> { - int day; - try { - day = Integer.parseInt(row.get("Wotag").replace(" ", "")); - } catch (NumberFormatException e) { - log.warn("Error parsing week day number: {}", row.get("Wotag")); - return false; - } - return day > 1 && day < 5; - }) - .collect(Collectors.toList()); + .filter(row -> keys.contains(row.get("Zst"))) + .filter(row -> { + int day; + try { + day = Integer.parseInt(row.get("Wotag").replace(" ", "")); + } catch (NumberFormatException e) { + log.warn("Error parsing week day number: {}", row.get("Wotag")); + return false; + } + return day > 1 && day < 5; + }) + .collect(Collectors.toList()); if (preFilteredRecords == null || preFilteredRecords.isEmpty()) { log.warn("Records read from {} don't contain the stations ... ", pathToDisaggregatedData); @@ -228,8 +220,8 @@ private void readHourlyTrafficVolume(Path pathToDisaggregatedData, Map stationIds = preFilteredRecords.stream() - .map(row -> row.get("Zst")) - .collect(Collectors.toSet()); + .map(row -> row.get("Zst")) + .collect(Collectors.toSet()); for (String number : stationIds) { BAStCountStation direction1 = stations.get(number + "_1"); @@ -244,12 +236,12 @@ private void readHourlyTrafficVolume(Path pathToDisaggregatedData, Map allEntriesOfStation = preFilteredRecords.stream() - .filter(row -> row.get("Zst").equals(number)).toList(); + .filter(row -> row.get("Zst").equals(number)).toList(); for (String hour : hours) { var hourlyTrafficVolumes = allEntriesOfStation.stream() - .filter(row -> row.get("Stunde").replace("\"", "") - .equals(hour)).toList(); + .filter(row -> row.get("Stunde").replace("\"", "") + .equals(hour)).toList(); if (hourlyTrafficVolumes.isEmpty()) { log.warn("No volume for station {} at hour {}", direction1.getName(), hour); @@ -258,12 +250,12 @@ private void readHourlyTrafficVolume(Path pathToDisaggregatedData, Map Double.parseDouble(row.get(mivCol1))) - .sum(); + .mapToDouble(row -> Double.parseDouble(row.get(mivCol1))) + .sum(); double sumMiv2 = hourlyTrafficVolumes.stream() - .mapToDouble(row -> Double.parseDouble(row.get(mivCol2))) - .sum(); + .mapToDouble(row -> Double.parseDouble(row.get(mivCol2))) + .sum(); double meanMiv1 = sumMiv1 / divisor; double meanMiv2 = sumMiv2 / divisor; @@ -273,12 +265,12 @@ private void readHourlyTrafficVolume(Path pathToDisaggregatedData, Map Double.parseDouble(row.get(freightCol1))) - .sum(); + .mapToDouble(row -> Double.parseDouble(row.get(freightCol1))) + .sum(); double sumFreight2 = hourlyTrafficVolumes.stream() - .mapToDouble(row -> Double.parseDouble(row.get(freightCol2))) - .sum(); + .mapToDouble(row -> Double.parseDouble(row.get(freightCol2))) + .sum(); double meanFreight1 = sumFreight1 / divisor; double meanFreight2 = sumFreight2 / divisor; @@ -382,12 +374,12 @@ private Map readBAStCountStations(Path pathToAggregate try (BufferedReader reader = Files.newBufferedReader(pathToAggregatedData, StandardCharsets.ISO_8859_1)) { CSVParser records = CSVFormat - .Builder.create() - .setAllowMissingColumnNames(true) - .setDelimiter(';') - .setHeader() - .build() - .parse(reader); + .Builder.create() + .setAllowMissingColumnNames(true) + .setDelimiter(';') + .setHeader() + .build() + .parse(reader); for (CSVRecord row : records) { @@ -417,7 +409,7 @@ private Map readBAStCountStations(Path pathToAggregate Set ignored = counts.getIgnored(); final Predicate optFilter = station -> !ignored.contains(station.getId().replaceAll("_1", "") - .replaceAll("_2", "")); + .replaceAll("_2", "")); final Predicate shpFilter; if (shp.getShapeFile() != null) { @@ -430,9 +422,9 @@ private Map readBAStCountStations(Path pathToAggregate // Return filtered map with id as key and station as value return stations.stream() - .filter(optFilter.and(shpFilter)) - .collect(Collectors.toMap( - BAStCountStation::getId, Function.identity() - )); + .filter(optFilter.and(shpFilter)) + .collect(Collectors.toMap( + BAStCountStation::getId, Function.identity() + )); } } diff --git a/contribs/application/src/test/java/org/matsim/application/prepare/counts/CreateCountsFromBAStDataTest.java b/contribs/application/src/test/java/org/matsim/application/prepare/counts/CreateCountsFromBAStDataTest.java index 96db8afa2ae..2bb18be2668 100644 --- a/contribs/application/src/test/java/org/matsim/application/prepare/counts/CreateCountsFromBAStDataTest.java +++ b/contribs/application/src/test/java/org/matsim/application/prepare/counts/CreateCountsFromBAStDataTest.java @@ -4,26 +4,23 @@ import org.junit.Rule; import org.junit.Test; import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.TransportMode; import org.matsim.api.core.v01.network.Link; import org.matsim.core.utils.io.IOUtils; -import org.matsim.counts.Count; -import org.matsim.counts.Counts; -import org.matsim.counts.CountsReaderMatsimV1; -import org.matsim.counts.MatsimCountsReader; +import org.matsim.counts.*; import org.matsim.examples.ExamplesUtils; import org.matsim.testcases.MatsimTestUtils; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; + public class CreateCountsFromBAStDataTest { @Rule public MatsimTestUtils utils = new MatsimTestUtils(); - String carOutput = "car-test-counts.xml.gz"; - String freightOutput = "freight-test-counts.xml.gz"; + String countsOutput = "test-counts.xml.gz"; String ignoredCounts = "ignored.csv"; String manualMatchedCounts = "manual.csv"; @@ -39,72 +36,76 @@ public class CreateCountsFromBAStDataTest { String shpCrs = "EPSG:3857"; @Test - public void testCreateCountsFromBAStData(){ + public void testCreateCountsFromBAStData() { String version = "normal-"; - String car = utils.getOutputDirectory() + version + carOutput; - String freight = utils.getOutputDirectory() + version + freightOutput; + String out = utils.getOutputDirectory() + version + countsOutput; String[] args = new String[]{ - "--station-data=" + utils.getPackageInputDirectory() + stationData, - "--network=" + network, - "--input-crs=" + networkCrs, - "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, - "--primary-data=" + utils.getPackageInputDirectory() + primaryData, - "--shp=" + utils.getPackageInputDirectory() + shp, - "--shp-crs=" + shpCrs, - "--year=2021", - "--car-output=" + car, - "--freight-output=" + freight + "--station-data=" + utils.getPackageInputDirectory() + stationData, + "--network=" + network, + "--input-crs=" + networkCrs, + "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, + "--primary-data=" + utils.getPackageInputDirectory() + primaryData, + "--shp=" + utils.getPackageInputDirectory() + shp, + "--shp-crs=" + shpCrs, + "--year=2021", + "--output=" + out, }; new CreateCountsFromBAStData().execute(args); Counts counts = new Counts<>(); - new MatsimCountsReader(counts).readFile(car); + new MatsimCountsReader(counts).readFile(out); - Integer size = counts.getCounts().size(); + assertThat(counts.getMeasureLocations()) + .hasSize(24); - assertThat(size).isGreaterThan(0); + assertThat(counts.getCounts()) + .hasSize(24); + + for (Map.Entry, MeasurementLocation> e : counts.getMeasureLocations().entrySet()) { + assertThat(e.getValue().hasMeasurableForMode(Measurable.VOLUMES, TransportMode.car)) + .isTrue(); + + assertThat(e.getValue().hasMeasurableForMode(Measurable.VOLUMES, TransportMode.truck)) + .isTrue(); + } } @Test - public void testWithIgnoredStations(){ + public void testWithIgnoredStations() { String version = "with-ignored-"; - String car1 = utils.getOutputDirectory() + version + carOutput; - String freight1 = utils.getOutputDirectory() + version + freightOutput; + String out1 = utils.getOutputDirectory() + version + countsOutput; String[] args1 = new String[]{ - "--station-data=" + utils.getPackageInputDirectory() + stationData, - "--network=" + network, - "--input-crs=" + networkCrs, - "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, - "--primary-data=" + utils.getPackageInputDirectory() + primaryData, - "--shp=" + utils.getPackageInputDirectory() + shp, - "--shp-crs=" + shpCrs, - "--year=2021", - "--car-output=" + car1, - "--freight-output=" + freight1 + "--station-data=" + utils.getPackageInputDirectory() + stationData, + "--network=" + network, + "--input-crs=" + networkCrs, + "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, + "--primary-data=" + utils.getPackageInputDirectory() + primaryData, + "--shp=" + utils.getPackageInputDirectory() + shp, + "--shp-crs=" + shpCrs, + "--year=2021", + "--output=" + out1, }; new CreateCountsFromBAStData().execute(args1); - String car2 = utils.getOutputDirectory() + "without-ignored-" + carOutput; - String freight2 = utils.getOutputDirectory() + "without-ignored-" + freightOutput; + String out2 = utils.getOutputDirectory() + "without-ignored-" + countsOutput; String[] args2 = new String[]{ - "--station-data=" + utils.getPackageInputDirectory() + stationData, - "--network=" + network, - "--input-crs=" + networkCrs, - "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, - "--primary-data=" + utils.getPackageInputDirectory() + primaryData, - "--shp=" + utils.getPackageInputDirectory() + shp, - "--shp-crs=" + shpCrs, - "--year=2021", - "--car-output=" + car2, - "--freight-output=" + freight2, - "--ignored-counts=" + utils.getPackageInputDirectory() + ignoredCounts, + "--station-data=" + utils.getPackageInputDirectory() + stationData, + "--network=" + network, + "--input-crs=" + networkCrs, + "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, + "--primary-data=" + utils.getPackageInputDirectory() + primaryData, + "--shp=" + utils.getPackageInputDirectory() + shp, + "--shp-crs=" + shpCrs, + "--year=2021", + "--output=" + out2, + "--ignored-counts=" + utils.getPackageInputDirectory() + ignoredCounts, }; new CreateCountsFromBAStData().execute(args2); @@ -112,76 +113,72 @@ public void testWithIgnoredStations(){ Counts countsComplete = new Counts<>(); Counts countsWithoutIgnored = new Counts<>(); - new MatsimCountsReader(countsComplete).readFile(car1); - new MatsimCountsReader(countsWithoutIgnored).readFile(car2); + new MatsimCountsReader(countsComplete).readFile(out1); + new MatsimCountsReader(countsWithoutIgnored).readFile(out2); - int completeSize = countsComplete.getCounts().size(); - int ignoredSize = countsWithoutIgnored.getCounts().size(); + int completeSize = countsComplete.getMeasureLocations().size(); + int ignoredSize = countsWithoutIgnored.getMeasureLocations().size(); assertThat(completeSize).isGreaterThan(ignoredSize); } @Test - public void testManualMatchedCounts(){ + public void testManualMatchedCounts() { - String car = utils.getOutputDirectory() + "manual-matched-" + carOutput; - String freight = utils.getOutputDirectory() + "manual-matched-" + freightOutput; + String out = utils.getOutputDirectory() + "manual-matched-" + countsOutput; //Map contains supposed matching from manual.csv Map, String> manual = Map.of(Id.createLinkId("4205"), "Neukölln_N", Id.createLinkId("4219"), "Neukölln_S"); String[] args = new String[]{ - "--station-data=" + utils.getPackageInputDirectory() + stationData, - "--network=" + network, - "--input-crs=" + networkCrs, - "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, - "--primary-data=" + utils.getPackageInputDirectory() + primaryData, - "--shp=" + utils.getPackageInputDirectory() + shp, - "--shp-crs=" + shpCrs, - "--year=2021", - "--car-output=" + car, - "--freight-output=" + freight, - "--manual-matched-counts=" + utils.getPackageInputDirectory() + manualMatchedCounts, + "--station-data=" + utils.getPackageInputDirectory() + stationData, + "--network=" + network, + "--input-crs=" + networkCrs, + "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, + "--primary-data=" + utils.getPackageInputDirectory() + primaryData, + "--shp=" + utils.getPackageInputDirectory() + shp, + "--shp-crs=" + shpCrs, + "--year=2021", + "--output=" + out, + "--manual-matched-counts=" + utils.getPackageInputDirectory() + manualMatchedCounts, }; new CreateCountsFromBAStData().execute(args); Counts countsWithManualMatched = new Counts<>(); - new CountsReaderMatsimV1(countsWithManualMatched).readFile(car); + new MatsimCountsReader(countsWithManualMatched).readFile(out); - var map = countsWithManualMatched.getCounts(); + var map = countsWithManualMatched.getMeasureLocations(); - for(var entry: manual.entrySet()){ + for (var entry : manual.entrySet()) { Id supposed = entry.getKey(); String station = entry.getValue(); - Count actual = map.get(supposed); - String actualStation = actual.getCsLabel(); + MeasurementLocation actual = map.get(supposed); + String actualStation = actual.getStationName(); Assert.assertEquals(station, actualStation); } } @Test - public void testManualMatchingWithWrongInput(){ + public void testManualMatchingWithWrongInput() { - String car = utils.getOutputDirectory() + "manual-matched-" + carOutput; - String freight = utils.getOutputDirectory() + "manual-matched-" + freightOutput; + String out = utils.getOutputDirectory() + "manual-matched-" + countsOutput; String[] args = new String[]{ - "--station-data=" + utils.getPackageInputDirectory() + stationData, - "--network=" + network, - "--input-crs=" + networkCrs, - "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, - "--primary-data=" + utils.getPackageInputDirectory() + primaryData, - "--shp=" + utils.getPackageInputDirectory() + shp, - "--shp-crs=" + shpCrs, - "--year=2021", - "--car-output=" + car, - "--freight-output=" + freight, - "--manual-matched-counts=" + utils.getPackageInputDirectory() + wrongManualMatchedCounts, + "--station-data=" + utils.getPackageInputDirectory() + stationData, + "--network=" + network, + "--input-crs=" + networkCrs, + "--motorway-data=" + utils.getPackageInputDirectory() + motorwayData, + "--primary-data=" + utils.getPackageInputDirectory() + primaryData, + "--shp=" + utils.getPackageInputDirectory() + shp, + "--shp-crs=" + shpCrs, + "--year=2021", + "--output=" + out, + "--manual-matched-counts=" + utils.getPackageInputDirectory() + wrongManualMatchedCounts, }; Assert.assertThrows(RuntimeException.class, () -> new CreateCountsFromBAStData().execute(args)); diff --git a/contribs/cadytsIntegration/src/main/java/org/matsim/contrib/cadyts/general/CadytsBuilderImpl.java b/contribs/cadytsIntegration/src/main/java/org/matsim/contrib/cadyts/general/CadytsBuilderImpl.java index 12a9ae8bfbc..c601376b19b 100644 --- a/contribs/cadytsIntegration/src/main/java/org/matsim/contrib/cadyts/general/CadytsBuilderImpl.java +++ b/contribs/cadytsIntegration/src/main/java/org/matsim/contrib/cadyts/general/CadytsBuilderImpl.java @@ -25,6 +25,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Identifiable; import org.matsim.core.config.Config; import org.matsim.core.config.ConfigUtils; import org.matsim.core.gbl.MatsimRandom; @@ -44,9 +45,8 @@ public final class CadytsBuilderImpl { private CadytsBuilderImpl(){} // do not instantiate - public static AnalyticalCalibrator buildCalibratorAndAddMeasurements(final Config config, final Counts occupCounts, - LookUpItemFromId lookUp, Class idType) { - + public static > AnalyticalCalibrator buildCalibratorAndAddMeasurements(final Config config, final Counts occupCounts, + LookUpItemFromId lookUp, Class idType) { if (occupCounts.getCounts().size() == 0) { log.warn("Counts container is empty."); } diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboard.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboard.java index 43964f3c585..bc430127bf0 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboard.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboard.java @@ -88,7 +88,7 @@ else if (i == labels.size() - 1) argList.addAll(List.of("--counts", countsPath)); if (networkModes != null) - argList.addAll(List.of("--transport-mode", String.join(",", networkModes))); + argList.addAll(List.of("--network-mode", String.join(",", networkModes))); String[] args = argList.toArray(new String[0]); diff --git a/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboardTest.java b/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboardTest.java index 98841ed290d..876e5d01552 100644 --- a/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboardTest.java +++ b/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/TrafficCountsDashboardTest.java @@ -10,10 +10,7 @@ import org.matsim.core.config.Config; import org.matsim.core.controler.Controler; import org.matsim.core.network.NetworkUtils; -import org.matsim.counts.Count; -import org.matsim.counts.Counts; -import org.matsim.counts.CountsModule; -import org.matsim.counts.CountsWriter; +import org.matsim.counts.*; import org.matsim.examples.ExamplesUtils; import org.matsim.simwrapper.Dashboard; import org.matsim.simwrapper.SimWrapper; @@ -43,22 +40,22 @@ public void generate() { SimWrapperConfigGroup simWrapperConfigGroup = new SimWrapperConfigGroup(); SimWrapperConfigGroup.ContextParams contextParams = simWrapperConfigGroup.get(""); - contextParams.mapCenter = "12, 48.95"; + contextParams.mapCenter = "12,48.95"; contextParams.sampleSize = 0.01; contextParams.mapZoomLevel = 9.0; SimWrapper sw = SimWrapper.create(config) - .addDashboard(new TrafficCountsDashboard()) - .addDashboard(Dashboard.customize(new TrafficCountsDashboard() - .withQualityLabels( - List.of(0.0, 0.3, 1.7, 2.5), - List.of("way too few", "fewer", "exact", "too much", "way too much") - ) - ).context("custom")) - .addDashboard(Dashboard.customize(new TrafficCountsDashboard( - Path.of(utils.getPackageInputDirectory()).normalize().toAbsolutePath() + "/dummy_counts.xml", - Set.of(TransportMode.car, "freight")) - ).context("freight").title("Freight counts")); + .addDashboard(new TrafficCountsDashboard()) + .addDashboard(Dashboard.customize(new TrafficCountsDashboard() + .withQualityLabels( + List.of(0.0, 0.3, 1.7, 2.5), + List.of("way too few", "fewer", "exact", "too much", "way too much") + ) + ).context("custom")) + .addDashboard(Dashboard.customize(new TrafficCountsDashboard( + Path.of(utils.getPackageInputDirectory()).normalize().toAbsolutePath() + "/dummy_counts.xml", + Set.of(TransportMode.car, "freight")) + ).context("freight").title("Freight counts")); Controler controler = MATSimApplication.prepare(new TestScenario(sw), config); controler.addOverridingModule(new CountsModule()); @@ -68,12 +65,12 @@ public void generate() { Path freightDir = Path.of(utils.getOutputDirectory(), "analysis", "traffic-freight"); Assertions.assertThat(defaultDir) - .isDirectoryContaining("glob:**count_comparison_daily.csv") - .isDirectoryContaining("glob:**count_comparison_by_hour.csv"); + .isDirectoryContaining("glob:**count_comparison_daily.csv") + .isDirectoryContaining("glob:**count_comparison_by_hour.csv"); Assertions.assertThat(freightDir) - .isDirectoryContaining("glob:**count_comparison_daily.csv") - .isDirectoryContaining("glob:**count_comparison_by_hour.csv"); + .isDirectoryContaining("glob:**count_comparison_daily.csv") + .isDirectoryContaining("glob:**count_comparison_by_hour.csv"); } @@ -92,12 +89,18 @@ public void generateDummyCounts(Config config) { for (int i = 0; i <= 100; i++) { Link link = links.get(random.nextInt(size)); - if (counts.getCounts().containsKey(link.getId())) + if (counts.getMeasureLocations().containsKey(link.getId())) continue; - Count count = counts.createAndAddCount(link.getId(), link.getId().toString() + "_count_station"); - for (int hour = 1; hour < 25; hour++) - count.createVolume(hour, random.nextInt(75)); + MeasurementLocation station = counts.createAndAddMeasureLocation(link.getId(), link.getId().toString() + "_count_station"); + + Measurable carVolume = station.createVolume(TransportMode.car); + Measurable freightVolume = station.createVolume(TransportMode.truck); + + for (int hour = 0; hour < 24; hour++) { + carVolume.setAtHour(hour, random.nextInt(500)); + freightVolume.setAtHour(hour, random.nextInt(100)); + } } try { diff --git a/contribs/simwrapper/test/input/org/matsim/simwrapper/dashboard/dummy_counts.xml b/contribs/simwrapper/test/input/org/matsim/simwrapper/dashboard/dummy_counts.xml index 09a85d77576..5bf5b21b8e5 100644 --- a/contribs/simwrapper/test/input/org/matsim/simwrapper/dashboard/dummy_counts.xml +++ b/contribs/simwrapper/test/input/org/matsim/simwrapper/dashboard/dummy_counts.xml @@ -1,2907 +1,5663 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matsim/pom.xml b/matsim/pom.xml index 915d802b4ec..560a03900e8 100644 --- a/matsim/pom.xml +++ b/matsim/pom.xml @@ -123,6 +123,11 @@ + + it.unimi.dsi + fastutil-core + 8.5.12 + org.geotools gt-main diff --git a/matsim/src/main/java/org/matsim/api/core/v01/TransportMode.java b/matsim/src/main/java/org/matsim/api/core/v01/TransportMode.java index 2f23401f045..aa0e7010eee 100644 --- a/matsim/src/main/java/org/matsim/api/core/v01/TransportMode.java +++ b/matsim/src/main/java/org/matsim/api/core/v01/TransportMode.java @@ -51,8 +51,8 @@ public final class TransportMode { public static final String egress_walk = "non_network_walk" ; // (The directionality is not useful: what may be an egress_walk from the point of view of drt may be an access_walk from the point of view of pt. // kai, jun'19) - - // non_network_walk as access/egress to modes other than walk on the network was replaced by walk. - kn/gl-nov'19 + + // non_network_walk as access/egress to modes other than walk on the network was replaced by walk. - kn/gl-nov'19 public static final String non_network_walk = "non_network_walk" ; diff --git a/matsim/src/main/java/org/matsim/counts/Count.java b/matsim/src/main/java/org/matsim/counts/Count.java index 328144cde71..6901214f23f 100644 --- a/matsim/src/main/java/org/matsim/counts/Count.java +++ b/matsim/src/main/java/org/matsim/counts/Count.java @@ -20,92 +20,139 @@ package org.matsim.counts; -import java.util.HashMap; - +import it.unimi.dsi.fastutil.ints.Int2DoubleMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.matsim.api.core.v01.Coord; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.Identifiable; +import org.matsim.api.core.v01.TransportMode; +import java.util.HashMap; +import java.util.OptionalDouble; + +/** + * This class is a wrapper around the newer {@link MeasurementLocation}. + * + * @param + */ +@Deprecated public class Count implements Identifiable { - private final Id linkId; - private String stationName; - private final HashMap volumes = new HashMap<>(); - private Coord coord; + private static final Logger log = LogManager.getLogger(Counts.class); + + private final MeasurementLocation location; + private final Measurable volume; + + protected Count(MeasurementLocation v) { + this.location = v; + String mode; + if (location.hasMeasurableForMode(Measurable.VOLUMES, Measurable.ANY_MODE)) + mode = Measurable.ANY_MODE; + else + mode = TransportMode.car; - protected Count(final Id linkId2, final String stationName) { - this.linkId = linkId2; - this.stationName = stationName; + // Counts only support hourly volumes + this.volume = location.createVolume(mode, Measurable.HOURLY); + + if (!this.volume.supportsHourlyAggregate()) { + log.warn("Unsupported counts interval '" + this.volume.getInterval() + "' for analysis functionality. " + + "Interval should be able to aggregate hourly, otherwise counts will be empty"); + } } /** * Creates and adds a {@link Volume} to the {@link Count}ing station. - * @param h indicating the hour-of-day. Note: the hours for a counting - * station must be from 1-24, and not from 0-23, - * otherwise the {@link MatsimCountsReader} will throw an error. - * + * + * @param h indicating the hour-of-day. Note: the hours for a counting + * station must be from 1-24, and not from 0-23, + * otherwise the {@link MatsimCountsReader} will throw an error. + * * @param val the total number of vehicles counted during hour h. * @return the {@link Count}ing station's {@link Volume}. */ public final Volume createVolume(final int h, final double val) { - if ( h < 1 ) { - throw new RuntimeException( "counts start at 1, not at 0. If you have a use case where you need to go below one, " - + "let us know and we think about it, but so far we had numerous debugging sessions because someone inserted counts at 0.") ; + if (h < 1) { + throw new RuntimeException("counts start at 1, not at 0. If you have a use case where you need to go below one, " + + "let us know and we think about it, but so far we had numerous debugging sessions because someone inserted counts at 0."); } - // overkill? - Volume v = new Volume(h,val); - this.volumes.put(Integer.valueOf(h), v); + + Volume v = new Volume(h, val); + + // Only hourly volumes can be set + if (this.volume.getInterval() == Measurable.HOURLY) + this.volume.setAtHour(h - 1, val); + else + log.warn("Unsupported counts '" + this.volume.getInterval() + "' for setting values. Resolution must be hourly."); + return v; } public final void setCsId(final String cs_id) { - this.stationName = cs_id; + location.setStationName(cs_id); } @Override public final Id getId() { - return this.linkId; + return location.getRefId(); } public final String getCsLabel() { - return this.stationName; + return location.getStationName(); } public final Volume getMaxVolume() { - Volume v_max = null; + int hour = -1; double max = -1.0; - for (Volume v : this.volumes.values()) { - if (v.getValue() > max) { max = v.getValue(); v_max = v; } + for (Int2DoubleMap.Entry e : this.volume) { + if (e.getDoubleValue() > max) { + max = e.getDoubleValue(); + hour = e.getIntKey() / Measurable.HOURLY; + } } - return v_max; + return hour >= 0 ? new Volume(hour + 1, max) : null; } public final Volume getVolume(final int h) { - return this.volumes.get(Integer.valueOf(h)); - } + if (this.volume.supportsHourlyAggregate()) { + // Uses old format where hours starts at 1 + OptionalDouble v = this.volume.aggregateAtHour(h - 1); + return v.isPresent() ? new Volume(h, v.getAsDouble()) : null; + } - public final HashMap getVolumes() { - return this.volumes; + return null; } - public void setCoord(final Coord coord) { - this.coord = coord; + public final HashMap getVolumes() { + HashMap res = new HashMap<>(); + for (Int2DoubleMap.Entry e : volume) { + // Offset for old API + int h = (e.getIntKey() / Measurable.HOURLY) + 1; + res.put(h, new Volume(h, e.getDoubleValue())); + } + + return res; } - /** @return Returns the exact coordinate, where this counting station is + /** + * @return Returns the exact coordinate, where this counting station is * located, or null if no exact location is available. **/ public Coord getCoord() { - return this.coord; + return location.getCoordinates(); + } + + public void setCoord(final Coord coord) { + location.setCoordinates(coord); } @Override public final String toString() { - return "[Loc_id=" + this.linkId + "]" + - "[cs_id=" + this.stationName + "]" + - "[nof_volumes=" + this.volumes.size() + "]"; + return "[Loc_id=" + this.location.getId() + "]" + + "[cs_id=" + this.location.getStationName() + "]" + + "[nof_volumes=" + this.volume.size() + "]"; } } diff --git a/matsim/src/main/java/org/matsim/counts/Counts.java b/matsim/src/main/java/org/matsim/counts/Counts.java index 4bc9af6f654..f0919822946 100644 --- a/matsim/src/main/java/org/matsim/counts/Counts.java +++ b/matsim/src/main/java/org/matsim/counts/Counts.java @@ -1,87 +1,152 @@ -/* *********************************************************************** * - * project: org.matsim.* - * Counts.java - * * - * *********************************************************************** * - * * - * copyright : (C) 2007 by the members listed in the COPYING, * - * LICENSE and WARRANTY file. * - * email : info at matsim dot org * - * * - * *********************************************************************** * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * See also COPYING, LICENSE and WARRANTY file * - * * - * *********************************************************************** */ - package org.matsim.counts; import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Identifiable; +import org.matsim.utils.objectattributes.attributable.Attributable; +import org.matsim.utils.objectattributes.attributable.Attributes; +import org.matsim.utils.objectattributes.attributable.AttributesImpl; +import java.util.Map; +import java.util.Set; import java.util.TreeMap; - -public class Counts { - - public static final String ELEMENT_NAME = "counts"; - - private String name = null; - private String desc = null; - private int year = 0; - private final TreeMap, Count> counts = new TreeMap<>(); +import java.util.stream.Collectors; + +/** + * This class provides count object, that can assign any measurable values (traffic volumes, velocities e.g.) for any matsim transport mode + * to an identifiable object (links, nodes, transit stations e.g) + * Structure is similar to regular counts, but more flexible to use. + */ +public final class Counts> implements Attributable { + + public static final String ELEMENT_NAME = "counts"; + private final Map, MeasurementLocation> locations = new TreeMap<>(); + private final Attributes attributes = new AttributesImpl(); + private String name; + private String description; + private String source; + private int year; + + public Counts() { + } /** - * @param linkId the link to which the counting station is assigned, must be unique - * @param stationName some additional identifier for humans, e.g. the original name/id of the counting station - * @return the created Count object, or {@linkplain RuntimeException} if it could not be created because it already exists + * Creates a MeasurementLocation object and adds to count tree map. Argument has to be an id for an matsim Identifiable object (link, node, pt station e.g). */ - public final Count createAndAddCount(final Id linkId, final String stationName) { - // check id string for uniqueness - if (this.counts.containsKey(linkId)) { - throw new RuntimeException("There is already a counts object for location " + linkId); + public MeasurementLocation createAndAddMeasureLocation(final Id id, String stationName) { + + if (this.locations.containsKey(id)) { + throw new RuntimeException("There is already a measurement object for location " + id.toString()); } - Count c = new Count<>(linkId, stationName); - this.counts.put(linkId, c); - return c; + + MeasurementLocation loc = new MeasurementLocation(id, stationName); + this.locations.put(id, loc); + + return loc; } - public final void setName(final String name) { + public String getName() { + return name; + } + + public void setName(String name) { this.name = name; } - public final void setDescription(final String desc) { - this.desc = desc; + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public int getYear() { + return year; } - public final void setYear(final int year) { + public void setYear(int year) { this.year = year; } - public final String getName() { - return this.name; + /** + * Returns all measured types of observations for all modes. + */ + public Set getMeasurableTypes() { + return locations.values().stream() + .map(MeasurementLocation::getMeasurables) + .flatMap(e -> e.keySet().stream()) + .map(MeasurementLocation.TypeAndMode::type) + .collect(Collectors.toSet()); } - public final String getDescription() { - return this.desc; + public Map, MeasurementLocation> getMeasureLocations() { + return locations; } - public final int getYear() { - return this.year; + public MeasurementLocation getMeasureLocation(Id id) { + return this.locations.get(id); + } + + /// Old API + // These functions belong to the older Counts API and are kept for compatibility + // Consider using the new API, which provides more flexibility (createAndAddLocation, getMeasureLocations) + + /** + * Old style API to create a measurement for car volumes at one station. + * Consider using {@link #createAndAddMeasureLocation(Id, String)} instead. + * + * @param linkId the link to which the counting station is assigned, must be unique + * @param stationName some additional identifier for humans, e.g. the original name/id of the counting station + * @return the created Count object, or {@linkplain RuntimeException} if it could not be created because it already exists + * @deprecated use {@link #createAndAddMeasureLocation(Id, String)} instead + */ + @Deprecated + public final Count createAndAddCount(final Id linkId, final String stationName) { + MeasurementLocation location = createAndAddMeasureLocation(linkId, stationName); + return new Count<>(location); } + /** + * Retrieve map of all counts. This will be inefficient because all intermediate objects for the old API will be created. + * + * @deprecated use {@link #getMeasureLocations()} instead + */ + @Deprecated public final TreeMap, Count> getCounts() { - return this.counts; + TreeMap, Count> result = new TreeMap<>(); + + for (Map.Entry, MeasurementLocation> e : locations.entrySet()) { + result.put(e.getKey(), new Count<>(e.getValue())); + } + + return result; + } + + /** + * Use {@link #getMeasureLocation(Id)} instead. + */ + @Deprecated + public Count getCount(final Id locId) { + MeasurementLocation loc = getMeasureLocation(locId); + return loc == null ? null : new Count<>(loc); } - public final Count getCount(final Id locId) { - return this.counts.get(locId); + + @Override + public Attributes getAttributes() { + return attributes; } @Override - public final String toString() { - return "[name=" + this.name + "]" + "[nof_counts=" + this.counts.size() + "]"; + public String toString() { + return "[name=" + this.name + "]" + "[nof_locations=" + this.locations.size() + "]"; } } diff --git a/matsim/src/main/java/org/matsim/counts/CountsReaderMatsimV2.java b/matsim/src/main/java/org/matsim/counts/CountsReaderMatsimV2.java new file mode 100644 index 00000000000..9a7013ff0a9 --- /dev/null +++ b/matsim/src/main/java/org/matsim/counts/CountsReaderMatsimV2.java @@ -0,0 +1,104 @@ +package org.matsim.counts; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.Identifiable; +import org.matsim.core.utils.geometry.CoordinateTransformation; +import org.matsim.core.utils.geometry.transformations.IdentityTransformation; +import org.matsim.core.utils.io.MatsimXmlParser; +import org.matsim.utils.objectattributes.attributable.AttributesXmlReaderDelegate; +import org.xml.sax.Attributes; + +import java.util.Stack; + +/** + * A reader for counts-files of MATSim according to counts_v2.xsd. + */ +public class CountsReaderMatsimV2 extends MatsimXmlParser { + + private final static String VALUE = "value"; + private final static String ATTRIBUTES = "attributes"; + + private final Class> idClass; + private final CoordinateTransformation coordinateTransformation; + private final Counts counts; + private final AttributesXmlReaderDelegate attributesDelegate = new AttributesXmlReaderDelegate(); + private MeasurementLocation currLocation; + private Measurable currMeasurable; + private org.matsim.utils.objectattributes.attributable.Attributes currAttributes = null; + + public CountsReaderMatsimV2(Counts counts, Class> idClass) { + this(new IdentityTransformation(), counts, idClass); + } + + public CountsReaderMatsimV2(CoordinateTransformation coordinateTransformation, Counts counts, Class> idClass) { + super(ValidationType.NO_VALIDATION); + this.coordinateTransformation = coordinateTransformation; + this.counts = counts; + this.idClass = idClass; + } + + @Override + public void startTag(String name, Attributes atts, Stack context) { + + switch (name) { + case Counts.ELEMENT_NAME -> startMultiModeCounts(atts); + case MeasurementLocation.ELEMENT_NAME -> startMeasurementLocation(atts); + case Measurable.ELEMENT_NAME -> startMeasurable(name, atts); + case ATTRIBUTES -> attributesDelegate.startTag(name, atts, context, currAttributes); + case VALUE -> addValuesToMeasurable(atts); + } + } + + @Override + public void endTag(String name, String content, Stack context) { + + } + + private void addValuesToMeasurable(Attributes atts) { + int t = Integer.parseInt(atts.getValue("t")); + double val = Double.parseDouble(atts.getValue("val")); + this.currMeasurable.setAtSecond(t, val); + } + + private void startMeasurable(String tag, Attributes atts) { + int interval = Integer.parseInt(atts.getValue("interval")); + currMeasurable = currLocation.createMeasurable(atts.getValue("type"), atts.getValue("networkMode"), interval); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void startMeasurementLocation(Attributes atts) { + String idString = atts.getValue("refId"); + Id id = Id.create(idString, this.idClass); + String stationName = atts.getValue("name"); + + currLocation = counts.createAndAddMeasureLocation(id, stationName); + currLocation.setId(atts.getValue("id")); + + String x = atts.getValue("x"); + String y = atts.getValue("y"); + if (x != null && y != null) { + currLocation.setCoordinates(coordinateTransformation.transform( + new Coord(Double.parseDouble(x), Double.parseDouble(y)) + )); + } + + currAttributes = currLocation.getAttributes(); + } + + private void startMultiModeCounts(Attributes atts) { + currAttributes = counts.getAttributes(); + + for (int i = 0; i < atts.getLength(); i++) { + String name = atts.getQName(i); + String value = atts.getValue(i); + + switch (name) { + case "name" -> counts.setName(value); + case "source" -> counts.setSource(value); + case "year" -> counts.setYear(Integer.parseInt(value)); + case "description" -> counts.setDescription(value); + } + } + } +} diff --git a/matsim/src/main/java/org/matsim/counts/CountsWriter.java b/matsim/src/main/java/org/matsim/counts/CountsWriter.java index 12bef3e217f..85adfbceca5 100644 --- a/matsim/src/main/java/org/matsim/counts/CountsWriter.java +++ b/matsim/src/main/java/org/matsim/counts/CountsWriter.java @@ -26,112 +26,41 @@ import org.matsim.core.utils.io.MatsimXmlWriter; import java.io.IOException; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Vector; +/** + * Generalized counts writer. Which delegates to specific version. Version 1 can be forced with system property + * MATSIM_COUNTS_VERSION=1 + */ public class CountsWriter extends MatsimXmlWriter implements MatsimWriter { - private final CountsWriterHandler handler; + private final CoordinateTransformation ct; private final Counts counts; public CountsWriter(final Counts counts) { - this( new IdentityTransformation() , counts ); + this(new IdentityTransformation(), counts); } public CountsWriter( - final CoordinateTransformation coordinateTransformation, - final Counts counts) { + final CoordinateTransformation ct, + final Counts counts) { + this.ct = ct; this.counts = counts; - - // use the newest writer-version by default - this.handler = new CountsWriterHandlerImplV1(coordinateTransformation); } @Override public final void write(final String filename) { try { - openFile(filename); - doTheWriting(); - close(); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - - public final void write(final OutputStream stream) { - try { - openOutputStream(stream); - doTheWriting(); - close(); - } - catch (IOException e) { + if (System.getProperty("MATSIM_COUNTS_VERSION", "").equalsIgnoreCase("1")) + new CountsWriterV1(ct, counts).write(filename); + else + new CountsWriterV2(ct, counts).write(filename); + } catch (IOException e) { throw new RuntimeException(e); } } - private void doTheWriting() throws IOException { - // write custom header - writeXmlHead(); - - this.handler.startCounts(this.counts, this.writer); - this.handler.writeSeparator(this.writer); - - List countsTemp = new Vector(); - countsTemp.addAll(this.counts.getCounts().values()); - Collections.sort(countsTemp, new CountComparator()); - - for (Count c : countsTemp) { - List volumesTemp = new Vector(); - volumesTemp.addAll(c.getVolumes().values()); - Collections.sort(volumesTemp, new VolumeComparator()); - this.handler.startCount(c, this.writer); - for (Volume v : volumesTemp) { - this.handler.startVolume(v, this.writer); - this.handler.endVolume(this.writer); - } - this.handler.endCount(this.writer); - this.handler.writeSeparator(this.writer); - this.writer.flush(); - } - this.handler.endCounts(this.writer); - } - @Override public final String toString() { return super.toString(); } - - static class VolumeComparator implements Comparator, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(final Volume v1, final Volume v2) { - return Double.compare(v1.getHourOfDayStartingWithOne(), v2.getHourOfDayStartingWithOne()); - } - } - - static class CountComparator implements Comparator, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(final Count c1, final Count c2) { - - // This seemed to be intended for a rather special case with a rather special - // numbering of counts... michaz mar 10 - -// int i1 = Integer.parseInt(c1.getCsId().substring(c1.getCsId().length()-3)); -// int i2 = Integer.parseInt(c2.getCsId().substring(c2.getCsId().length()-3)); -// if (i1 < i2) return -1; -// else if (i1 > i2) return 1; -// else return 0; - - - return c1.getCsLabel().compareTo(c2.getCsLabel()); - } - } } diff --git a/matsim/src/main/java/org/matsim/counts/CountsWriterV1.java b/matsim/src/main/java/org/matsim/counts/CountsWriterV1.java new file mode 100644 index 00000000000..cf2f98c909b --- /dev/null +++ b/matsim/src/main/java/org/matsim/counts/CountsWriterV1.java @@ -0,0 +1,138 @@ +/* *********************************************************************** * + * project: org.matsim.* + * CountsWriter.java + * * + * *********************************************************************** * + * * + * copyright : (C) 2007 by the members listed in the COPYING, * + * LICENSE and WARRANTY file. * + * email : info at matsim dot org * + * * + * *********************************************************************** * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * See also COPYING, LICENSE and WARRANTY file * + * * + * *********************************************************************** */ + +package org.matsim.counts; + +import org.matsim.core.api.internal.MatsimWriter; +import org.matsim.core.utils.geometry.CoordinateTransformation; +import org.matsim.core.utils.geometry.transformations.IdentityTransformation; +import org.matsim.core.utils.io.MatsimXmlWriter; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Vector; + +@Deprecated +final class CountsWriterV1 extends MatsimXmlWriter implements MatsimWriter { + + private final CountsWriterHandler handler; + private final Counts counts; + + public CountsWriterV1(final Counts counts) { + this( new IdentityTransformation() , counts ); + } + + public CountsWriterV1( + final CoordinateTransformation coordinateTransformation, + final Counts counts) { + this.counts = counts; + + // use the newest writer-version by default + this.handler = new CountsWriterHandlerImplV1(coordinateTransformation); + } + + @Override + public final void write(final String filename) { + try { + openFile(filename); + doTheWriting(); + close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public final void write(final OutputStream stream) { + try { + openOutputStream(stream); + doTheWriting(); + close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void doTheWriting() throws IOException { + // write custom header + writeXmlHead(); + + this.handler.startCounts(this.counts, this.writer); + this.handler.writeSeparator(this.writer); + + List countsTemp = new Vector(); + countsTemp.addAll(this.counts.getCounts().values()); + Collections.sort(countsTemp, new CountComparator()); + + for (Count c : countsTemp) { + List volumesTemp = new Vector(); + volumesTemp.addAll(c.getVolumes().values()); + Collections.sort(volumesTemp, new VolumeComparator()); + this.handler.startCount(c, this.writer); + for (Volume v : volumesTemp) { + this.handler.startVolume(v, this.writer); + this.handler.endVolume(this.writer); + } + this.handler.endCount(this.writer); + this.handler.writeSeparator(this.writer); + this.writer.flush(); + } + this.handler.endCounts(this.writer); + } + + @Override + public final String toString() { + return super.toString(); + } + + static class VolumeComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public int compare(final Volume v1, final Volume v2) { + return Double.compare(v1.getHourOfDayStartingWithOne(), v2.getHourOfDayStartingWithOne()); + } + } + + static class CountComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public int compare(final Count c1, final Count c2) { + + // This seemed to be intended for a rather special case with a rather special + // numbering of counts... michaz mar 10 + +// int i1 = Integer.parseInt(c1.getCsId().substring(c1.getCsId().length()-3)); +// int i2 = Integer.parseInt(c2.getCsId().substring(c2.getCsId().length()-3)); +// if (i1 < i2) return -1; +// else if (i1 > i2) return 1; +// else return 0; + + + return c1.getCsLabel().compareTo(c2.getCsLabel()); + } + } +} diff --git a/matsim/src/main/java/org/matsim/counts/CountsWriterV2.java b/matsim/src/main/java/org/matsim/counts/CountsWriterV2.java new file mode 100644 index 00000000000..af56b65a987 --- /dev/null +++ b/matsim/src/main/java/org/matsim/counts/CountsWriterV2.java @@ -0,0 +1,177 @@ +package org.matsim.counts; + +import it.unimi.dsi.fastutil.ints.Int2DoubleMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Coord; +import org.matsim.core.utils.collections.Tuple; +import org.matsim.core.utils.geometry.CoordinateTransformation; +import org.matsim.core.utils.io.MatsimXmlWriter; +import org.matsim.utils.objectattributes.attributable.AttributesXmlWriterDelegate; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Writer class for counts_v2.xsd. + */ +final class CountsWriterV2 extends MatsimXmlWriter { + + private static final Logger logger = LogManager.getLogger(CountsWriterV2.class); + + private final Counts counts; + + private final AttributesXmlWriterDelegate attributesWriter = new AttributesXmlWriterDelegate(); + private final CoordinateTransformation coordinateTransformation; + + + /** + * Default Constructor, takes the Counts object as argument. + */ + public CountsWriterV2(CoordinateTransformation coordinateTransformation, Counts counts) { + this.coordinateTransformation = coordinateTransformation; + this.counts = counts; + } + + /** + * Writes the counts to a file. + * + * @param filename output file path + */ + public void write(String filename) throws IOException { + logger.info("Write Counts to {}", filename); + + this.openFile(filename); + + this.writeXmlHead(); + this.writeRootElement(); + + super.close(); + } + + /** + * Writes the counts to a file. + * + * @param filename output file path + */ + public void write(Path filename) throws IOException { + write(filename.toString()); + } + + private void writeRootElement() { + + List> atts = new ArrayList<>(); + + atts.add(createTuple(XMLNS, MatsimXmlWriter.MATSIM_NAMESPACE)); + atts.add(createTuple(XMLNS + ":xsi", DEFAULTSCHEMANAMESPACELOCATION)); + atts.add(createTuple("xsi:schemaLocation", MATSIM_NAMESPACE + " " + DEFAULT_DTD_LOCATION + "counts_v2.xsd")); + + if (counts.getSource() != null) + atts.add(createTuple("source", counts.getSource())); + + if (counts.getName() != null) + atts.add(createTuple("name", counts.getName())); + + if (counts.getDescription() != null) + atts.add(createTuple("description", counts.getDescription())); + + atts.add(createTuple("year", String.valueOf(counts.getYear()))); + + this.writeStartTag(Counts.ELEMENT_NAME, atts, false, true); + + attributesWriter.writeAttributes("\t", this.writer, counts.getAttributes()); + + writeCounts(); + + this.writeContent("\n", true); + this.writeEndTag(Counts.ELEMENT_NAME); + } + + private void writeCounts() { + + for (MeasurementLocation count : counts.getMeasureLocations().values()) { + + List> attributes = new ArrayList<>(); + + attributes.add(createTuple("refId", count.getRefId().toString())); + + if (count.getId() != null) + attributes.add(createTuple("id", count.getId())); + + if (count.getStationName() != null) + attributes.add(createTuple("name", count.getStationName())); + + if (count.getDescription() != null) + attributes.add(createTuple("description", counts.getDescription())); + + if (count.getCoordinates() != null) { + Coord c = coordinateTransformation.transform(count.getCoordinates()); + attributes.add(createTuple("x", c.getX())); + attributes.add(createTuple("y", c.getY())); + } + + writeStartTag(MeasurementLocation.ELEMENT_NAME, attributes, false, true); + + attributesWriter.writeAttributes("\t\t", this.writer, count.getAttributes()); + + //write volumes for each mode + writeMeasurables(count); + + writeEndTag(MeasurementLocation.ELEMENT_NAME); + writeEmptyLine(); + } + + } + + private void writeMeasurables(MeasurementLocation count) { + + Map measurables = count.getMeasurables(); + + for (Map.Entry entry : measurables.entrySet()) { + + Measurable m = entry.getValue(); + + int interval = m.getInterval(); + + writeStartTag(Measurable.ELEMENT_NAME, List.of( + new Tuple<>("type", m.getMeasurableType()), + new Tuple<>("networkMode", m.getMode()), + new Tuple<>("interval", String.valueOf(interval))) + ); + + //write values + Int2DoubleMap values = m.getValues(); + try { + for (int second : values.keySet()) { + double v = values.get(second); + writeEmptyLine(); + + //start tag + super.writer.write("\t\t\t\t"); + } + } catch (IOException e) { + logger.error("Error writing Measurables", e); + } + + writeEndTag(Measurable.ELEMENT_NAME); + } + } + + private void writeEmptyLine() { + try { + this.writer.write(NL); + } catch (IOException e) { + logger.warn("Error writing counts", e); + } + } +} diff --git a/matsim/src/main/java/org/matsim/counts/MatsimCountsReader.java b/matsim/src/main/java/org/matsim/counts/MatsimCountsReader.java index 1185b18176d..774c6c335ef 100644 --- a/matsim/src/main/java/org/matsim/counts/MatsimCountsReader.java +++ b/matsim/src/main/java/org/matsim/counts/MatsimCountsReader.java @@ -24,6 +24,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Identifiable; +import org.matsim.api.core.v01.network.Link; import org.matsim.core.utils.geometry.CoordinateTransformation; import org.matsim.core.utils.geometry.transformations.IdentityTransformation; import org.matsim.core.utils.io.MatsimXmlParser; @@ -39,11 +41,14 @@ public class MatsimCountsReader extends MatsimXmlParser { private final static Logger log = LogManager.getLogger(MatsimCountsReader.class); private final static String COUNTS_V1 = "counts_v1.xsd"; + private final static String COUNTS_V2 = "counts_v2.xsd"; private final Counts counts; private MatsimXmlParser delegate = null; private final CoordinateTransformation coordinateTransformation; + private final Class> idClass; + /** * Creates a new reader for MATSim counts files. @@ -51,7 +56,7 @@ public class MatsimCountsReader extends MatsimXmlParser { * @param counts The Counts-object to store the configuration settings in. */ public MatsimCountsReader(final Counts counts) { - this( new IdentityTransformation() , counts ); + this( new IdentityTransformation() , counts, Link.class); } /** @@ -63,9 +68,24 @@ public MatsimCountsReader(final Counts counts) { public MatsimCountsReader( final CoordinateTransformation coordinateTransformation, final Counts counts) { + this(coordinateTransformation, counts, Link.class); + } + + /** + * Creates a new reader for MATSim counts files. + * + * @param coordinateTransformation transformation from the CRS of the file to the internal CRS for MATSim + * @param counts the counts object to store the configuration settings in + * @param idClass id class of locations + */ + public MatsimCountsReader( + final CoordinateTransformation coordinateTransformation, + final Counts counts, + Class> idClass) { super(ValidationType.XSD_ONLY); this.coordinateTransformation = coordinateTransformation; this.counts = counts; + this.idClass = idClass; } @Override @@ -83,8 +103,12 @@ protected void setDoctype(final String doctype) { super.setDoctype(doctype); // Currently the only counts-type is v1 if (COUNTS_V1.equals(doctype)) { - this.delegate = new CountsReaderMatsimV1( coordinateTransformation , this.counts); + this.delegate = new CountsReaderMatsimV1( coordinateTransformation, this.counts); log.info("using counts_v1-reader."); + } else if (COUNTS_V2.equals(doctype)) { + this.delegate = new CountsReaderMatsimV2(coordinateTransformation, this.counts, idClass); + log.info("using counts_v2-reader."); + } else { throw new IllegalArgumentException("Doctype \"" + doctype + "\" not known."); } diff --git a/matsim/src/main/java/org/matsim/counts/Measurable.java b/matsim/src/main/java/org/matsim/counts/Measurable.java new file mode 100644 index 00000000000..8bb2764cf6d --- /dev/null +++ b/matsim/src/main/java/org/matsim/counts/Measurable.java @@ -0,0 +1,204 @@ +package org.matsim.counts; + +import it.unimi.dsi.fastutil.ints.Int2DoubleAVLTreeMap; +import it.unimi.dsi.fastutil.ints.Int2DoubleMap; +import it.unimi.dsi.fastutil.ints.Int2DoubleSortedMap; + +import java.util.Iterator; +import java.util.Objects; +import java.util.OptionalDouble; + +/** + * A MeasurementLocation station can hold any kind of measurable data to calibrate a scenario provided as an implementation of this interface. + * A single instance holds values for only one transport mode. + * Average velocities and traffic volumes are already implemented. + */ +public final class Measurable implements Iterable { + + /** + * String to denote that the mode includes all vehicles. + */ + public static final String ANY_MODE = "any_vehicle"; + static final String ELEMENT_NAME = "measurements"; + public static String VOLUMES = "volumes"; + public static String VELOCITIES = "velocities"; + public static String PASSENGERS = "passengers"; + + /** + * Daily interval in seconds. + */ + public static int DAILY = 24 * 60 * 60; + /** + * Hourly interval in seconds. + */ + public static int HOURLY = 60 * 60; + /** + * Quarter hourly interval in seconds. + */ + public static int QUARTER_HOURLY = 15 * 60; + + private final String type; + private final String mode; + + private final Int2DoubleSortedMap values; + + /** + * Measurement interval in minutes. + */ + private final int interval; + + Measurable(String mode, String type, int interval) { + this.mode = mode; + this.type = type; + this.values = new Int2DoubleAVLTreeMap(); + this.interval = interval; + } + + Int2DoubleMap getValues() { + return values; + } + + /** + * Adds a value observed at a certain hour. + */ + public void setAtHour(int hour, double value) { + setAtSecond(hour * HOURLY, value); + } + + /** + * Adds a value observed at a certain minute. Note that the minute must match the given interval, for example if the intervall is set to 15 minutes + * the minute must be something like 15, 30, 45, 300 etc. + */ + public void setAtMinute(int minute, double value) { + setAtSecond(minute * 60, value); + } + + public void setAtSecond(int seconds, double value) { + if (seconds < 0) + throw new IllegalArgumentException("Time value starts at 0."); + + if (seconds % this.interval != 0) + throw new IllegalArgumentException("Time value doesn't match the interval!"); + + this.values.put(seconds, value); + } + + /** + * Returns the observed daily value. + */ + public OptionalDouble getDailyValue() { + if (interval != DAILY) + throw new IllegalArgumentException("Does not contain daily values!"); + + return getAtHour(0); + } + + public void setDailyValue(double value) { + if (interval != DAILY) + throw new IllegalArgumentException("Does not contain daily values!"); + + setAtHour(0, value); + } + + /** + * Return sum of values or daily value. + */ + public double aggregateDaily() { + if (interval == DAILY) + return values.get(0); + + return values.values().doubleStream().sum(); + } + + /** + * Returns the observed value at a certain hour. + */ + public OptionalDouble getAtHour(int hour) { + return getAtSecond(hour * HOURLY); + } + + /** + * Whether the interval allows for an hourly aggregation. + */ + public boolean supportsHourlyAggregate() { + return (interval <= HOURLY) && (HOURLY % interval) == 0; + } + + /** + * Returns the aggregate for an specific hour. If the resolution does not allow this aggregation an error is thrpwn. + */ + public OptionalDouble aggregateAtHour(int hour) { + if (!supportsHourlyAggregate()) + throw new IllegalArgumentException("Can not aggregate hourly values."); + + if (interval == HOURLY) + return getAtHour(hour); + + Int2DoubleSortedMap values = this.values.subMap(hour * HOURLY, (hour + 1) * HOURLY); + if (values.isEmpty()) + return OptionalDouble.empty(); + + return OptionalDouble.of(values.values().doubleStream().sum()); + } + + public OptionalDouble getAtSecond(int second) { + if (values.containsKey(second)) + return OptionalDouble.of(values.get(second)); + + return OptionalDouble.empty(); + } + + /** + * Returns the observed value at a certain minute. + */ + public OptionalDouble getAtMinute(int minutes) { + return getAtSecond(minutes * 60); + } + + /** + * Returns the transport mode of the observed data. + */ + public String getMode() { + return mode; + } + + /** + * Returns the name of the implementation. Information is needed for data writing. + */ + public String getMeasurableType() { + return type; + } + + public int getInterval() { + return interval; + } + + /** + * Iterate over all values as seconds-value pairs. + */ + @Override + public Iterator iterator() { + return values.int2DoubleEntrySet().iterator(); + } + + /** + * Number of entries. + */ + public int size() { + return values.size(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Measurable entries = (Measurable) o; + return interval == entries.interval && Objects.equals(type, entries.type) && Objects.equals(mode, entries.mode) && Objects.equals(values, entries.values); + } + + @Override + public int hashCode() { + return Objects.hash(type, mode, values, interval); + } +} + diff --git a/matsim/src/main/java/org/matsim/counts/MeasurementLocation.java b/matsim/src/main/java/org/matsim/counts/MeasurementLocation.java new file mode 100644 index 00000000000..9f5b6802d39 --- /dev/null +++ b/matsim/src/main/java/org/matsim/counts/MeasurementLocation.java @@ -0,0 +1,178 @@ +package org.matsim.counts; + +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.utils.objectattributes.attributable.Attributable; +import org.matsim.utils.objectattributes.attributable.Attributes; +import org.matsim.utils.objectattributes.attributable.AttributesImpl; + +import javax.annotation.Nullable; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A MeasurementLocation can hold measurable traffic stats (traffic volumes or velocities e.g.) for a single matsim infrastructure object. + * Measurable values are provided as Measurable instances for a certain mode. It is possible to assign the same kind of value to + * several transport modes. + * A single MeasurementLocation instance for example can hold traffic volumes for the mode 'car' and average velocities for the mode 'freight'. + */ +public final class MeasurementLocation implements Attributable, Iterable { + + static final String ELEMENT_NAME = "location"; + + private final Id refId; + private final Map measurables = new LinkedHashMap<>(); + private final Attributes attributes = new AttributesImpl(); + + private String id; + private String stationName; + private String description; + private Coord coordinates; + + MeasurementLocation(final Id refId, String stationName) { + this.refId = refId; + this.stationName = stationName; + } + + /** + * Id reference to the matsim infrastructure object. + */ + public Id getRefId() { + return refId; + } + + /** + * Id that may be used internally, not corresponding to matsim ids. + */ + public String getId() { + return id; + } + + @Override + public Attributes getAttributes() { + return attributes; + } + + /** + * Create arbitrary measurable for certain mode and minute interval. If this measurable exists already, it is returned. + */ + public Measurable createMeasurable(String typeOfMeasurableData, String mode, int interval) { + return measurables.computeIfAbsent(new TypeAndMode(typeOfMeasurableData, mode), + k -> new Measurable(mode, typeOfMeasurableData, interval)); + } + + /** + * Delete measurable for certain mode. + */ + public boolean deleteMeasurable(String typeOfMeasurableData, String mode) { + return this.measurables.remove(new TypeAndMode(typeOfMeasurableData, mode)) != null; + } + + /** + * Create hourly volumes for car mode. + */ + public Measurable createVolume() { + return createMeasurable(Measurable.VOLUMES, TransportMode.car, Measurable.HOURLY); + } + + /** + * Create hourly values for certain mode. + */ + public Measurable createVolume(String mode) { + return createMeasurable(Measurable.VOLUMES, mode, Measurable.HOURLY); + } + + public Measurable createVolume(String mode, int interval) { + return createMeasurable(Measurable.VOLUMES, mode, interval); + } + + public Measurable createVelocity(String mode, int interval) { + return createMeasurable(Measurable.VELOCITIES, mode, interval); + } + + public Measurable createPassengerCounts(String mode, int interval) { + return createMeasurable(Measurable.PASSENGERS, mode, interval); + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getStationName() { + return stationName; + } + + /** + * Returns the display name of this location. If the station name is set, it is returned. Otherwise the id is returned. + */ + public String getDisplayName() { + if (stationName != null) + return stationName; + if (id != null) + return id; + + return description; + } + + public void setId(String id) { + this.id = id; + } + + public void setStationName(String stationName) { + this.stationName = stationName; + } + + public Coord getCoordinates() { + return coordinates; + } + + public void setCoordinates(Coord coordinates) { + this.coordinates = coordinates; + } + + public Measurable getVolumesForMode(String mode) { + return this.measurables.get(new TypeAndMode(Measurable.VOLUMES, mode)); + } + + /** + * Return whether this location has measurable data for certain mode. + */ + public boolean hasMeasurableForMode(String measurableType, String mode) { + return this.measurables.containsKey(new TypeAndMode(measurableType, mode)); + } + + @Nullable + public Measurable getMeasurableForMode(String measurableType, String mode) { + return this.measurables.get(new TypeAndMode(measurableType, mode)); + } + + Map getMeasurables() { + return this.measurables; + } + + @Override + public String toString() { + return "MeasurementLocation{" + + "id=" + refId + + ", stationName='" + stationName + '\'' + + '}'; + } + + @Override + public Iterator iterator() { + return this.measurables.keySet().iterator(); + } + + /** + * Stores the measurable type and mode of a {@link Measurable}. + */ + public final record TypeAndMode(String type, String mode) { + } + +} diff --git a/matsim/src/main/java/org/matsim/counts/Volume.java b/matsim/src/main/java/org/matsim/counts/Volume.java index 68401e80fab..c37df5a7749 100644 --- a/matsim/src/main/java/org/matsim/counts/Volume.java +++ b/matsim/src/main/java/org/matsim/counts/Volume.java @@ -21,14 +21,17 @@ package org.matsim.counts; // import org.matsim.demandmodeling.gbl.Gbl; +/** + * Old API to access data fron {@link Count}. This class is not needed when using new API via {@link MeasurementLocation}. + */ public class Volume { private final int h_; private double val_; protected Volume(final int h, final double val) { - + /* no error checking needed as we use schema instead of dtd - + if ((h == -1)) { Gbl.errorMsg("[h="+h+", negative values are not allowed!]"); } @@ -36,9 +39,9 @@ protected Volume(final int h, final double val) { Gbl.errorMsg("[val="+val+", negative values are not allowed!]"); } */ - + this.h_ = h; - this.val_ = val; + this.val_ = val; } public final void setValue(double val) { @@ -51,7 +54,7 @@ public final int getHourOfDayStartingWithOne() { public final double getValue() { return this.val_; } - + @Override public final String toString() { return "[" + this.h_ + "===" + this.val_ + "]"; diff --git a/matsim/src/main/resources/dtd/counts_v2.xsd b/matsim/src/main/resources/dtd/counts_v2.xsd new file mode 100644 index 00000000000..956b1411944 --- /dev/null +++ b/matsim/src/main/resources/dtd/counts_v2.xsd @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/matsim/src/test/java/org/matsim/counts/CountTest.java b/matsim/src/test/java/org/matsim/counts/CountTest.java index 51c3dc7a9d8..3ea7868ec61 100644 --- a/matsim/src/test/java/org/matsim/counts/CountTest.java +++ b/matsim/src/test/java/org/matsim/counts/CountTest.java @@ -24,6 +24,7 @@ import java.util.Iterator; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.matsim.api.core.v01.Id; @@ -35,22 +36,28 @@ public class CountTest { @Rule public MatsimTestUtils utils = new MatsimTestUtils(); + private Counts counts; + + @Before + public void setUp() throws Exception { + this.counts = new Counts<>(); + } @Test public void testCreateVolume() { - Count count = new Count(Id.create(0, Link.class), "1"); + Count count = counts.createAndAddCount(Id.create(0, Link.class), "1"); Volume volume = count.createVolume(1, 100.0); assertTrue("Creation and initialization of volume failed", volume.getHourOfDayStartingWithOne()==1); assertTrue("Creation and initialization of volume failed", volume.getValue()==100.0); } @Test public void testGetVolume() { - Count count = new Count(Id.create(0, Link.class), "1"); + Count count = counts.createAndAddCount(Id.create(0, Link.class), "1"); count.createVolume(1, 100.0); assertTrue("Getting volume failed", count.getVolume(1).getValue() == 100.0); } @Test public void testGetVolumes() { - Count count = new Count(Id.create(0, Link.class), "1"); + Count count = counts.createAndAddCount(Id.create(0, Link.class), "1"); count.createVolume(1, 100.0); Iterator vol_it = count.getVolumes().values().iterator(); diff --git a/matsim/src/test/java/org/matsim/counts/CountsParserWriterTest.java b/matsim/src/test/java/org/matsim/counts/CountsParserWriterTest.java index ea2bf38265c..0d9c6b0dd34 100644 --- a/matsim/src/test/java/org/matsim/counts/CountsParserWriterTest.java +++ b/matsim/src/test/java/org/matsim/counts/CountsParserWriterTest.java @@ -97,7 +97,7 @@ public void testWriteParse_nameIsNull() throws SAXException, ParserConfiguration f.counts.setName(null); Assert.assertNull(f.counts.getName()); String filename = this.utils.getOutputDirectory() + "counts.xml"; - new CountsWriter(f.counts).write(filename); + new CountsWriterV1(f.counts).write(filename); Counts counts2 = new Counts(); new CountsReaderMatsimV1(counts2).readFile(filename); diff --git a/matsim/src/test/java/org/matsim/counts/CountsV2Test.java b/matsim/src/test/java/org/matsim/counts/CountsV2Test.java new file mode 100644 index 00000000000..696aa521795 --- /dev/null +++ b/matsim/src/test/java/org/matsim/counts/CountsV2Test.java @@ -0,0 +1,180 @@ +package org.matsim.counts; + +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.core.network.NetworkUtils; +import org.matsim.core.utils.geometry.transformations.IdentityTransformation; +import org.matsim.examples.ExamplesUtils; +import org.matsim.testcases.MatsimTestUtils; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; +import java.util.SplittableRandom; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CountsV2Test { + + private final SplittableRandom random = new SplittableRandom(1234); + @Rule + public MatsimTestUtils utils = new MatsimTestUtils(); + + @Test + public void test_general_handling() throws IOException { + + Counts counts = new Counts<>(); + counts.setName("test"); + counts.setYear(2100); + counts.setDescription("Test counts for several transport modes."); + counts.setSource("unit4"); + counts.getAttributes().putAttribute("generationType", "random"); + + generateDummyCounts(counts); + + CountsWriterV2 writer = new CountsWriterV2(new IdentityTransformation(), counts); + String filename = utils.getOutputDirectory() + "test_counts.xml"; + + writer.write(filename); + assertThat(Files.exists(Path.of(filename))).isTrue(); + } + + @Test + public void test_reader_writer() throws IOException { + + String filename = utils.getOutputDirectory() + "test_counts.xml"; + + Counts dummyCounts = new Counts<>(); + generateDummyCounts(dummyCounts); + + CountsWriterV2 writer = new CountsWriterV2(new IdentityTransformation(), dummyCounts); + writer.write(filename); + + Counts counts = new Counts<>(); + CountsReaderMatsimV2 reader = new CountsReaderMatsimV2(counts, Link.class); + + Assertions.assertThatNoException().isThrownBy(() -> reader.readFile(filename)); + + Map, MeasurementLocation> countMap = counts.getMeasureLocations(); + Assert.assertEquals(21, countMap.size()); + + boolean onlyDailyValues = countMap.get(Id.create("12", Link.class)).getMeasurableForMode(Measurable.VOLUMES, TransportMode.car).getInterval() == 24 * 60; + Assert.assertFalse(onlyDailyValues); + + assertThat(dummyCounts.getMeasurableTypes()) + .isEqualTo(counts.getMeasurableTypes()); + + + // Compare if all content is equal + for (Map.Entry, MeasurementLocation> e : dummyCounts.getMeasureLocations().entrySet()) { + MeasurementLocation otherLocation = counts.getMeasureLocation(e.getKey()); + + for (MeasurementLocation.TypeAndMode typeAndMode : otherLocation) { + + Measurable m = e.getValue().getMeasurableForMode(typeAndMode.type(), typeAndMode.mode()); + assertThat(m) + .isEqualTo(otherLocation.getMeasurableForMode(typeAndMode.type(), typeAndMode.mode())); + } + } + } + + + @Test(expected = IllegalArgumentException.class) + public void test_illegal() { + + Counts dummyCounts = new Counts<>(); + + MeasurementLocation station = dummyCounts.createAndAddMeasureLocation(Id.create("12", Link.class), "12_test"); + Measurable volume = station.createVolume(TransportMode.car, Measurable.HOURLY); + + volume.setAtHour(-1, 500); + + } + + + @Test + public void aggregate() { + + Counts counts = new Counts<>(); + + MeasurementLocation station = counts.createAndAddMeasureLocation(Id.createLinkId(1), "test"); + Measurable volumes = station.createVolume(TransportMode.car, Measurable.QUARTER_HOURLY); + + volumes.setAtMinute(0, 100); + volumes.setAtMinute(15, 100); + volumes.setAtMinute(45, 100); + volumes.setAtMinute(60, 100); + volumes.setAtMinute(75, 100); + + assertThat(volumes.aggregateAtHour(0).orElse(-1)) + .isEqualTo(300); + + + assertThat(volumes.aggregateAtHour(1).orElse(-1)) + .isEqualTo(200); + + assertThat(volumes.aggregateAtHour(2).isEmpty()) + .isTrue(); + + assertThat(volumes.aggregateDaily()) + .isEqualTo(500); + } + + public void generateDummyCounts(Counts counts) { + Set modes = Set.of(TransportMode.car, TransportMode.bike, TransportMode.drt); + + URL berlin = ExamplesUtils.getTestScenarioURL("berlin"); + + Network network = NetworkUtils.readNetwork(berlin + "network.xml.gz"); + + int counter = 0; + + for (Id id : network.getLinks().keySet()) { + if (counter++ > 20) + break; + + if (id.toString().equals("12")) { + MeasurementLocation count = counts.createAndAddMeasureLocation(id, id + "_test"); + Measurable volume = count.createVolume(TransportMode.car, Measurable.HOURLY); + + for (int i = 0; i <= 23; i++) { + volume.setAtHour(i, random.nextInt(300, 800)); + } + continue; + } + + MeasurementLocation count = counts.createAndAddMeasureLocation(id, id + "_test"); + + if (random.nextBoolean()) + count.getAttributes().putAttribute("testAttribute", "test"); + + for (String mode : modes) { + boolean dailyValuesOnly = random.nextBoolean(); + Measurable volume; + if (dailyValuesOnly) { + volume = count.createMeasurable(Measurable.VOLUMES, mode, Measurable.DAILY); + volume.setDailyValue(random.nextInt(1000, 10000)); + } else { + volume = count.createVolume(mode, Measurable.HOURLY); + for (int i = 0; i <= 23; i++) + volume.setAtHour(i, random.nextInt(300, 800)); + + } + + if (random.nextBoolean()) { + Measurable velocity = count.createVelocity(mode, Measurable.DAILY); + velocity.setDailyValue(random.nextDouble(27.78)); + } + } + } + } +} diff --git a/matsim/src/test/java/org/matsim/counts/MatsimCountsIOTest.java b/matsim/src/test/java/org/matsim/counts/MatsimCountsIOTest.java index d8888cad5a4..3e3ef220219 100644 --- a/matsim/src/test/java/org/matsim/counts/MatsimCountsIOTest.java +++ b/matsim/src/test/java/org/matsim/counts/MatsimCountsIOTest.java @@ -131,7 +131,7 @@ public void testReading_year1padded() { public void testDefaultYear_empty() { Counts counts = new Counts(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - new CountsWriter(counts).write(out); + new CountsWriterV1(counts).write(out); Counts counts2 = new Counts(); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); @@ -139,4 +139,4 @@ public void testDefaultYear_empty() { // there should not have been an Exception } -} \ No newline at end of file +}