Skip to content

Commit

Permalink
Merge pull request #807 from azavea/feature/ked/add-default-speed-to-…
Browse files Browse the repository at this point in the history
…analysis-job

Add default speed to AnalysisJob model and display on the frontend
  • Loading branch information
kevinearldenny authored Nov 17, 2020
2 parents 2e5182a + 21fe1b9 commit 9396503
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
#### Added
- S3 caching of Census data files
- Upgrade Django (2.2.10 -> 2.2.16) and psycopg2 (2.8.4 -> 2.8.5)
- Add default speed limit for neighborhood to AnalysisJob model and display it on frontend

## [0.13.0] - 2020-02-14

Expand Down
21 changes: 21 additions & 0 deletions src/analysis/import/import_osm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,27 @@ else
echo "The city residential default speed is ${CITY_DEFAULT}."
fi

# Save default speed limit to a table for export later
psql -h $NB_POSTGRESQL_HOST -U $NB_POSTGRESQL_USER -d $NB_POSTGRESQL_DB \
-c "CREATE TABLE IF NOT EXISTS \"residential_speed_limit\" (
state_fips_code char(2),
city_fips_code char(7),
state_speed smallint,
city_speed smallint
);"
psql -h $NB_POSTGRESQL_HOST -U $NB_POSTGRESQL_USER -d $NB_POSTGRESQL_DB \
-c "INSERT INTO \"residential_speed_limit\" (
state_fips_code,
city_fips_code,
state_speed,
city_speed
) VALUES (
${PFB_STATE_FIPS},
${PFB_CITY_FIPS},
${STATE_DEFAULT},
${CITY_DEFAULT}
);"

rm -rf "${SPEED_TEMPDIR}"
echo "DONE: Importing city default residential speed"

Expand Down
5 changes: 5 additions & 0 deletions src/analysis/scripts/export_connectivity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ then
# Send overall_scores to Django app
update_overall_scores "${OUTPUT_DIR}/neighborhood_overall_scores.csv"

# Export residential_speed_limit as CSV
ec_export_table_csv "${OUTPUT_DIR}" "residential_speed_limit"
# Send residential_speed_limit to Django app
update_residential_speed_limit "${OUTPUT_DIR}/residential_speed_limit.csv"

if [ -n "${AWS_STORAGE_BUCKET_NAME}" ] && [ -n "${PFB_S3_RESULTS_PATH}" ]
then
sync # Probably superfluous, but the s3 command said "file changed while reading" once
Expand Down
9 changes: 9 additions & 0 deletions src/analysis/scripts/utils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ function update_overall_scores() {
fi
}

function update_residential_speed_limit() {
# Usage:
# update_residential_speed_limit RESIDENTIAL_SPEED_LIMIT_CSV
if [ -n "${PFB_JOB_ID}" ];
then
/opt/pfb/django/manage.py load_residential_speed_limit "${PFB_JOB_ID}" "$@"
fi
}

function set_job_attr() {
# Usage:
# update_job_attr ATTRIBUTE VALUE
Expand Down
3 changes: 3 additions & 0 deletions src/angularjs/src/app/components/map/map.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
position: 'bottomright',
colors: ['#009fdf', '#ff3300'],
labels: ['Low Stress', 'High Stress']
},
speedLimit: {
position: 'bottomright'
}
}
};
Expand Down
4 changes: 4 additions & 0 deletions src/angularjs/src/app/components/map/map.directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
function MapController($element) {
var ctl = this;
ctl.map = null;
ctl.speedLimit = null;
ctl.mapHTMLElement = $element[0];
ctl.pfbMapOptions = ctl.pfbMapOptions || {};

Expand Down Expand Up @@ -43,6 +44,9 @@
ctl.map.addLayer(ctl.baseLayer);
}
}
if (changes.speedLimit) {
ctl.speedLimit = changes.speedLimit
}
};
}

Expand Down
6 changes: 4 additions & 2 deletions src/angularjs/src/app/components/places.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@
dfd.resolve({});
return dfd.promise;
}

place.results = results;
place.scores = results.overall_scores;

_.each(results.overall_scores, function (scores, key) {
scores.score_normalized = (key === 'population_total' ?
scores.score_original : scores.score_normalized)
});

place.scores.default_speed_limit = {
score_normalized: results.residential_speed_limit
};
dfd.resolve(place);
});
});
Expand Down
36 changes: 36 additions & 0 deletions src/angularjs/src/app/leaflet/map-speed-limit-legend.control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
(function() {

// Custom private Leaflet control implements a simple legend for all map layers
L.Control.SpeedLegend = L.Control.extend({
initialize: function (options) {
L.Util.setOptions(this, options);
this.speedLimit = options.speedLimit;
if (this.speedLimit <= 25) {
this.stressLevel = 'low'
} else if (this.speedLimit <= 30) {
this.stressLevel = 'med.'
} else {
this.stressLevel = 'high'
}
},
onAdd: function () {
var div = L.DomUtil.create('div', 'leaflet-control-layers pfb-speed-limit-legend');
var kphLimit = Math.round(this.speedLimit * 1.609);
div.innerHTML += '<h6>Residential Speed Limit</h6>';
div.innerHTML += '<div class="speed-limit-inner">' +
'<div class="speed-limit-block"><div class="speed-limit">' + this.speedLimit + '</div><div>mph</div></div>' +
'<div class="speed-limit-block"><div class="speed-limit">' + kphLimit + '</div><div>km/h</div></div>' +
'</div>';
div.innerHTML += '<div style="margin: auto; background: navy;"><div class="speed-stress">Stress impact: ' + this.stressLevel + '</div></div>';
return div;
},
stressLevel: function () {

}
});
if (!L.control.speedLegend) {
L.control.speedLegend = function (opts) {
return new L.Control.SpeedLegend(opts);
}
}
})();
11 changes: 9 additions & 2 deletions src/angularjs/src/app/places/detail/place-map.directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
if (!layers) {
return;
}

var satelliteLayer = L.tileLayer(MapConfig.baseLayers.Satellite.url, {
attribution: MapConfig.baseLayers.Satellite.attribution,
maxZoom: MapConfig.conusMaxZoom
Expand All @@ -97,6 +96,11 @@
$window.print();
}).addTo(ctl.map);
}
if (ctl.pfbPlaceMapSpeedLimit && !ctl.speedLimitLegend) {
var speedLimitLegendOptions = MapConfig.legends["speedLimit"];
speedLimitLegendOptions.speedLimit = ctl.pfbPlaceMapSpeedLimit;
ctl.speedLimitLegend = L.control.speedLegend(speedLimitLegendOptions).addTo(ctl.map);
}

_.map(layers.tileLayers, function(layerObj) {
// Get desired label
Expand Down Expand Up @@ -149,6 +153,8 @@
});
});



function onEachFeature(feature, layer) {
// TODO: Style marker and popup
layer.on({
Expand Down Expand Up @@ -202,7 +208,8 @@
restrict: 'E',
scope: {
pfbPlaceMapLayers: '<',
pfbPlaceMapUuid: '<'
pfbPlaceMapUuid: '<',
pfbPlaceMapSpeedLimit: '<'
},
controller: 'PlaceMapController',
controllerAs: 'ctl',
Expand Down
1 change: 1 addition & 0 deletions src/angularjs/src/app/places/detail/place-map.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!-- keep the .map class. It's important for sizing. -->
<div class="map map-below" id="map-{{::$id}}"
pfb-map
pfb-place-map-speed-limit="ctl.pfbPlaceMapSpeedLimit"
pfb-place-map-layers="ctl.mapLayers"
pfb-place-map-uuid="ctl.pfbPlaceMapUuid"
pfb-map-zoom="12"
Expand Down
6 changes: 4 additions & 2 deletions src/angularjs/src/app/places/detail/places-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ <h4 class="sidebar-title">{{placeDetail.place.label_suffix}}</h4>
ng-class="m.subscoreClass">
{{ ::m.label }}
<div class="tooltip" data-title="{{ ::m.description }}"><i class="icon-info-circled"></i></div>
<span class="network-score small">{{ ::placeDetail.scores[m.name].score_normalized | number:0 }}</span>
<span class="network-score small" ng-if="m.name!=='default_speed_limit'">{{ ::placeDetail.scores[m.name].score_normalized | number:0 }}</span>
<span class="network-score small" ng-if="m.name==='default_speed_limit'">{{ ::placeDetail.scores[m.name].score_normalized | number:0 }} mph</span>
</li>
</ul>
</div>
Expand All @@ -78,7 +79,8 @@ <h4 class="sidebar-title">{{placeDetail.place.label_suffix}}</h4>
<div class="preview-map">
<pfb-place-map
pfb-place-map-layers="placeDetail.mapLayers"
pfb-place-map-uuid="placeDetail.place.uuid">
pfb-place-map-uuid="placeDetail.place.uuid"
pfb-place-map-speed-limit="placeDetail.scores.default_speed_limit.score_normalized">
</pfb-place-map>
</div>
<!-- Map -->
32 changes: 32 additions & 0 deletions src/angularjs/src/styles/components/_map.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,35 @@
opacity: 0.7;
}
}

.pfb-speed-limit-legend {
width: 210px;
color: #555;
text-align: center;

h6 {
margin-top: 0;
padding-top: 0.3rem;
background: lightgray;
}
.speed-limit-inner {
margin: auto;
width: 140px;
}

.speed-limit-block {
display: inline-block;
margin: 0 15px 0 15px;
}

.speed-limit {
font-weight: 700;
font-size: 24pt;
}
.speed-stress {
font-weight: 500;
font-size: 1.4rem;
color: white;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import csv

from django.core.management.base import BaseCommand

from pfb_analysis.models import AnalysisJob


def load_speed_limit(job, csv_filename):
with open(csv_filename, 'r') as csv_file:
reader = csv.DictReader(csv_file)
row = next(reader)
# Take the city value if there is one, or fall back to the state value.
# In both cases check for truthiness, since a missing value will come back as zero
if row.get("city_speed"):
job.default_speed_limit = row["city_speed"]
job.speed_limit_src = AnalysisJob.SpeedLimitSource.CITY
elif row.get("state_speed"):
job.default_speed_limit = row["state_speed"]
job.speed_limit_src = AnalysisJob.SpeedLimitSource.STATE
job.save()


class Command(BaseCommand):
help = """ Load 'residential_speed_limit.csv' produced by the analysis into
AnalysisJob.default_speed_limit
Expected CSV format: one line, with state and city FIPS codes and speeds.
The city code and speed can be blank.
state_fips_code,city_fips_code,state_speed,city_speed
24,2404000,30,25
Saves the city speed limit if present, otherwise the state limit.
"""

def add_arguments(self, parser):
parser.add_argument('job_uuid', type=str)
parser.add_argument('csv_file', type=str,
help='Absolute path to residential speed limit csv to load')

def handle(self, *args, **options):
job_uuid = options['job_uuid']
csv_filename = options['csv_file']

try:
job = AnalysisJob.objects.get(pk=job_uuid)
except (AnalysisJob.DoesNotExist):
print('WARNING: Tried to set default_speed_limit for invalid job {} '
'from file {}'.format(job_uuid, csv_filename))
raise
load_speed_limit(job, csv_filename)
self.stdout.write('{}: Loaded default_speed_limit from {}'.format(job, csv_filename))
23 changes: 23 additions & 0 deletions src/django/pfb_analysis/migrations/0041_auto_20201006_2320.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2.10 on 2020-10-06 23:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('pfb_analysis', '0040_neighborhood_analysis_job_ondelete'),
]

operations = [
migrations.AddField(
model_name='analysisjob',
name='default_speed_limit',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='analysisjob',
name='speed_limit_src',
field=models.CharField(blank=True, choices=[('State', 'State'), ('City', 'City')], max_length=20, null=True),
),
]
20 changes: 18 additions & 2 deletions src/django/pfb_analysis/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ def get_queryset(self):
('overall_score', 'score_normalized')))
.annotate(population_total=ObjectAtPath('overall_scores',
('population_total', 'score_original'),
output_field=models.PositiveIntegerField())))
output_field=models.PositiveIntegerField()))
)
return qs


Expand Down Expand Up @@ -531,6 +532,15 @@ class Status:
(ERROR, 'Error',),
)

class SpeedLimitSource:
STATE = 'State'
CITY = 'City'

CHOICES = (
(STATE, 'State'),
(CITY, 'City')
)

batch_job_id = models.CharField(max_length=256, blank=True, null=True)
neighborhood = models.ForeignKey(Neighborhood,
related_name='analysis_jobs',
Expand All @@ -549,7 +559,13 @@ class Status:
start_time = models.DateTimeField(null=True, blank=True)
final_runtime = models.PositiveIntegerField(default=0)
status = models.CharField(choices=Status.CHOICES, max_length=12, default=Status.CREATED)

default_speed_limit = models.PositiveIntegerField(blank=True, null=True)
speed_limit_src = models.CharField(
choices=SpeedLimitSource.CHOICES,
max_length=20,
blank=True,
null=True
)
objects = AnalysisJobManager()

@property
Expand Down
1 change: 1 addition & 0 deletions src/django/pfb_analysis/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ class Meta:
'analysis_job_definition', '_analysis_job_name',)
read_only_fields = ('uuid', 'createdAt', 'modifiedAt', 'createdBy', 'modifiedBy',
'batch_job_id', 'batch', 'census_block_count', 'final_runtime',
'default_speed_limit',
'local_upload_task')


Expand Down
Loading

0 comments on commit 9396503

Please sign in to comment.