Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added activity dashboard and activity analysis #3544

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String, String> activityMapping;

public static void main(String[] args) {
new ActivityCountAnalysis().execute(args);
}

@Override
public Integer call() throws Exception {

HashMap<String, Set<String>> formattedActivityMapping = new HashMap<>();

if (this.activityMapping == null) this.activityMapping = new HashMap<>();

for (Map.Entry<String, String> entry : this.activityMapping.entrySet()) {
String pattern = entry.getKey();
String activity = entry.getValue();
Set<String> 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<Object, Object2IntMap<String>> regionActivityCounts = new Object2ObjectOpenHashMap<>();
// stores the activities that have been counted for each person in each region
Object2ObjectOpenHashMap<Object, Set<String>> 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<String, Set<String>> entry : formattedActivityMapping.entrySet()) {
String pattern = entry.getKey();
Set<String> 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<String> 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<String> 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<String> uniqueActivities = new HashSet<>();

for (Object2IntMap<String> 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<Object, Object2IntMap<String>> entry : regionActivityCounts.entrySet()) {
Object region = entry.getKey();
double value = 0;
for (Map.Entry<String, Integer> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions contribs/simwrapper/src/main/java/org/matsim/simwrapper/Data.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,11 @@ void ptCustom() {

run(pt);
}

@Test
void activity() {
ActivityDashboard ad = new ActivityDashboard();

run(ad);
}
}
1 change: 1 addition & 0 deletions contribs/simwrapper/src/test/resources/kehlheim_test.cpg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UTF-8
Binary file not shown.
1 change: 1 addition & 0 deletions contribs/simwrapper/src/test/resources/kehlheim_test.prj
Original file line number Diff line number Diff line change
@@ -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]]
27 changes: 27 additions & 0 deletions contribs/simwrapper/src/test/resources/kehlheim_test.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis version="3.38.3-Grenoble">
<identifier></identifier>
<parentidentifier></parentidentifier>
<language></language>
<type>dataset</type>
<title></title>
<abstract></abstract>
<links/>
<dates/>
<fees></fees>
<encoding></encoding>
<crs>
<spatialrefsys nativeFormat="Wkt">
<wkt>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]]</wkt>
<proj4>+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs</proj4>
<srsid>2105</srsid>
<srid>25832</srid>
<authid>EPSG:25832</authid>
<description>ETRS89 / UTM zone 32N</description>
<projectionacronym>utm</projectionacronym>
<ellipsoidacronym>EPSG:7019</ellipsoidacronym>
<geographicflag>false</geographicflag>
</spatialrefsys>
</crs>
<extent/>
</qgis>
Binary file not shown.
Binary file not shown.