From 1c940ae9382385b67e23f2f146d31dd3041fe05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Friedrich=20V=C3=B6lkers?= Date: Wed, 18 Dec 2024 11:05:07 +0100 Subject: [PATCH] added activity dashboard and activity analysis (#3544) * added activity dashboard and activity analysis * first version of the activity analysis and the dashboard * add csv example for berlin * remove unused files * remove unused files * remove unused files * remove unused files * remove unused files * remove unused files * remove unused files * remove unused files * remove unused files * Improve wording on the activity dashboards * add some documentation * fix: activity dashboard bug * catch non existing network nodes * update activity dashboard * fix relative legend * fix scale in test data * fix number of breakpoints --------- Co-authored-by: rakow --- .../activity/ActivityCountAnalysis.java | 220 ++++++++++++++++++ .../traveltime/TravelTimeComparison.java | 23 ++ .../application/options/ShpOptions.java | 1 + .../main/java/org/matsim/simwrapper/Data.java | 33 ++- .../simwrapper/SimWrapperConfigGroup.java | 14 ++ .../matsim/simwrapper/SimWrapperListener.java | 7 +- .../matsim/simwrapper/SimWrapperRunner.java | 5 + .../dashboard/ActivityDashboard.java | 185 +++++++++++++++ .../org/matsim/simwrapper/viz/MapPlot.java | 6 +- .../simwrapper/dashboard/DashboardTests.java | 19 +- .../src/test/resources/kehlheim_ref.csv | 12 + .../src/test/resources/kehlheim_shape.cpg | 1 + .../src/test/resources/kehlheim_shape.dbf | Bin 0 -> 187 bytes .../src/test/resources/kehlheim_shape.prj | 1 + .../src/test/resources/kehlheim_shape.qmd | 44 ++++ .../src/test/resources/kehlheim_shape.shp | Bin 0 -> 4476 bytes .../src/test/resources/kehlheim_shape.shx | Bin 0 -> 188 bytes 17 files changed, 564 insertions(+), 7 deletions(-) create mode 100644 contribs/application/src/main/java/org/matsim/application/analysis/activity/ActivityCountAnalysis.java create mode 100644 contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/ActivityDashboard.java create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_ref.csv create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_shape.cpg create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_shape.dbf create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_shape.prj create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_shape.qmd create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_shape.shp create mode 100644 contribs/simwrapper/src/test/resources/kehlheim_shape.shx diff --git a/contribs/application/src/main/java/org/matsim/application/analysis/activity/ActivityCountAnalysis.java b/contribs/application/src/main/java/org/matsim/application/analysis/activity/ActivityCountAnalysis.java new file mode 100644 index 00000000000..b944ee044fb --- /dev/null +++ b/contribs/application/src/main/java/org/matsim/application/analysis/activity/ActivityCountAnalysis.java @@ -0,0 +1,220 @@ +package org.matsim.application.analysis.activity; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.geotools.api.feature.Property; +import org.geotools.api.feature.simple.SimpleFeature; +import org.locationtech.jts.geom.Geometry; +import org.matsim.api.core.v01.Coord; +import org.matsim.application.CommandSpec; +import org.matsim.application.MATSimAppCommand; +import org.matsim.application.options.*; +import org.matsim.core.utils.io.IOUtils; +import picocli.CommandLine; +import tech.tablesaw.api.*; +import tech.tablesaw.io.csv.CsvReadOptions; +import tech.tablesaw.selection.Selection; + +import java.util.*; +import java.util.regex.Pattern; + +@CommandSpec( + requires = {"activities.csv"}, + produces = {"activities_%s_per_region.csv"} +) +public class ActivityCountAnalysis implements MATSimAppCommand { + + private static final Logger log = LogManager.getLogger(ActivityCountAnalysis.class); + + @CommandLine.Mixin + private final InputOptions input = InputOptions.ofCommand(ActivityCountAnalysis.class); + @CommandLine.Mixin + private final OutputOptions output = OutputOptions.ofCommand(ActivityCountAnalysis.class); + @CommandLine.Mixin + private ShpOptions shp; + @CommandLine.Mixin + private SampleOptions sample; + @CommandLine.Mixin + private CrsOptions crs; + + /** + * Specifies the column in the shapefile used as the region ID. + */ + @CommandLine.Option(names = "--id-column", description = "Column to use as ID for the shapefile", required = true) + private String idColumn; + + /** + * Maps patterns to merge activity types into a single category. + * Example: `home;work` can merge activities "home1" and "work1" into categories "home" and "work". + */ + @CommandLine.Option(names = "--activity-mapping", description = "Map of patterns to merge activity types", split = ";") + private Map activityMapping; + + /** + * Specifies activity types that should be counted only once per agent per region. + */ + @CommandLine.Option(names = "--single-occurrence", description = "Activity types that are only counted once per agent") + private Set singleOccurrence; + + public static void main(String[] args) { + new ActivityCountAnalysis().execute(args); + } + + /** + * Executes the activity count analysis. + * + * @return Exit code (0 for success). + * @throws Exception if errors occur during execution. + */ + @Override + public Integer call() throws Exception { + + // Prepares the activity mappings and reads input data + HashMap> formattedActivityMapping = new HashMap<>(); + Map regionAreaMap = new HashMap<>(); + + if (this.activityMapping == null) this.activityMapping = new HashMap<>(); + + for (Map.Entry entry : this.activityMapping.entrySet()) { + String pattern = entry.getKey(); + String activity = entry.getValue(); + Set activities = new HashSet<>(Arrays.asList(activity.split(","))); + formattedActivityMapping.put(pattern, activities); + } + + // Reading the input csv + Table activities = Table.read().csv(CsvReadOptions.builder(IOUtils.getBufferedReader(input.getPath("activities.csv"))) + .columnTypesPartial(Map.of("person", ColumnType.TEXT, "activity_type", ColumnType.TEXT)) + .sample(false) + .separator(CsvOptions.detectDelimiter(input.getPath("activities.csv"))).build()); + + // remove the underscore and the number from the activity_type column + TextColumn activityType = activities.textColumn("activity_type"); + activityType.set(Selection.withRange(0, activityType.size()), activityType.replaceAll("_[0-9]{2,}$", "")); + + ShpOptions.Index index = crs.getInputCRS() == null ? shp.createIndex(idColumn) : shp.createIndex(crs.getInputCRS(), idColumn); + + // stores the counts of activities per region + Object2ObjectOpenHashMap> regionActivityCounts = new Object2ObjectOpenHashMap<>(); + // stores the activities that have been counted for each person in each region + Object2ObjectOpenHashMap> personActivityTracker = new Object2ObjectOpenHashMap<>(); + + // iterate over the csv rows + for (Row row : activities) { + String person = row.getString("person"); + String activity = row.getText("activity_type"); + + for (Map.Entry> entry : formattedActivityMapping.entrySet()) { + String pattern = entry.getKey(); + Set activities2 = entry.getValue(); + for (String act : activities2) { + if (Pattern.matches(act, activity)) { + activity = pattern; + break; + } + } + } + + Coord coord = new Coord(row.getDouble("coord_x"), row.getDouble("coord_y")); + + // get the region for the current coordinate + SimpleFeature feature = index.queryFeature(coord); + + if (feature == null) { + continue; + } + + Geometry geometry = (Geometry) feature.getDefaultGeometry(); + + Property prop = feature.getProperty(idColumn); + if (prop == null) + throw new IllegalArgumentException("No property found for column %s".formatted(idColumn)); + + Object region = prop.getValue(); + if (region != null && region.toString().length() > 0) { + + double area = geometry.getArea(); + regionAreaMap.put(region.toString(), area); + + // Add region to the activity counts and person activity tracker if not already present + regionActivityCounts.computeIfAbsent(region, k -> new Object2IntOpenHashMap<>()); + personActivityTracker.computeIfAbsent(region, k -> new HashSet<>()); + + Set trackedActivities = personActivityTracker.get(region); + String personActivityKey = person + "_" + activity; + + // adding activity only if it has not been counted for the person in the region + if (singleOccurrence == null || !singleOccurrence.contains(activity) || !trackedActivities.contains(personActivityKey)) { + Object2IntMap activityCounts = regionActivityCounts.get(region); + activityCounts.mergeInt(activity, 1, Integer::sum); + + // mark the activity as counted for the person in the region + trackedActivities.add(personActivityKey); + } + } + } + + Set uniqueActivities = new HashSet<>(); + + for (Object2IntMap map : regionActivityCounts.values()) { + uniqueActivities.addAll(map.keySet()); + } + + for (String activity : uniqueActivities) { + Table resultTable = Table.create(); + TextColumn regionColumn = TextColumn.create("id"); + DoubleColumn activityColumn = DoubleColumn.create("count"); + DoubleColumn distributionColumn = DoubleColumn.create("relative_density"); + DoubleColumn countRatioColumn = DoubleColumn.create("density"); + DoubleColumn areaColumn = DoubleColumn.create("area"); + + resultTable.addColumns(regionColumn, activityColumn, distributionColumn, countRatioColumn, areaColumn); + for (Map.Entry> entry : regionActivityCounts.entrySet()) { + Object region = entry.getKey(); + double value = 0; + for (Map.Entry entry2 : entry.getValue().object2IntEntrySet()) { + String ect = entry2.getKey(); + if (Pattern.matches(ect, activity)) { + value = entry2.getValue() * sample.getUpscaleFactor(); + break; + } + } + + + Row row = resultTable.appendRow(); + row.setString("id", region.toString()); + row.setDouble("count", value); + } + + for (Row row : resultTable) { + Double area = regionAreaMap.get(row.getString("id")); + if (area != null) { + row.setDouble("area", area); + row.setDouble("density", row.getDouble("count") / area); + } else { + log.warn("Area for region {} is not found", row.getString("id")); + } + } + + Double averageDensity = countRatioColumn.mean(); + + for (Row row : resultTable) { + Double value = row.getDouble("density"); + if (averageDensity != 0) { + row.setDouble("relative_density", value / averageDensity); + } else { + row.setDouble("relative_density", 0.0); + } + } + + + resultTable.write().csv(output.getPath("activities_%s_per_region.csv", activity).toFile()); + log.info("Wrote activity counts for {} to {}", activity, output.getPath("activities_%s_per_region.csv", activity)); + } + + return 0; + } +} diff --git a/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/TravelTimeComparison.java b/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/TravelTimeComparison.java index 3c22fc678a3..41abdbcff54 100644 --- a/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/TravelTimeComparison.java +++ b/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/TravelTimeComparison.java @@ -1,5 +1,7 @@ package org.matsim.application.analysis.traffic.traveltime; +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.TransportMode; import org.matsim.api.core.v01.network.Link; @@ -47,6 +49,8 @@ ) public class TravelTimeComparison implements MATSimAppCommand { + private static final Logger log = LogManager.getLogger(TravelTimeComparison.class); + @CommandLine.Mixin private InputOptions input = InputOptions.ofCommand(TravelTimeComparison.class); @@ -90,6 +94,13 @@ public Integer call() throws Exception { for (Row row : data) { LeastCostPathCalculator.Path congested = computePath(network, congestedRouter, row); + + // Skip if path is not found + if (congested == null) { + row.setDouble("simulated", Double.NaN); + continue; + } + double dist = congested.links.stream().mapToDouble(Link::getLength).sum(); double speed = 3.6 * dist / congested.travelTime; @@ -102,6 +113,8 @@ public Integer call() throws Exception { row.setDouble("free_flow", speed); } + data = data.dropWhere(data.doubleColumn("simulated").isMissing()); + data.addColumns( data.doubleColumn("simulated").subtract(data.doubleColumn("mean")).setName("bias") ); @@ -129,6 +142,16 @@ private LeastCostPathCalculator.Path computePath(Network network, LeastCostPathC Node fromNode = network.getNodes().get(Id.createNodeId(row.getString("from_node"))); Node toNode = network.getNodes().get(Id.createNodeId(row.getString("to_node"))); + if (fromNode == null) { + log.error("Node {} not found in network", row.getString("from_node")); + return null; + } + + if (toNode == null) { + log.error("Node {} not found in network", row.getString("to_node")); + return null; + } + return router.calcLeastCostPath(fromNode, toNode, row.getInt("hour") * 3600, null, null); } diff --git a/contribs/application/src/main/java/org/matsim/application/options/ShpOptions.java b/contribs/application/src/main/java/org/matsim/application/options/ShpOptions.java index 5d9f8ccec99..55b7e01846e 100644 --- a/contribs/application/src/main/java/org/matsim/application/options/ShpOptions.java +++ b/contribs/application/src/main/java/org/matsim/application/options/ShpOptions.java @@ -230,6 +230,7 @@ public Geometry getGeometry() { /** * Return the union of all geometries in the shape file and project it to the target crs. + * * @param toCRS target coordinate system */ public Geometry getGeometry(String toCRS) { diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java index d13bbc48ae4..16029040d47 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java @@ -3,8 +3,10 @@ import org.apache.commons.io.FilenameUtils; import org.matsim.application.CommandRunner; import org.matsim.application.MATSimAppCommand; +import org.matsim.core.utils.io.IOUtils; import javax.annotation.Nullable; +import java.io.UncheckedIOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; @@ -154,13 +156,40 @@ public String subcommand(String command, String file) { */ public String resource(String name) { - URL resource = this.getClass().getResource(name); + String path = resolveResource(name, true); + + // Handle shape files separately, copy additional files that are known to belong to shp files + if (name.endsWith(".shp")) { + resolveResource(name.replace(".shp", ".cpg"), false); + resolveResource(name.replace(".shp", ".dbf"), false); + resolveResource(name.replace(".shp", ".qix"), false); + resolveResource(name.replace(".shp", ".qmd"), false); + resolveResource(name.replace(".shp", ".prj"), false); + resolveResource(name.replace(".shp", ".shx"), false); + } + + return path; + } + + private String resolveResource(String name, boolean required) { + URL resource = null; + + try { + resource = IOUtils.resolveFileOrResource(name); + } catch (UncheckedIOException e) { + // Nothing to do + } if (resource == null) { // Try to prefix / automatically resource = this.getClass().getResource("/" + name); - if (resource == null) + } + + if (resource == null) { + if (required) throw new IllegalArgumentException("Resource '" + name + "' not found!"); + else + return null; } String baseName = FilenameUtils.getName(resource.getPath()); diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperConfigGroup.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperConfigGroup.java index 6bb420d1292..0914fda1eb7 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperConfigGroup.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperConfigGroup.java @@ -1,5 +1,6 @@ package org.matsim.simwrapper; +import org.matsim.core.config.Config; import org.matsim.core.config.ConfigGroup; import org.matsim.core.config.ReflectiveConfigGroup; @@ -28,6 +29,10 @@ public class SimWrapperConfigGroup extends ReflectiveConfigGroup { @Comment("Set of simple class names or fully qualified class names of dashboards to exclude") public Set exclude = new HashSet<>(); + @Parameter + @Comment("Set of simple class names or fully qualified class names of dashboards to include. Any none included dashboard will be excluded.") + public Set include = new HashSet<>(); + @Parameter @Comment("Sample size of the run, which may be required by certain analysis functions.") public Double sampleSize = 1.0d; @@ -83,6 +88,15 @@ public void addParameterSet(ConfigGroup set) { } } + @Override + protected void checkConsistency(Config config) { + super.checkConsistency(config); + + if (!include.isEmpty() && !exclude.isEmpty()) { + throw new IllegalStateException("Include and exclude option can't be set both."); + } + } + /** * Mode how default dashboards are loaded. */ diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperListener.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperListener.java index 1270c938ee4..b9bd8c261a3 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperListener.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/SimWrapperListener.java @@ -34,8 +34,8 @@ public class SimWrapperListener implements StartupListener, ShutdownListener { @Inject public SimWrapperListener(SimWrapper simWrapper, Set bindings, Config config) { this.simWrapper = simWrapper; - this.bindings = bindings; - this.config = config; + this.bindings = bindings; + this.config = config; } /** @@ -105,6 +105,9 @@ private void addFromProvider(SimWrapperConfigGroup config, Iterable exclude; + @CommandLine.Option(names = "--include", split = ",", description = "Use only the dashboards which classnames match.") + private Set include; + public static void main(String[] args) { new SimWrapperRunner().execute(args); } @@ -58,6 +61,8 @@ public Integer call() throws Exception { if (exclude != null) simWrapperConfigGroup.exclude.addAll(exclude); + if (include != null) + simWrapperConfigGroup.include.addAll(include); SimWrapperListener listener = new SimWrapperListener(SimWrapper.create(config), config); try { diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/ActivityDashboard.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/ActivityDashboard.java new file mode 100644 index 00000000000..5561ea641e8 --- /dev/null +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/ActivityDashboard.java @@ -0,0 +1,185 @@ +package org.matsim.simwrapper.dashboard; + +import org.apache.commons.lang3.StringUtils; +import org.matsim.application.analysis.activity.ActivityCountAnalysis; +import org.matsim.simwrapper.Dashboard; +import org.matsim.simwrapper.Header; +import org.matsim.simwrapper.Layout; +import org.matsim.simwrapper.viz.ColorScheme; +import org.matsim.simwrapper.viz.MapPlot; +import org.matsim.simwrapper.viz.TextBlock; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Dashboard to show activity related statistics aggregated by type and location. + *

+ * Note that {@link #addActivityType(String, List, List, boolean, String)} needs to be called for each activity type. + * There is no default configuration. + */ +public class ActivityDashboard implements Dashboard { + + private static final String ID_COLUMN = "id"; + private static final String REF_JOIN = "id"; + + private final String shpFile; + private final Map activityMapping = new LinkedHashMap<>(); + private final Map refCsvs = new LinkedHashMap<>(); + private final Set countMultipleOccurrencesSet = new HashSet<>(); + private List indicators = new ArrayList<>(); + + public ActivityDashboard(String shpFile) { + this.shpFile = Objects.requireNonNull(shpFile, "Shapefile can not be null!"); + } + + /** + * Convenience method to add an activity type with default configuration. + */ + public ActivityDashboard addActivityType(String name, List activities, List indicators) { + return addActivityType(name, activities, indicators, true, null); + } + + /** + * Add an activity type to the dashboard. + * + * @param name name to show in the dashboard + * @param activities List of activity names to include in this type + * @param indicators List of indicators to show + * @param countMultipleOccurrences Whether multiple occurrences of the same activity for one person should be counted. + * Can be used to count home or workplaces only once. + * @param refCsv Reference CSV file to compare the activities to. Can be null. + */ + public ActivityDashboard addActivityType(String name, List activities, List indicators, + boolean countMultipleOccurrences, @Nullable String refCsv) { + activityMapping.put(name, String.join(",", activities)); + refCsvs.put(name, refCsv); + + if (countMultipleOccurrences) { + countMultipleOccurrencesSet.add(name); + } + + this.indicators = indicators; + return this; + } + + @Override + public void configure(Header header, Layout layout) { + + header.title = "Activities"; + header.description = "Displays the activities by type and location."; + + List args = new ArrayList<>(List.of("--id-column", ID_COLUMN, "--shp", shpFile)); + args.add("--activity-mapping"); + args.add(activityMapping.entrySet().stream() + .map(e -> "%s=%s".formatted(e.getKey(), e.getValue())) + .collect(Collectors.joining(";"))); + + args.add("--single-occurrence"); + if (!countMultipleOccurrencesSet.isEmpty()) { + args.add(String.join(";", countMultipleOccurrencesSet)); + } + + + for (Map.Entry activity : activityMapping.entrySet()) { + + String activityName = StringUtils.capitalize(activity.getKey()); + + layout.row("category_header_" + activity.getKey()) + .el(TextBlock.class, (viz, data) -> { + viz.content = "## **" + activityName + "**"; + viz.backgroundColor = "transparent"; + }); + + for (Indicator ind : Indicator.values()) { + + if (indicators.contains(ind)) { + + Layout.Row row = layout.row(activity.getKey() + "_" + ind.name) + .el(MapPlot.class, (viz, data) -> { + viz.title = "Simulated %s Activities (%s)".formatted(activityName, ind.displayName); + viz.height = 8.; + String shp = data.resource(shpFile); + viz.setShape(shp, ID_COLUMN); + viz.addDataset("transit-trips", data.computeWithPlaceholder(ActivityCountAnalysis.class, "activities_%s_per_region.csv", activity.getKey(), args.toArray(new String[0]))); + viz.display.fill.columnName = ind.name; + viz.display.fill.dataset = "transit-trips"; + viz.display.fill.join = REF_JOIN; + if (ind == Indicator.RELATIVE_DENSITY) { + viz.display.fill.setColorRamp(ColorScheme.RdBu, 11, false, "0.2, 0.25, 0.33, 0.5, 0.67, 1.5, 2.0, 3.0, 4.0, 5.0"); + } + }); + + if (refCsvs.get(activity.getKey()) != null) { + row.el(MapPlot.class, (viz, data) -> { + + viz.title = "Reference %s Activities (%s)".formatted(activityName, ind.displayName); + viz.height = 8.; + + String shp = data.resource(shpFile); + viz.setShape(shp, ID_COLUMN); + + viz.addDataset("transit-trips", data.resource(refCsvs.get(activity.getKey()))); + + viz.display.fill.dataset = "transit-trips"; + viz.display.fill.join = REF_JOIN; + + if (ind == Indicator.RELATIVE_DENSITY) { + viz.display.fill.columnName = "relative_density"; + viz.display.fill.setColorRamp(ColorScheme.RdBu, 11, false, "0.2, 0.25, 0.33, 0.5, 0.67, 1.5, 2.0, 3.0, 4.0, 5.0"); + } else if (ind == Indicator.DENSITY) { + viz.display.fill.columnName = "density"; + } else { + viz.display.fill.columnName = "count"; + } + }); + } + } + } + } + } + + /** + * Metric to show in the dashboard. + */ + public enum Indicator { + COUNTS("count", "Counts"), + DENSITY("density", "Density"), + RELATIVE_DENSITY("relative_density", "Relative Density"); + + private final String name; + private final String displayName; + + Indicator(String name, String displayName) { + this.name = name; + this.displayName = displayName; + } + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/viz/MapPlot.java b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/viz/MapPlot.java index ac675cc6381..96ea10d5d94 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/viz/MapPlot.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/viz/MapPlot.java @@ -10,15 +10,14 @@ */ public final class MapPlot extends Viz { + private final Map datasets = new HashMap<>(); public double[] center; public Double zoom; - public Display display = new Display(); public Double minValue; public Double maxValue; @JsonProperty(required = true) private Object shapes; - private Map datasets = new HashMap<>(); public MapPlot() { super("map"); @@ -77,6 +76,9 @@ public static final class DisplaySettings { @JsonProperty(required = true) public String columnName; + @JsonProperty(required = true) + public String normalize; + @JsonProperty(required = true) public String join; diff --git a/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java b/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java index e3e92fa87a7..a741dfeba78 100644 --- a/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java +++ b/contribs/simwrapper/src/test/java/org/matsim/simwrapper/dashboard/DashboardTests.java @@ -20,11 +20,12 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.List; import java.util.Set; public class DashboardTests { @RegisterExtension - private MatsimTestUtils utils = new MatsimTestUtils(); + private final MatsimTestUtils utils = new MatsimTestUtils(); private void run(Dashboard... dashboards) { @@ -153,4 +154,20 @@ void ptCustom() { Assertions.assertThat(out) .isDirectoryContaining("glob:**pt_pax_volumes.csv.gz"); } + + @Test + void activity() { + ActivityDashboard ad = new ActivityDashboard("kehlheim_shape.shp"); + + ad.addActivityType( + "work", + List.of("work"), + List.of(ActivityDashboard.Indicator.COUNTS, ActivityDashboard.Indicator.RELATIVE_DENSITY, ActivityDashboard.Indicator.DENSITY), true, + "kehlheim_ref.csv" + ); + + run(ad); + } + + } diff --git a/contribs/simwrapper/src/test/resources/kehlheim_ref.csv b/contribs/simwrapper/src/test/resources/kehlheim_ref.csv new file mode 100644 index 00000000000..1b3ba9c3824 --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_ref.csv @@ -0,0 +1,12 @@ +id;count;area;density;relative_density +2;12000.0;1743518.43;0.006882634;1.07723188785 +6;11000.0;350075.92;0.031421756;3.91795945 +4;2000.0;545791.40;0.003664404;0.5735321986 +8;13000.0;2121061.25;0.006129007;0.9592783427 +9;15000.0;3785838.06;0.003962135;0.6201314016 +10;7000.0;5744728.89;0.001218508;0.19071418629999998 +1;2000.0;1981892.09;0.001009137;0.19071418629999998 +3;12000.0;1955593.52;0.006136245;0.9604110622400001 +7;7000.0;942460.23;0.007427369;1.1624907459 +5;1000.0;876651.07;0.001140705;0.1785367932 +0;8000.0;6205676.03;0.001289142;0.2017694383 \ No newline at end of file diff --git a/contribs/simwrapper/src/test/resources/kehlheim_shape.cpg b/contribs/simwrapper/src/test/resources/kehlheim_shape.cpg new file mode 100644 index 00000000000..3ad133c048f --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_shape.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/contribs/simwrapper/src/test/resources/kehlheim_shape.dbf b/contribs/simwrapper/src/test/resources/kehlheim_shape.dbf new file mode 100644 index 0000000000000000000000000000000000000000..d3b37577b5dff9efce8a897c02f1f0018b8ded66 GIT binary patch literal 187 zcmZRs;TGX$U|?`$0Fjs=GX*Z@2V!x-xex}g0vs5@SqjDorU`;+ieQ={nC1wk1%hb_ OXBry7nF>fuLn#13UJ_6M literal 0 HcmV?d00001 diff --git a/contribs/simwrapper/src/test/resources/kehlheim_shape.prj b/contribs/simwrapper/src/test/resources/kehlheim_shape.prj new file mode 100644 index 00000000000..bd846aeb220 --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_shape.prj @@ -0,0 +1 @@ +PROJCS["ETRS_1989_UTM_Zone_32N",GEOGCS["GCS_ETRS_1989",DATUM["D_ETRS_1989",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",9.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/contribs/simwrapper/src/test/resources/kehlheim_shape.qmd b/contribs/simwrapper/src/test/resources/kehlheim_shape.qmd new file mode 100644 index 00000000000..53f5af5ac96 --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_shape.qmd @@ -0,0 +1,44 @@ + + + + + + dataset + + + + + + + + + + + + + + + + + + GEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["World."],BBOX[-90,-180,90,180]],ID["EPSG",4326]] + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + EPSG:7030 + true + + + + + + + + + + + + diff --git a/contribs/simwrapper/src/test/resources/kehlheim_shape.shp b/contribs/simwrapper/src/test/resources/kehlheim_shape.shp new file mode 100644 index 0000000000000000000000000000000000000000..ff0f9b67afd83cba76787f3f2db8f4f606c8669a GIT binary patch literal 4476 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 literal 0 HcmV?d00001 diff --git a/contribs/simwrapper/src/test/resources/kehlheim_shape.shx b/contribs/simwrapper/src/test/resources/kehlheim_shape.shx new file mode 100644 index 0000000000000000000000000000000000000000..1be796e8141d61ef66765e3577cb6dce8dc65f5b GIT binary patch literal 188 zcmZQzQ0HR64&q)gGcd3M<#wH7^?5W;)p66Gw5i@}LL3T*1J=Xa%HCFfcH&0qFoB9R;LsFfcHi0p&Ot7+9Pj@~nJ7z5oLQn-YX( Khstw+