Skip to content

Commit

Permalink
Add activities layer
Browse files Browse the repository at this point in the history
  • Loading branch information
sebhoerl committed Jun 10, 2020
1 parent 826eaaf commit f33dd7e
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
data/*
__pycache__

.DS_Store
node_modules
Expand Down
35 changes: 35 additions & 0 deletions backend/matsim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import gzip
import xml.sax

class PopulationReader(xml.sax.handler.ContentHandler):
def __init__(self):
self.population = []

self.person = None
self.process_activities = False

def startElement(self, name, attributes):
if name == "person":
self.person = { "id": attributes["id"], "activities": [] }
self.process_activities = False

elif name == "plan":
self.process_activities = attributes["selected"] == "yes"

elif name == "activity":
self.person["activities"].append({
"purpose": attributes["type"],
"x": float(attributes["x"]),
"y": float(attributes["y"])
})

def endElement(self, name):
if name == "person":
self.population.append(self.person)
self.person = None

def read_population(path):
with gzip.open(path) as f:
reader = PopulationReader()
xml.sax.parse(f, reader)
return reader.population
30 changes: 30 additions & 0 deletions backend/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from flask_restful import Resource, Api
from flask_cors import CORS
from flask import request
import matsim

app = Flask(__name__)
CORS(app)
Expand Down Expand Up @@ -38,6 +39,8 @@
df_services = df_services[["departure_time", "pickup_time", "origin_x", "origin_y"]]
df_services["id"] = np.arange(len(df_services))

population = matsim.read_population("../data/output_plans.xml.gz")

def gini(x):
"""Compute Gini coefficient of array of values"""
diffsum = 0
Expand All @@ -49,6 +52,31 @@ class RequestsLayer(Resource):
def get(self):
return json.loads(df_services.to_json(orient = "records"))

class ActivitiesLayer(Resource):
def get(self, person_id):
for person in population:
if person["id"] == person_id:
return person["activities"]

class PersonsLayer(Resource):
def get(self):
persons = []

for person in population:
for activity in person["activities"]:
if activity["purpose"] == "home":
distance = np.sqrt((activity["x"] - center.x)**2 + (activity["y"] - center.y)**2)

if distance < radius:
persons.append(dict(
id = person["id"],
x = activity["x"],
y = activity["y"]
))
break

return persons

class PopulationLayer(Resource):
def get(self, attribute, metric):
if not attribute in ("age", "income"):
Expand Down Expand Up @@ -92,6 +120,8 @@ def transform(iris_id, iris_name, population, shape):

api.add_resource(Network, '/network')
api.add_resource(RequestsLayer, '/requests')
api.add_resource(PersonsLayer, '/persons')
api.add_resource(ActivitiesLayer, '/activities/<string:person_id>')
api.add_resource(PopulationLayer, '/population/<string:attribute>/<string:metric>')

if __name__ == '__main__':
Expand Down
33 changes: 28 additions & 5 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@
<b-navbar toggleable="lg" type="dark" variant="info" style="position: absolute; z-index: 100; width: 100%; margin-left:-1em;">
<b-navbar-brand href="#">eqarun</b-navbar-brand>
<b-navbar-nav>
<b-nav-item href="#">Runs</b-nav-item>
<b-nav-item href="#">Visualization</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-row align-v="stretch" style="height: 100%;">
<b-col id="panel" cols="3" style="padding-top: 60px;">
<ZoneLayerPanel :layerState="layerState" />
<RequestsLayerPanel :layerState="requestsLayerState" />
<RequestsLayerPanel :layerState="requestsLayerState" v-if="false" />
<ActivitiesLayerPanel :layerState="activitiesLayerState" />
</b-col>
<b-col>
<svg id="map" v-on:wheel="onScale" v-on:mousedown="onMouseDown" v-on:mouseup="onMouseUp" v-on:mouseover="onMouseMove" >
<ZoneLayer :layerState="layerState" />
<RequestsLayer :layerState="requestsLayerState" />
<RequestsLayer :layerState="requestsLayerState" v-if="false" />
<ActivitiesLayer :layerState="activitiesLayerState" />
</svg>
</b-col>
</b-row>
Expand All @@ -31,13 +32,16 @@ import ZoneLayer from "./components/ZoneLayer.vue"
import RequestsLayerPanel from "./components/RequestsLayerPanel.vue"
import RequestsLayer from "./components/RequestsLayer.vue"
import ActivitiesLayerPanel from "./components/ActivitiesLayerPanel.vue"
import ActivitiesLayer from "./components/ActivitiesLayer.vue"
import * as axios from "axios";
import * as _ from "lodash";
export default {
name: 'App',
components: {
ZoneLayerPanel, ZoneLayer, RequestsLayerPanel, RequestsLayer
ZoneLayerPanel, ZoneLayer, RequestsLayerPanel, RequestsLayer, ActivitiesLayerPanel, ActivitiesLayer
},
data() {
var layerState = Vue.observable({
Expand All @@ -57,15 +61,22 @@ export default {
scale: 0.028, offset: [0, 0]
})
var activitiesLayerState = Vue.observable({
loading: false,
persons: [], selectedPerson: undefined,
scale: 0.028, offset: [0, 0]
})
return {
layerState: layerState, requestsLayerState: requestsLayerState,
layerState: layerState, requestsLayerState: requestsLayerState, activitiesLayerState: activitiesLayerState,
scale: 0.028, offset: [0, 0],
mouseDownLocation: undefined
};
},
mounted() {
this.load();
this.loadRequests();
this.loadActivities();
},
methods: {
load() {
Expand All @@ -90,13 +101,24 @@ export default {
this.requestsLayerState.loading = false;
});
},
loadActivities() {
this.activitiesLayerState.loading = true;
var url = window.location.protocol + "//" + window.location.hostname + ":5000";
axios.get(
url + "/persons").then((response) => {
this.activitiesLayerState.persons = response.data;
this.activitiesLayerState.loading = false;
});
},
onScale(event) {
this.scale -= 1e-3 * event.deltaY;
this.updateScale();
},
updateScale: _.debounce(function() {
this.layerState.scale = this.scale;
this.requestsLayerState.scale = this.scale;
this.activitiesLayerState.scale = this.scale;
}, 100),
onMouseDown(event) {
this.mouseDownLocation = [event.clientX, event.clientY];
Expand All @@ -113,6 +135,7 @@ export default {
updateOffset: _.debounce(function() {
this.layerState.offset = this.offset;
this.requestsLayerState.offset = this.offset;
this.activitiesLayerState.offset = this.offset;
}, 100),
},
watch: {
Expand Down
133 changes: 133 additions & 0 deletions src/components/ActivitiesLayer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<template>
<g class="layer">
<circle
v-for="person in selectedPersons"
v-bind:key="person.id"
v-bind:cx="person.x"
v-bind:cy="person.y"
r="4"
stroke="white" stroke-width="1" fill="rgba(255, 0, 0, 0.1)"
v-on:mouseenter="selectPerson(person)"
/>
<circle
v-if="layerState.selectedPerson"
v-bind:cx="layerState.selectedPerson.x"
v-bind:cy="layerState.selectedPerson.y"
r="8"
stroke="white" stroke-width="1" fill="rgba(255, 0, 0, 1.0)"
/>
<g v-if="selectedActivities">
<circle
v-for="point in selectedActivities.points"
v-bind:key="point.index"
v-bind:cx="point.x"
v-bind:cy="point.y"
r="4"
stroke="white" stroke-width="1" fill="rgb(255, 0, 0)"
/>
</g>
<path
v-if="selectedActivities"
v-bind:d="selectedActivities.geometry"
stroke="red" stroke-width="2" fill="none" />
</g>
</template>

<script>
// import * as d3 from "d3";
import * as _ from "lodash";
import * as axios from "axios";
import * as d3 from "d3";
export default {
name: "ActivitiesLayer",
data() {
return {
width: 0, height: 0,
selectedPersons: [],
selectedActivities: []
};
},
props: ["layerState"],
watch: {
"layerState.scale": _.debounce(function() { this.redraw(); }, 100),
"layerState.persons": _.debounce(function() {
var self = this;
this.layerState.persons.forEach(function(person) {
if (person.id == "8902471") {
self.selectPerson(person);
}
});
this.redraw();
}, 100),
"layerState.offset": _.debounce(function() { this.redraw(); }, 100),
},
methods: {
redraw() {
var scale = this.layerState.scale;
var width = this.width;
var height = this.height;
var offset = this.layerState.offset;
var projection = function(x, y) {
return [
(x - 651791.0) * scale + width * 0.5 + offset[0],
(y - 6862293.0) * scale + height * 0.5 + offset[1]
];
}
this.selectedPersons = this.layerState.persons.map((item) => {
var person = { id: item.id };
var coordinates = projection(item.x, item.y);
person["x"] = coordinates[0];
person["y"] = coordinates[1];
return person;
});
},
onResize() {
this.width = this.$el.parentNode.clientWidth;
this.height = this.$el.parentNode.clientHeight;
this.redraw();
},
selectPerson: _.debounce(function(person) {
this.layerState.selectedPerson = person;
var url = window.location.protocol + "//" + window.location.hostname + ":5000";
axios.get(
url + "/activities/" + person.id).then((response) => {
var scale = this.layerState.scale;
var width = this.width;
var height = this.height;
var offset = this.layerState.offset;
var line = d3.line()
.x(function(d) { return (d.x - 651791.0) * scale + width * 0.5 + offset[0]; })
.y(function(d) { return (d.y - 6862293.0) * scale + height * 0.5 + offset[1]; });
var points = response.data.map(function(item) {
return {
x: (item.x - 651791.0) * scale + width * 0.5 + offset[0],
y: (item.y - 6862293.0) * scale + height * 0.5 + offset[1],
};
});
var selectedActivities = response.data;
selectedActivities.geometry = line(selectedActivities);
selectedActivities.points = points;
this.selectedActivities = selectedActivities;
});
}, 100)
},
mounted() {
this.onResize();
window.addEventListener('resize', this.onResize);
}
}
</script>

<style scoped>
</style>
26 changes: 26 additions & 0 deletions src/components/ActivitiesLayerPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<div>
<div class="panel-title">
Activities
<b-spinner v-if="layerState.loading" small label="Spinning"></b-spinner>
</div>
<div v-if="layerState.selectedPerson">
Person: {{ layerState.selectedPerson.id }}
</div>
<b-form>
<!--b-form-group label="Time" label-cols="3">
<b-form-input id="range-1" v-model="layerState.time" type="range" min="0" max="86400" :disabled="layerState.loading"></b-form-input>
<span>
{{ (Math.round(layerState.time / 3600) + "").padStart(2, "0") }}:{{ (Math.round((layerState.time % 3600) / 60) + "").padStart(2, "0") }}:{{ (Math.round(layerState.time % 60) + "").padStart(2, "0") }}
</span>
</b-form-group-->
</b-form>
</div>
</template>

<script>
export default {
name: "ActivitiesLayerPanel",
props: ["layerState"]
}
</script>

0 comments on commit f33dd7e

Please sign in to comment.