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..3dfadf1a325 --- /dev/null +++ b/contribs/application/src/main/java/org/matsim/application/analysis/activity/ActivityCountAnalysis.java @@ -0,0 +1,163 @@ +package org.matsim.application.analysis.activity; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.geotools.api.feature.Feature; +import org.geotools.api.feature.Property; +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.Option(names = "--id-column", description = "Column to use as ID for the shapefile", required = true) + private String idColumn; + + @CommandLine.Option(names = "--activity-mapping", description = "Map of patterns to merge activity types", split = ";") + private Map activityMapping; + + public static void main(String[] args) { + new ActivityCountAnalysis().execute(args); + } + + @Override + public Integer call() throws Exception { + + HashMap> formattedActivityMapping = 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 = shp.createIndex(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 + Feature feature = index.queryFeature(coord); + + if (feature == null) { + continue; + } + + 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) { + + // 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 (!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("region"); + DoubleColumn activityColumn = DoubleColumn.create("count"); + resultTable.addColumns(regionColumn, activityColumn); + 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("region", region.toString()); + row.setDouble("count", value); + } + resultTable.addColumns(activityColumn.divide(activityColumn.sum()).setName("count_normalized")); + 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/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..07cdbdc43f3 100644 --- a/contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java @@ -1,5 +1,6 @@ package org.matsim.simwrapper; +import com.google.common.io.Resources; import org.apache.commons.io.FilenameUtils; import org.matsim.application.CommandRunner; import org.matsim.application.MATSimAppCommand; @@ -186,6 +187,17 @@ public String resource(String name) { return this.getUnixPath(this.path.getParent().relativize(resolved)); } + public String resources(String... names) { + String first = null; + for (String name : names) { + String resource = resource(name); + if (first == null) + first = resource; + } + + return first; + } + /** * Copies an input file (which can be local or an url) to the output directory. This method is intended to copy input for analysis classes. */ 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..b51a24347f5 --- /dev/null +++ b/contribs/simwrapper/src/main/java/org/matsim/simwrapper/dashboard/ActivityDashboard.java @@ -0,0 +1,36 @@ +package org.matsim.simwrapper.dashboard; + +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.MapPlot; + +public class ActivityDashboard implements Dashboard { + @Override + public void configure(Header header, Layout layout) { + + header.title = "Activity Analysis"; + header.description = "Displays the activities by type and location."; + + layout.row("activites") + .el(MapPlot.class, (viz, data) -> { + viz.title = "Activity Map"; + viz.description = "Activities per region"; + viz.height = 12.0; + viz.center = data.context().getCenter(); + viz.zoom = data.context().mapZoomLevel; + viz.display.fill.dataset = "activities_home"; + + // --shp= + data.resource(fff.shp) + + viz.display.fill.columnName = "count"; + String shp = data.resources("kehlheim_test.shp", "kehlheim_test.shx", "kehlheim_test.dbf", "kehlheim_test.prj"); + viz.setShape(shp); + + + viz.addDataset("activities_home", data.computeWithPlaceholder(ActivityCountAnalysis.class, "activities_%s_per_region.csv", "home", "--id-column=id", "--shp=" + shp)); + viz.addDataset("activities_other", data.computeWithPlaceholder(ActivityCountAnalysis.class, "activities_%s_per_region.csv", "other", "--id-column=id", "--shp=" + shp)); + }); + } +} 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 223b0fdaaec..734d93a911d 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 @@ -179,4 +179,11 @@ void ptCustom() { run(pt); } + + @Test + void activity() { + ActivityDashboard ad = new ActivityDashboard(); + + run(ad); + } } diff --git a/contribs/simwrapper/src/test/resources/kehlheim_test.cpg b/contribs/simwrapper/src/test/resources/kehlheim_test.cpg new file mode 100644 index 00000000000..3ad133c048f --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_test.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/contribs/simwrapper/src/test/resources/kehlheim_test.dbf b/contribs/simwrapper/src/test/resources/kehlheim_test.dbf new file mode 100644 index 00000000000..c13bf61d766 Binary files /dev/null and b/contribs/simwrapper/src/test/resources/kehlheim_test.dbf differ diff --git a/contribs/simwrapper/src/test/resources/kehlheim_test.prj b/contribs/simwrapper/src/test/resources/kehlheim_test.prj new file mode 100644 index 00000000000..bd846aeb220 --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_test.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_test.qmd b/contribs/simwrapper/src/test/resources/kehlheim_test.qmd new file mode 100644 index 00000000000..087bef5cdd2 --- /dev/null +++ b/contribs/simwrapper/src/test/resources/kehlheim_test.qmd @@ -0,0 +1,27 @@ + + + + + + dataset + + + + + + + + + PROJCRS["ETRS89 / UTM zone 32N",BASEGEOGCRS["ETRS89",ENSEMBLE["European Terrestrial Reference System 1989 ensemble",MEMBER["European Terrestrial Reference Frame 1989"],MEMBER["European Terrestrial Reference Frame 1990"],MEMBER["European Terrestrial Reference Frame 1991"],MEMBER["European Terrestrial Reference Frame 1992"],MEMBER["European Terrestrial Reference Frame 1993"],MEMBER["European Terrestrial Reference Frame 1994"],MEMBER["European Terrestrial Reference Frame 1996"],MEMBER["European Terrestrial Reference Frame 1997"],MEMBER["European Terrestrial Reference Frame 2000"],MEMBER["European Terrestrial Reference Frame 2005"],MEMBER["European Terrestrial Reference Frame 2014"],ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[0.1]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4258]],CONVERSION["UTM zone 32N",METHOD["Transverse Mercator",ID["EPSG",9807]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",9,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["Scale factor at natural origin",0.9996,SCALEUNIT["unity",1],ID["EPSG",8805]],PARAMETER["False easting",500000,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Engineering survey, topographic mapping."],AREA["Europe between 6°E and 12°E: Austria; Belgium; Denmark - onshore and offshore; Germany - onshore and offshore; Norway including - onshore and offshore; Spain - offshore."],BBOX[38.76,6,84.33,12]],ID["EPSG",25832]] + +proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs + 2105 + 25832 + EPSG:25832 + ETRS89 / UTM zone 32N + utm + EPSG:7019 + false + + + + diff --git a/contribs/simwrapper/src/test/resources/kehlheim_test.shp b/contribs/simwrapper/src/test/resources/kehlheim_test.shp new file mode 100644 index 00000000000..e9fb03fa9b2 Binary files /dev/null and b/contribs/simwrapper/src/test/resources/kehlheim_test.shp differ diff --git a/contribs/simwrapper/src/test/resources/kehlheim_test.shx b/contribs/simwrapper/src/test/resources/kehlheim_test.shx new file mode 100644 index 00000000000..9612c3d6be5 Binary files /dev/null and b/contribs/simwrapper/src/test/resources/kehlheim_test.shx differ