Skip to content

Commit

Permalink
Merge pull request #22 from sdl60660/global
Browse files Browse the repository at this point in the history
Global Paths
  • Loading branch information
sdl60660 authored Jan 5, 2022
2 parents 89db13b + 76695bc commit 9c23f54
Show file tree
Hide file tree
Showing 52 changed files with 4,236 additions and 2,702,554 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/node_modules/
/name_server/node_modules
/name_server/.env
/public/build/

.DS_Store
Expand All @@ -20,4 +22,5 @@
# Original shapefile can be found here: https://hub.arcgis.com/datasets/esri::usa-detailed-water-bodies?geometry=167.943%2C22.299%2C-35.172%2C47.182
# /data_processing/data/water_bodies.geojson

name_server
package-lock.json
*/package-lock.json
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: npm start --prefix name_server
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# River Runner

This uses USGS [NHDPlus data](https://www.usgs.gov/core-science-systems/ngp/national-hydrography/nhdplus-high-resolution) and their [NLDI API](https://waterdata.usgs.gov/blog/nldi-intro/) to visualize the path of a rain droplet from any point in the contiguous United States to its end point (usually the ocean, sometimes the Great Lakes, Canada/Mexico, or another inland water feature). It'll find the closest river/stream flowline coordinate to a click/search and then animate along that flowline's downstream path.
This project visualizes the path of a rain droplet from any point in the world to its end point (usually an ocean or an inland water features). It will find the closest river/stream flowline coordinate to a click/search and then animate along that flowline's downstream path. The data used in this project comes from the [River Runner API](https://ksonda.github.io/global-river-runner/), which is based on several open source projects and datasets. Similar data, initially used for the project, came from the USGS's [NHDPlus data](https://www.usgs.gov/core-science-systems/ngp/national-hydrography/nhdplus-high-resolution) and their [NLDI API](https://waterdata.usgs.gov/blog/nldi-intro/)

I've used mapbox to animate the downstream path, but needed to make all sorts of adjustments for elevation and bearing changes to prevent jerkiness/nausea (just moving from point to point feels a little like flying through turbulence while shaking your head side-to-side).

I've hosted a dataset with NHDPlus [Value Added Attributes](https://www.usgs.gov/core-science-systems/ngp/national-hydrography/value-added-attributes-vaas) on Firebase, which allows me to group flowlines into their parent features and determine distances quickly.

**Note**: The newly-released, global version of this project is in beta. We currently have relatively poor coverage of river names outside of the United States, which we are hoping to fill out, as well as some UX edge-cases and bugs that we hope to resolve.

## Examples

Here are a couple of examples of what it looks like in action.
Expand Down Expand Up @@ -35,7 +37,7 @@ Thank you to [Mapbox](https://www.mapbox.com/) for sponsoring this project!
<img src="https://user-images.githubusercontent.com/12772904/129089126-5c528d47-961f-427f-820f-df58974d15c3.png" alt="mapbox-logo-black" width="300"/>

## Updates
* **January 2021**: The [global version](https://river-runner-global.samlearner.com/) of this tool is now released and in beta! While some lingering issues are resolved and it remains in beta, it can be found on this branch, while the original, US-only version is preserved [here](https://github.com/sdl60660/river-runner/tree/us-only) in Github, and at its original URL: https://river-runner.samlearner.com/. This is to avoid any breaking changes to existing share links/paths due to any discrepancies and because minor US issues persist on the global version, mainly when paths involve dams, canals, or conduits.

* **January 2021**: The [global version](https://river-runner-global.samlearner.com/) of this tool is now released and in beta! While some lingering issues are resolved and it remains in beta, it can be found on [this branch](https://github.com/sdl60660/river-runner/tree/global). The original, US-only version is preserved on [this branch](https://github.com/sdl60660/river-runner/tree/us-only) in Github and at its original URL: https://river-runner.samlearner.com/. This is to avoid any breaking changes to existing share links/paths due to any discrepancies and because minor US issues persist on the global version, mainly when paths involve dams, canals, or conduits.

If you'd like to be notified about major updates to the tool, you can sign up for an email list [here](https://tinyletter.com/samlearner)
42 changes: 42 additions & 0 deletions data_processing/create_global_stop_feature_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import json

with open('data/ne_10m_geography_marine_polys.geojson', 'r') as f:
seas = json.load(f)['features']

with open('data/ne_10m_lakes.geojson', 'r') as f:
lakes = json.load(f)['features']


output_cols = ['name','featurecla']
ocean_feature_types = ['strait', 'gulf', 'river', 'inlet', 'sound', 'lagoon', 'ocean', 'sea', 'bay', 'fjord', 'generic', 'channel', 'reef']
lake_feature_types = ['alkaline lake', 'lake', 'reservoir']

all_output_features = []

for feature in (seas + lakes):
if feature['properties']['featurecla'].lower() in ocean_feature_types:
stop_feature_type = 'ocean'
else:
stop_feature_type = 'inland lake'

slimmed_feature_properties = {
'stop_feature_type': stop_feature_type,
'stop_feature_type_specific': feature['properties']['featurecla'].lower(),
'stop_feature_name': feature['properties']['name'],
}

geometry = feature['geometry']
if type(geometry['coordinates'][0][0][0]) == list:
geometry['type'] = "MultiPolygon"

output_feature = {
"type": "Feature",
"geometry": geometry,
"properties": slimmed_feature_properties
}
all_output_features.append(output_feature)

# Output as a GeoJSON file for now, but then use mapshaper.org to compress to a TopoJSON
output_data = {"type": "FeatureCollection", "features": all_output_features}
with open('data/global_stopping_features.geojson', 'w') as f:
json.dump(output_data, f)
1 change: 1 addition & 0 deletions data_processing/data/global_stopping_features.geojson

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions data_processing/data/global_stopping_features.json

Large diffs are not rendered by default.

307 changes: 307 additions & 0 deletions data_processing/data/ne_10m_geography_marine_polys.geojson

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions data_processing/data/ne_10m_geography_marine_polys.json

Large diffs are not rendered by default.

1,357 changes: 1,357 additions & 0 deletions data_processing/data/ne_10m_lakes.geojson

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions data_processing/data/ne_10m_lakes.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions name_server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Port number
PORT=3000

# URL of the Mongo DB
MONGODB_URL=mongodb://127.0.0.1:27017/node-boilerplate
15 changes: 15 additions & 0 deletions name_server/ecosystem.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"apps": [
{
"name": "app",
"script": "src/index.js",
"instances": 1,
"autorestart": true,
"watch": false,
"time": true,
"env": {
"NODE_ENV": "production"
}
}
]
}
43 changes: 43 additions & 0 deletions name_server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "name_server",
"version": "1.0.0",
"private": true,
"description": "node/express name server for processing name suggestions from river runner app",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "pm2 start ecosystem.config.json --no-daemon",
"dev": "cross-env NODE_ENV=development nodemon src/index.js"
},
"author": "Sam Learner",
"repository": {
"type": "git",
"url": "https://github.com/sdl60660/river-runner"
},
"license": "ISC",
"dependencies": {
"compression": "^1.7.4",
"cors": "^2.8.5",
"cross-env": "^7.0.0",
"dotenv": "^10.0.0",
"express": "^4.17.2",
"express-mongo-sanitize": "^2.1.0",
"express-rate-limit": "^5.5.1",
"helmet": "^4.1.0",
"http-status": "^1.5.0",
"joi": "^17.5.0",
"mongoose": "^5.13.13",
"nodemailer": "^6.3.1",
"passport": "^0.5.2",
"pm2": "^5.1.2",
"validator": "^13.0.0",
"winston": "^3.3.3",
"xss-clean": "^0.1.1"
},
"devDependencies": {
"nodemon": "^2.0.15"
},
"engines": {
"node": "14.x"
}
}
49 changes: 49 additions & 0 deletions name_server/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const express = require('express');
const helmet = require('helmet');
const xss = require('xss-clean');
const mongoSanitize = require('express-mongo-sanitize');
const compression = require('compression');
const cors = require('cors');
const httpStatus = require('http-status');
const { errorConverter, errorHandler } = require('./middlewares/error');
const ApiError = require('./utils/ApiError');

const { router } = require('./routes');

const app = express();

// set security HTTP headers
app.use(helmet());

// parse json request body
app.use(express.json());

// parse urlencoded request body
app.use(express.urlencoded({ extended: true }));

// sanitize request data
app.use(xss());
app.use(mongoSanitize());

// gzip compression
app.use(compression());

// enable cors
app.use(cors());
app.options('*', cors());

// API Routes
app.use('/api', router);

// send back a 404 error for any unknown api request
app.use((req, res, next) => {
next(new ApiError(httpStatus.NOT_FOUND, 'Not Found'));
});

// convert error to ApiError, if needed
app.use(errorConverter);

// handle error
app.use(errorHandler);

module.exports = app;
34 changes: 34 additions & 0 deletions name_server/src/config/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const dotenv = require('dotenv');
const path = require('path');
const Joi = require('joi');

if (process.env.NODE_ENV !== "production") {
dotenv.config({ path: path.join(__dirname, '../../.env') });
}

const envVarsSchema = Joi.object()
.keys({
NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),
PORT: Joi.number().default(3000),
MONGODB_URL: Joi.string().required().description('Mongo DB url')
})
.unknown();

const { value: envVars, error } = envVarsSchema.prefs({ errors: { label: 'key' } }).validate(process.env);

if (error) {
throw new Error(`Config validation error: ${error.message}`);
}

module.exports = {
env: envVars.NODE_ENV,
port: envVars.PORT,
mongoose: {
url: envVars.MONGODB_URL,
options: {
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
},
}
};
26 changes: 26 additions & 0 deletions name_server/src/config/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const winston = require('winston');
const config = require('./config');

const enumerateErrorFormat = winston.format((info) => {
if (info instanceof Error) {
Object.assign(info, { message: info.stack });
}
return info;
});

const logger = winston.createLogger({
level: config.env === 'development' ? 'debug' : 'info',
format: winston.format.combine(
enumerateErrorFormat(),
config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
winston.format.splat(),
winston.format.printf(({ level, message }) => `${level}: ${message}`)
),
transports: [
new winston.transports.Console({
stderrLevels: ['error'],
}),
],
});

module.exports = logger;
38 changes: 38 additions & 0 deletions name_server/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const mongoose = require('mongoose');
const app = require('./app');
const config = require('./config/config');
const logger = require('./config/logger');

let server;
mongoose.connect(config.mongoose.url, config.mongoose.options).then(() => {
logger.info('Connected to MongoDB');
server = app.listen(config.port, () => {
logger.info(`Listening to port ${config.port}`);
});
});

const exitHandler = () => {
if (server) {
server.close(() => {
logger.info('Server closed');
process.exit(1);
});
} else {
process.exit(1);
}
};

const unexpectedErrorHandler = (error) => {
logger.error(error);
exitHandler();
};

process.on('uncaughtException', unexpectedErrorHandler);
process.on('unhandledRejection', unexpectedErrorHandler);

process.on('SIGTERM', () => {
logger.info('SIGTERM received');
if (server) {
server.close();
}
});
39 changes: 39 additions & 0 deletions name_server/src/middlewares/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const mongoose = require('mongoose');
const httpStatus = require('http-status');
const config = require('../config/config');
const ApiError = require('../utils/ApiError');

const errorConverter = (err, req, res, next) => {
let error = err;
if (!(error instanceof ApiError)) {
const statusCode =
error.statusCode || error instanceof mongoose.Error ? httpStatus.BAD_REQUEST : httpStatus.INTERNAL_SERVER_ERROR;
const message = error.message || httpStatus[statusCode];
error = new ApiError(statusCode, message, false, err.stack);
}
next(error);
};

// eslint-disable-next-line no-unused-vars
const errorHandler = (err, req, res, next) => {
let { statusCode, message } = err;
if (config.env === 'production' && !err.isOperational) {
statusCode = httpStatus.INTERNAL_SERVER_ERROR;
message = httpStatus[httpStatus.INTERNAL_SERVER_ERROR];
}

res.locals.errorMessage = err.message;

const response = {
code: statusCode,
message,
...(config.env === 'development' && { stack: err.stack }),
};

res.status(statusCode).send(response);
};

module.exports = {
errorConverter,
errorHandler,
};
11 changes: 11 additions & 0 deletions name_server/src/middlewares/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const rateLimit = require('express-rate-limit');

const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 20,
skipSuccessfulRequests: true,
});

module.exports = {
authLimiter,
};
24 changes: 24 additions & 0 deletions name_server/src/models/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const mongoose = require("mongoose");

const querySchema = new mongoose.Schema({
lat: {
type: String,
required: true,
},
lng: {
type: String,
required: true,
},
from_share_link: {
type: Boolean,
required: true,
},
query_error: {
type: Boolean,
required: false
},
timestamp: Number,
});

const Query = mongoose.model("Query", querySchema);
module.exports = Query;
Loading

0 comments on commit 9c23f54

Please sign in to comment.