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

watson wang | philly landmark engagement #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Binary file added .DS_Store
Binary file not shown.
128 changes: 72 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,72 @@
# Public Engagement Tool (final project)
## Description

Browser-based applications built with JavaScript serve a multitude of functions beyond the mere presentation and exploration of data; while these functionalities are undeniably crucial, they represent only a fraction of its potential applications. One significant application, particularly within the realm of urban planning and design, is constituent engagement.
In your story map project you created an explanation of some topic that users could explore. In the previous dashboard project, you created a web application that allowed users to sort through data visually displayed. Through either filter checkboxes, search bars, or something else, you displayed different data and made your web tool interactive.
The goal for this project is to go beyond data filtering. You want your web tool to offer **something new** to the user *beyond* a visual display of the data. The user will be able to engage and *add* their input to the web somehow. Let's call this the **engagement component**. The engagement component could be:
- user is able to *add* data (ie add a point to the map)
- user is able to *save notes* about data
- user is able to *manipulate* data (ie change boundaries, move points)
- user is able to choose data to *graph*
- user is able to *run a model* that changes the data (ie click a button that calculates NDVI index)
To ground you in this process, **think of a client or primary user for your web application**. This could be nonprofit looking for funding, a community organizer looking to set up a location, a scientist looking at bird migration patterns. Be creative!
In addition, you will **present a 5-minute presentation on your progress on Dec 6** in class. Your presentation does not need to be very fancy but should include:
- a slide introducing your client and how you thought of them
- a slide on the goal of your web application (what need you wish it to fulfill)
- a tour of your web application on the browser
- if applicable, how you wish to improve on your web application
**If you are not able to be in class on Dec 6, let me know ASAP and you will submit a recorded presentation**
## Requirements
- a map
- interactive data filters (either build on to dashboard project or create a new repository)
- one engagement component
- submit a pull request for your fork of the repository
- presentation on Dec 6
## A note about grading
The above requirements are my strong recommendation for what you should aim to have in your project, but please remember that your grade is not necessarily attached to hitting every point. This project should above all be a demonstration of some of the things you've learned in this course. In this project you should demonstrate that you can empathize with how a user may be engaged to contribute to your website or application. Generally speaking, a recipe for a good demonstration project is one that does fewer things well than one that does a many things simply to check off boxes.
## Example projects
Examples in real life:
- Participatory budgeting _à la_ [Decidim](https://decidim.org/) or [Shareabouts](https://pbcambridgefy25.poepublic.com/)
- Streetscape design _à la_ [Streetmix](https://streetmix.net/)
- Redistricting input _à la_ [District Builder](https://www.districtbuilder.org/)
- City budget reallocation
- Transportation planning _à la_ [Remix](https://www.remix.com/)

Examples from past classes:
- adding data/save notes
- Philadelphia Birding Explorer [link](https://henryfeinstein.github.io/final-project-MUSA611/)
- Historic Seattle Philippino Town [link](https://myronbanez.github.io/final-project/site/)
- manipulating data
...
- graph data
...
- run a model
...
# Philadelphia "Trail Waze" App

See the live version at https://musa-611-spring-2022.github.io/trail_waze/frontend/

## Code Documentation

* Client-side [annotated code](https://musa-611-spring-2022.github.io/trail_waze/docs/frontend/js/issue_reporter.html)
* Server-side [annotated code](https://musa-611-spring-2022.github.io/trail_waze/docs/backend/server.html)

## Running locally (for debugging and development)

1. **Install the requirements**

This project uses the following Node JS packages:

* `express`
* `knex`
* `cors`
* `better-sqlite3`

All of these packages are listed in the `package.json` file and thus can be
installed using the Node Package Manager (NPM) by running `npm install`.

2. **Initialize the backend database**

In the Firebase console, create a project, and create a new app for the Web within it.
Don't worry about setting up hosting for the app right now.

Copy the `import` and `firebaseConfig` code after creating the app.
From the _backend/_ folder, run the `init_database.sh` script. This file
will create a SQLite3 database file named `db.sqlite3` inside of the folder,
and will create a table in that database named `trail_issue`.

> Note that the `init_database.sh` script _should_ work if you have Python3
> installed _and_ you are on a Mac or Linux computer. **If you are on Windows**
> then it may not work. Instead, you might be able to copy and run the
> following on the command line while in the _backend/_ folder:
>
> ```bash
> sqlite3 db.sqlite3 "
> CREATE TABLE IF NOT EXISTS trail_issue (
> id INTEGER PRIMARY KEY AUTOINCREMENT,
> category TEXT,
> encountered_at TEXT,
> latitude REAL,
> longitude REAL,
> details TEXT,
> trail_id INTEGER,
> trail_label TEXT,
> created_at TEXT DEFAULT (datetime('now', 'localtime'))
> );
> "
> ```

3. **Start the backend server**

From within the _backend/_ folder, run the following:

```bash
npx nodemon server.js
```

That command should start a new server at http://localhost:3000.

> Note that we could just start the server with `node server.js`, but using
> the `nodemon` tool makes it so that the server will restart whenever we
> make a change to the code in `server.js`. This can be extremely useful
> while you're doing development.

4. **Visit the app**

At this point you should be able to open the _frontend/index.html_ file in your browser by copying the full path to that file and pasting it into your browser's URL bar. You do _not_ need to start a local file server to view the app.
Binary file added backend/db.sqlite3
Binary file not shown.
16 changes: 16 additions & 0 deletions backend/init_database.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash

# Create a new database if one doesn't exist
sqlite3 db.sqlite3 "
CREATE TABLE IF NOT EXISTS trail_issue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT,
encountered_at TEXT,
latitude REAL,
longitude REAL,
details TEXT,
trail_id INTEGER,
trail_label TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
);
"
11 changes: 11 additions & 0 deletions backend/init_pg_database.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS trail_issue (
id SERIAL PRIMARY KEY,
category TEXT,
encountered_at TEXT,
latitude REAL,
longitude REAL,
details TEXT,
trail_id INTEGER,
trail_label TEXT,
created_at TEXT DEFAULT (to_char(now()::timestamp at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'))
);
159 changes: 159 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Read/Write API for "Trail Waze" App
// ===================================
//
// This API is implemented using the Express JS micro-framework, and the Knex JS
// SQL-generating library. It is a REST-style API and supports two primary
// operations: (1) retrieving a list of recent issues reported (within the last
// 30 days), and (2) submitting a new issue.
//
// (note that _Cross-Origin Resource Sharing_, or _CORS_, is not something you
// have to be very familiar with, except to know that it is a security feature
// of HTTP. Suffice to say that including `app.use(cors())` is something you'll
// often have to do. https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).

const express = require('express');
const knex = require('knex');
const cors = require('cors');

const app = express();
app.use(express.json());
app.use(cors());

let knexOptions;

if (process.env.DATABASE_URL) {
knexOptions = {
client: 'pg',
connection: {
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false }
}
};
} else {
knexOptions = {
client: 'better-sqlite3',
connection: {
filename: './db.sqlite3'
}
};
}

const db = knex(knexOptions);


// Serialization and Deserialization
// ---------------------------------
//
// HTTP APIs that exchange data with a client need to be able to **serialize**
// and **deserialize** data.

// * **Serialization** is the process of converting data that comes from, say, a
// database into a format that's ready to be sent to the client. In this case,
// we're creating a `recordToFeature` function to convert a database record as
// we would get from Knex into a GeoJSON feature. Sometimes "serializing"
// will be referred to as "rendering".

const recordToFeature = function (record) {
const geometry = {
type: 'Point',
coordinates: [record.longitude, record.latitude],
};
const properties = { ...record }; // This line makes a copy of the record.
delete properties.latitude;
delete properties.longitude;

return {
type: 'Feature',
geometry,
properties,
};
};

// * **Deserialization** is the process of converting data that comes from a
// client into something useable be your system. In this case, we're creating
// a `featureToRecord` function to convert a GeoJSON feature that we get from
// the client into a record that we can write to the database. Sometimes
// "deserializing" is referred to as "parsing".

const featureToRecord = function (feature) {
const coords = feature.geometry.coordinates;
const record = { ...feature.properties }; // This makes a copy of the feature properties.
[record.longitude, record.latitude] = coords;

if (feature.properties.created_at) {
record.created_at = (new Date(feature.properties.created_at)).toISOString();
}

if (feature.properties.encountered_at) {
record.encountered_at = (new Date(feature.properties.encountered_at)).toISOString();
}

return record;
};


// Routing Functions
// -----------------
//
// This API exposes (i.e., makes available) one route that can be used with two
// different methods. It is common in REST-style APIs to have a single URL do
// different things depending on the HTTP method used in the client's request.
//
// A REST API will determine the paths that it exposes by the "resources" that
// the API needs to represent. In this API, the main type of resource we're
// representing is a "trail issue", and we want to allow API clients to (1) get
// a list of trail issues, and (2) create a new trail issue. For this we'll
// expose a route at `/trail_issues/`, and use the **GET** and **POST** HTTP
// methods, respectively.
//
// Think of `/trail_issues/` as a resource that represents the collection of all
// trail issues in the system (for our case, we're limiting ourselves to
// returning issues that were reported within the last 30 days, assuming that
// older issues have likely been addressed). Sending a GET request for that
// resource is like asking the API to read those issues, and sending a POST
// request is asking the API to add a new issue to the collection.

app.get('/trail_issues/', (req, res) => {
const currentDateTime = new Date();
const currentTimestamp = currentDateTime.getTime();

const millisecsPerMonth = 30 * 24 * 60 * 60 * 1000;
const oneMonthAgo = new Date(currentTimestamp - millisecsPerMonth); // Calculate 30 days ago.

console.log(`Retrieving issues created after ${oneMonthAgo}`);
db.select().from('trail_issue').where('created_at', '>', oneMonthAgo.toISOString())
.then(records => {
res.json({
type: 'FeatureCollection',
features: records.map(recordToFeature), // Serialize the records.
});
});
});

app.post('/trail_issues/', (req, res) => {
const currentDateTime = new Date();
const newRecord = featureToRecord(req.body); // Deserialize the request data.
newRecord.created_at = currentDateTime.toISOString();

db.insert(newRecord).into('trail_issue').returning('*')
.then(insertedRecords => {
const newFeature = recordToFeature(insertedRecords[0]);
res.json(newFeature);
});
});


// Running the Server
// ------------------
//
// If there is an environment variable named `PORT` then we will run the server
// on the port number specified by that environment variable. Otherwise, we will
// use a default (arbitrary) port 3000. This pattern of looking for and using an
// environment variable if it's available will allow us flexibility when we
// deploy this server to a cloud hosting platform.

const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});

Binary file added docs/.DS_Store
Binary file not shown.
Loading