/access-route

A mapping website that calculates routes around accessibility obstacles, written in django and postgis

Primary LanguageHTML

access-route

Introduction

access-route is a routing/navigation website similar to Google Maps, but with a focus on accessibility. Users can search for a start and destination address and the website will display the shortest route that bypasses accessibility obstacles. It is written in Django python with street network data, obstacle data, and elevation data stored in Postgres/PostGIS. Shortest routes are calculated using the Dijkstra algorithm in PgRouting.

The map is drawn using the Mapbox API with data from OpenStreetMap. Routes and markers on the map are drawn with leaflet.js.

Features:

  • Start and end address search, powered by OpenCage API
  • Graph of elevation along route, drawn using d3.js
  • Accessibility obstacles marked on map with clickable pins; links to Google Street View imagery
  • Customizable cost calculation algorithm for routing

Installation/Usage (Easy Way) - Using Vagrant

Start by installing virtualbox (www.virtualbox.org/wiki/Downloads) and vagrant (www.vagrantup.com). Windows users should also install an SSH client as well (or the chance is you already have it if you are using git. Add C:\Program Files (x86)\Git\bin to the PATH. See here and here for more information.) Spend some time setting up and familiarizing yourself with vagrant. Afterwards, in the access-route directory run:

vagrant up

This will create a new Ubuntu Trusty 64 Bit virtual machine and configure it for this project. This step will take a while. When it completes, you can log into a terminal of your new virtual machine by using:

vagrant ssh

You can find the access-route project in the /vagrant directory. To start the app run:

cd /vagrant/routing
python manage.py runserver 0.0.0.0:8000

Then go to http://localhost:8000 on your laptop to see the website!

You can also access the PostgreSQL database running inside of vagrant. For example, on osx you can use an app like Postico to connect to the PostgreSQL database using the credentials:

Host: localhost:15432
User: vagrant
Password: sidewalk
Database: sidewalk

When you are done working be sure the suspend (or halt/destroy) your vagrant virtual machine:

vagrant suspend

When you are ready to start working again just use:

vagrant up
vagrant ssh

Installation/Usage (Hard Way) - On your native machine

Notes

This guide assumes that you have street network data in geojson format, and information about the locations of accessibility features/obstacles in geojson files with a different file for each type of feature. If you have the data a different format, you may have to do some conversions or make adjustments to the instructions below.

Python

Required Python 3.4 packages are listed in requirements.txt and can usually be installed with pip. It is probably best to set this up in a new Virtual Environment.

Database

Due to extensive use of PostGIS functions which don't fit well into Django models, this application performs raw SQL queries. Unfortunately, this means the appropriate database tables must be set up manually.

Basic setup
  1. Install Postgres and PostGIS
  2. Create a new Postgres database named sidewalk
  3. Enable PostGIS in the sidewalk database:
-- Enable PostGIS (includes raster)
CREATE EXTENSION postgis;
-- Enable Topology
CREATE EXTENSION postgis_topology;
-- fuzzy matching needed for Tiger
CREATE EXTENSION fuzzystrmatch;
-- Enable US Tiger Geocoder
CREATE EXTENSION postgis_tiger_geocoder;
  1. Enable PgRouting functions:
-- add pgRouting core functions
CREATE EXTENSION pgrouting;
Add the required tables:

First create a sidewalk schema by typing CREATE SCHEMA sidewalk;

sidewalk_edge stores the street network on which routes will be calculated. For details on how to create and populate this database, scroll down to Importing the street network data.

feature_types stores the types of accessibility features that can be present on the map.

CREATE TABLE sidewalk.feature_types
(
  type_id integer NOT NULL DEFAULT nextval('feature_types_type_id_seq'::regclass),
  type_string character varying(150),
  CONSTRAINT feature_types_pkey PRIMARY KEY (type_id)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE sidewalk.feature_types
  OWNER TO postgres;

After creating the table, you should add, at minimum, the following two entries:

type_id type_string
1 curbcut
2 construction

accessibility_feature stores accessibility features and their locations. After creating it, this table can be populated using a python script. Scroll down to Populating accessibility features for details.

CREATE TABLE sidewalk.accessibility_feature
(
  accessibility_feature_id integer NOT NULL DEFAULT nextval('accessibility_features_feature_id_seq'::regclass),
  feature_geometry geometry(Point,4326),
  feature_type integer,
  lng double precision,
  lat double precision,
  CONSTRAINT accessibility_features_pkey PRIMARY KEY (accessibility_feature_id),
  CONSTRAINT accessibility_feature_feature_type_fkey FOREIGN KEY (feature_type)
      REFERENCES sidewalk.feature_types (type_id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE
);
ALTER TABLE sidewalk.accessibility_feature
  OWNER TO postgres;

sidewalk_edge_accessibility_feature keeps track of which accessibility features belong to which streets. This table can also be populated automatically after creation; scroll down to Populating accessibility features for details.

CREATE TABLE sidewalk.sidewalk_edge_accessibility_feature
(
  sidewalk_edge_accessibility_feature_id integer NOT NULL DEFAULT nextval('sidewalk_edge_accessibility_f_sidewalk_edge_accessibility_f_seq'::regclass),
  sidewalk_edge_id integer,
  accessibility_feature_id integer,
  CONSTRAINT sidewalk_edge_accessibility_feature_pkey PRIMARY KEY (sidewalk_edge_accessibility_feature_id),
  CONSTRAINT sidewalk_edge_accessibility_feature_accessibility_feature_id_fk FOREIGN KEY (accessibility_feature_id)
      REFERENCES sidewalk.accessibility_feature (accessibility_feature_id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
  OIDS=FALSE
);
ALTER TABLE sidewalk.sidewalk_edge_accessibility_feature
  OWNER TO postgres;

The elevation table stores the elevation data used to generate a graph of elevation along a route. I obtained data from the USGS National Map Viewer - if you want to use elevation data from here, download it in IMG format and you can use a python script in this repository to automatically import it into the elevation table. See Importing USGS elevation data for details. If you're using data in a different format, you'll have to figure out a different way to populate this table. It has three columns which are pretty self-explanatory:

  • lat (double precision)
  • long (double precision)
  • elevation (double precision) - in meters
CREATE TABLE sidewalk.elevation
(
  lat double precision NOT NULL,
  "long" double precision NOT NULL,
  elevation double precision,
  CONSTRAINT elevation_pkey PRIMARY KEY (lat, long)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE sidewalk.elevation
  OWNER TO postgres;

CREATE INDEX combined_index
  ON sidewalk.elevation
  USING btree
  (lat, long);

CREATE INDEX lat_index
  ON sidewalk.elevation
  USING btree
  (lat);

CREATE INDEX lng_index
  ON sidewalk.elevation
  USING btree
  (long);
Custom functions

A custom function is used to calculate the cost of traversing a street segment. The function takes into account the length of the street, the number of curb ramps on the street, and the number of "construction" obstacles on the street. Feel free to customize this function by changing how the cost is calculated or by adding additional feature types.

CREATE OR REPLACE FUNCTION sidewalk.calculate_accessible_cost(integer)
  RETURNS double precision AS
$BODY$WITH allcosts
     AS (SELECT num_curbramps AS count,
                CASE
                  WHEN num_curbramps = 0 THEN 50 --If there are no curbramps, add 50 meters to the cost
                  WHEN num_curbramps > 3 THEN -10 --If there are more than 3 curbramps, subtract 10 meters from the cost
                  ELSE 0 --If there are only 1 or 2 curbramps, cost is not affected.
                END AS costcontrib
         FROM   (SELECT Count(*) AS num_curbramps --Count how many curbramps are on this street segment
                 FROM   (SELECT accessibility_feature.accessibility_feature_id,
                                feature_type,
                                sidewalk_edge_id
                         FROM   accessibility_feature
                                INNER JOIN sidewalk_edge_accessibility_feature
                                        ON
                sidewalk_edge_accessibility_feature.accessibility_feature_id
                =
                accessibility_feature.accessibility_feature_id) AS foo
                 WHERE  sidewalk_edge_id = $1
                        AND feature_type = 1) AS curbramps --feature_type corresponds to the feature_id in fature_types
         UNION
         SELECT num_construction AS count,
                CASE
                  WHEN num_construction = 0 THEN -10 --If there is no construction, subtract 10m from the cost
                  WHEN num_construction > 0 THEN num_construction * 10000 --For each construction obstacle, add 10km to the cost (which is so high that the street segment will probably be avoided)
                  ELSE 0
                END AS costcontrib
         FROM   (SELECT Count(*) AS num_construction --Count the number of construction obstacles on the street segment
                 FROM   (SELECT accessibility_feature.accessibility_feature_id,
                                feature_type,
                                sidewalk_edge_id
                         FROM   accessibility_feature
                                INNER JOIN sidewalk_edge_accessibility_feature
                                        ON
                sidewalk_edge_accessibility_feature.accessibility_feature_id
                =
                accessibility_feature.accessibility_feature_id) AS foo
                 WHERE  sidewalk_edge_id = $1
                        AND feature_type = 2) AS construction --feature_type corresponds to the feature_id in feature_types
         UNION
         (SELECT St_length(St_transform(wkb_geometry, 3637)), --Finally, add the length of the segment (in meters) to the cost
                 St_length(St_transform(wkb_geometry, 3637)) as costcontrib
          FROM   sidewalk_edge AS distance_cost
          WHERE  sidewalk_edge_id = $1))
SELECT sum(costcontrib)
FROM   allcosts; $BODY$
  LANGUAGE sql VOLATILE
  COST 100;
ALTER FUNCTION sidewalk.calculate_accessible_cost(integer)
  OWNER TO postgres;
Update database credentials

Finally, open routing/routing/settings.py. In the DATABASES section, replace the database username, password, and host as appropriate.

Create additional tables required by Django
cd routing
python manage.py makemigrations
python manage.py migrate

Importing the street network data

You can import street network data into the database using ogr2ogr. This command was tested using a geojson file as input, but you can probably use ogr2ogr with other data formats as well.

ogr2ogr -f "PostgreSQL" PG:"dbname=sidewalk user=postgres password=sidewalk" "source_data.geojson" -nln sidewalk_edge -append

(Remember to replace the postgres username and password with your own and "source_data.geojson" with the name of your data file, of course.)

After importing, change the name of the first column to sidewalk_edge_id.

Add topology

In order for PgRouting to work, a topology must be created on the sidewalk_edge table. This keeps track of which segments are connected to which other segments.

-- Add "source" and "target" column
ALTER TABLE ways ADD COLUMN "source" integer;
ALTER TABLE ways ADD COLUMN "target" integer;

-- Run topology function
SELECT pgr_createTopology('sidewalk_edge', 0.00001, 'wkb_geometry', 'sidewalk_edge_id');

Populating accessibility data

If you have the locations of accessibility features in geojson files, you can use scripts/ImportFeatures.py to automatically import them. There should be a separate geojson file for each type of feature - for instance, one file containing all the curbcut locations, one file containing all the construction locations, etc. You can create the geojson using geojson.io - just drop some Point features into the map.

Once you have the geojson files, open ImportFeatures.py and replace the information at the top as appropriate. FEATURE_TYPE_ID should correspond to the id of type of feature you are adding (from the feature_types table), and FILENAME should be the path to your geojson file. Also set the correct database username and password in conn_string. Then, simply run the script and the tables accessibility_feature and sidewalk_edge_accessibility_feature will both be populated.

Importing USGS Elevation Data

The instructions below are specific to elevation data in IMG format from nationalmap.gov/viewer. If your elevation data is in a different format, you may have to write your own import script.

You can populate the elevation table using scripts/readtopo.py. After downloading the IMG file, open readtopo.py and replace the information at the top of the file (in the "Customizable Variables" section) as appropriate. Then, run the script, and the table will be populated. This process can take a while.

Finally - Run and test!

Run:

cd routing
python manage.py runserver

Then go to http://localhost:8000 to see the website!