Skip to content

Instantly share code, notes, and snippets.

@kpwebb
Created July 9, 2020 19:52
Show Gist options
  • Save kpwebb/06e0792272d525b5546b56a9c1ebae3f to your computer and use it in GitHub Desktop.
Save kpwebb/06e0792272d525b5546b56a9c1ebae3f to your computer and use it in GitHub Desktop.
const fs = require("fs");
const path = require("path");
const express = require("express");
const fileUpload = require("express-fileupload");
const bodyParser = require("body-parser");
const rimraf = require("rimraf");
const mkdirp = require("mkdirp");
const child_process = require("child_process");
const turf = require("@turf/turf");
const archiver = require("archiver");
const copydir = require("copy-dir");
const Graph = require("./graph");
async function main() {
return new Promise(async (resolve, reject) => {
let app = express();
let ids = {
feature: 0,
image: 0,
};
app.use(fileUpload());
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
// constants
const PORT = 8081;
const PBF = path.join(__dirname, "../extract.osm.pbf");
const GRAPH = path.join(__dirname, "../graph.json");
const MBTILES = path.join(__dirname, "../extract.mbtiles");
const IMAGES = path.join(__dirname, "../static/images/survey");
const UNITS = { units: "meters" };
// application state
app.state = {};
// debug
app.state.graph = new Graph();
if (fs.existsSync(GRAPH)) {
console.log("Found graph.");
await app.state.graph.load(GRAPH);
} else {
console.log("No graph found.");
}
// setup static file server
app.use("/static", express.static(path.join(__dirname, "../static")));
app.use(
"/static/images",
express.static(path.join(__dirname, "../static/images"))
);
mkdirp.sync(IMAGES);
app.get("/", async (req, res) => {
if (fs.existsSync(GRAPH)) {
let template = (
await fs.promises.readFile(
path.join(__dirname, "../templates/index.html")
)
).toString();
template = template
.split("{{bounds}}")
.join(JSON.stringify(app.state.graph.bounds));
res.send(template);
} else {
// HTTP Status - 412 Precondition Failed
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412
res.status(412).redirect("/admin");
}
});
app.get("/counter", async (req, res) => {
let counterValue = parseInt(
(
await fs.promises.readFile(path.join(__dirname, "../ram/counter.txt"))
).toString()
);
res.json({ counter: counterValue });
});
app.get("/query", async (req, res) => {
let streets = await app.state.graph.query([
+req.query.minX,
+req.query.minY,
+req.query.maxX,
+req.query.maxY,
]);
res.send({
type: "FeatureCollection",
features: streets,
});
});
app.post("/pbf", async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send("No pbf file was uploaded.");
}
let pbf = req.files.pbf;
pbf.mv(PBF, async (err) => {
if (err) {
return res.status(500).send(err);
}
// extract pbf and build street database
app.state.graph = new Graph();
app.state.graph = await app.state.graph.extract(PBF);
await fs.promises.unlink(PBF);
if (fs.existsSync(GRAPH)) {
await fs.promises.unlink(GRAPH);
}
await app.state.graph.save(GRAPH);
res.status(200).send("Extract complete.");
});
});
app.post("/mbtiles", async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send("No mbtiles file was uploaded.");
}
let mbtiles = req.files.mbtiles;
mbtiles.mv(MBTILES, async (err) => {
if (err) {
return res.status(500).send(err);
}
res.status(200).send("Tiles upload complete.");
});
});
app.post("/photo", async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send("No image file was uploaded.");
}
let image = req.files.image;
let ext = path.extname(req.files.image.name);
let name = uuid() + ext;
let imagePath = path.join(IMAGES, "/" + name);
image.mv(imagePath, async (err) => {
if (err) {
return res.status(500).send(err);
}
res.status(200).send("/static/images/survey/" + name);
});
});
app.post("/reset-surveys", async (req, res) => {
app.state.graph.surveys = new Map();
rimraf.sync(IMAGES);
mkdirp.sync(IMAGES);
await app.state.graph.save(GRAPH);
res.status(200).redirect("/admin");
});
app.get("/export.zip", async (req, res) => {
let spans = [];
let positions = [];
let images = [];
let spanPoints = [];
let spanAndPositionPoints = [];
for (let [ref, surveys] of app.state.graph.surveys) {
if (!app.state.graph.refs.has(ref)) {
throw new Error("Surveyed street ref not found: ", ref);
}
let street = app.state.graph.streets[app.state.graph.refs.get(ref)];
// flip geometry if survey is back ref
if (ref === street.properties.back) {
street.geometry.coordinates.reverse();
}
for (let survey of surveys) {
let startOffset =
(street.properties.distance - survey.surveyed_distance) / 2;
let endOffset =
street.properties.distance -
(street.properties.distance - survey.surveyed_distance) / 2;
if (street.properties.distance <= survey.surveyed_distance) {
startOffset = 0;
endOffset = street.properties.distance;
}
let centered = turf.lineString([
turf.along(street, startOffset, UNITS).geometry.coordinates,
turf.along(street, endOffset, UNITS).geometry.coordinates,
]);
for (let feature of survey.features) {
try {
if (feature.geometry.type === "Span") {
let line = turf.lineSliceAlong(
centered,
feature.geometry.distances[0],
feature.geometry.distances[1],
UNITS
);
let span = {
type: "Feature",
geometry: {
type: "LineString",
coordinates: line.geometry.coordinates,
},
properties: {
created_at: survey.created_at,
cwheelid: "", // todo: figure out where to find this
shst_ref_id: survey.shst_ref_id,
ref_side: survey.side_of_street,
ref_len: street.properties.distance,
srv_dist: survey.surveyed_distance,
srv_id: survey.id,
feat_id: feature.id,
label: feature.label,
dst_st: feature.geometry.distances[0],
dst_end: feature.geometry.distances[1],
images: JSON.stringify(feature.images),
},
};
let start = turf.point(
span.geometry.coordinates[0],
span.properties
);
let end = turf.point(
span.geometry.coordinates[span.geometry.coordinates.length - 1],
span.properties
);
for (let image of feature.images) {
let pt = turf.along(centered, image.geometry.distance, UNITS);
pt.properties.url = image.url;
images.push(pt);
}
spans.push(span);
spanPoints.push(start);
spanPoints.push(end);
spanAndPositionPoints.push(start);
spanAndPositionPoints.push(end);
} else if (feature.geometry.type === "Position") {
// todo: Positions are being incorrectly stored with span style distances array. Fix upstream.
let point = turf.along(
centered,
feature.geometry.distances[0],
UNITS
);
point.properties = {
created_at: survey.created_at,
cwheelid: "", // todo: figure out where to find this
shst_ref_id: survey.shst_ref_id,
ref_side: survey.side_of_street,
ref_len: street.properties.distance,
srv_dist: survey.surveyed_distance,
srv_id: survey.id,
feat_id: feature.id,
label: feature.label,
dst_st: feature.geometry.distances[0],
images: JSON.stringify(feature.images),
};
for (let image of feature.images) {
let pt = turf.point(point.geometry.coordinates);
pt.properties.url = image.url;
images.push(pt);
}
positions.push(point);
spanAndPositionPoints.push(point);
} else {
throw new Error("Unknown geometry type.");
}
}
catch(e) {
console.log('unable to export feature: ' + e);
}
}
}
}
let exportDir = path.join(__dirname, "../export");
let zipDir = path.join(__dirname, "../export.zip");
try {
rimraf.sync(exportDir);
rimraf.sync(zipDir);
} catch (e) {
console.error(e);
}
mkdirp.sync(exportDir);
await fs.promises.writeFile(
path.join(exportDir, "spans.geojson"),
JSON.stringify(turf.featureCollection(spans)),
{
name: "spans.geojson",
}
);
await fs.promises.writeFile(
path.join(exportDir, "positions.geojson"),
JSON.stringify(turf.featureCollection(positions)),
{
name: "positions.geojson",
}
);
await fs.promises.writeFile(
path.join(exportDir, "spanPoints.geojson"),
JSON.stringify(turf.featureCollection(spanPoints)),
{
name: "spanPoints.geojson",
}
);
await fs.promises.writeFile(
path.join(exportDir, "spanAndPositionPoints.geojson"),
JSON.stringify(turf.featureCollection(spanAndPositionPoints)),
{
name: "spanAndPositionPoints.geojson",
}
);
await fs.promises.writeFile(
path.join(exportDir, "images.geojson"),
JSON.stringify(turf.featureCollection(images)),
{
name: "images.geojson",
}
);
copydir.sync(
path.join(__dirname, "../static/images/survey"),
path.join(exportDir, "./images")
);
var archive = archiver("zip", {
zlib: { level: 9 }, // compression level
});
let output = fs.createWriteStream(zipDir);
archive.directory(exportDir, false);
output.on("close", function () {
res.status(200).download(zipDir);
});
archive.pipe(output);
archive.finalize();
});
app.get("/surveys/:ref", async (req, res) => {
let ref = req.params.ref;
let surveys = app.state.graph.surveys.get(ref);
if (!surveys) {
surveys = [];
}
res.status(200).send(surveys);
});
app.post("/surveys/:ref", async (req, res) => {
let ref = req.params.ref;
let surveys = app.state.graph.surveys.get(ref);
if (!surveys) {
surveys = [];
}
surveys.push(req.body);
app.state.graph.surveys.set(ref, surveys);
await app.state.graph.save(GRAPH);
res.status(200).send("Uploaded survey.");
});
app.get("/admin", async (req, res) => {
let template = (
await fs.promises.readFile(
path.join(__dirname, "../templates/admin.html")
)
).toString();
res.send(template);
});
app.post("/admin/update", async (req, res) => {
res.status(200).send(wifiSettings);
});
app.get("/admin/wifi", async (req, res) => {
var wifiSettings = { mode: "ap", network: "", password: "" };
try {
wifiSettings = JSON.parse(
fs.readFileSync(path.join(__dirname, "../config/wifi.json"))
);
} catch (e) {
console.error(e);
}
// return wifiSettings
res.status(200).send(wifiSettings);
});
app.post("/admin/wifi", async (req, res) => {
const wifiSettings = JSON.stringify(req.body);
// todo validate wifi
//write wifiSettings to config file
fs.writeFileSync(
path.join(__dirname, "../config/wifi.json"),
wifiSettings
);
var wpaConfTemplate = fs.readFileSync(
path.join(__dirname, "../config/wpa_supplicant.conf.template"),
"utf8"
);
var wpaConf = wpaConfTemplate
.replace("[NAME OF WIFI NETWORK]", req.body.network)
.replace("[WIFI NETWORK PASSWORD]", req.body.password);
fs.writeFileSync(
path.join(__dirname, "../config/wpa_supplicant.conf"),
wpaConf
);
if (req.body.mode === "ap")
child_process.execSync("sh switch-to-ap.sh", {
cwd: "/home/pi/curb-wheel/",
});
else if (req.body.mode === "wifi")
child_process.execSync("sh switch-to-wifi.sh", {
cwd: "/home/pi/curb-wheel/",
});
// return wifiSettings
res.status(200).send(wifiSettings);
});
app.get("/admin/version", async (req, res) => {
const versionNumber = JSON.parse(
fs.readFileSync(path.join(__dirname, "../package.json"))
).version;
// return version number
res.status(200).send({ version: versionNumber });
});
app.get("/digitizer", async (req, res) => {
let template = (
await fs.promises.readFile(
path.join(__dirname, "../templates/digitizer.html")
)
).toString();
res.send(template);
});
app.get("/*", async (req, res) => {
let template = (
await fs.promises.readFile(
path.join(__dirname, "../templates/404.html")
)
).toString();
res.status(404).send(template);
});
let server = app.listen(PORT, () => {
console.log("listening on: 127.0.0.1:" + PORT);
return resolve(server);
});
});
}
function uuid() {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}
if (require.main === module) {
// cli
main();
} else {
// lib
module.exports = main;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment