Lint prop-types, camelCase

This commit is contained in:
Tom Russell 2019-05-27 18:26:29 +01:00
parent aef53a0ae0
commit 9b96872922
27 changed files with 524 additions and 246 deletions

View File

@ -1,13 +1,15 @@
{
"extends": [
"eslint:recommended",
"plugin:jest/recommended",
"plugin:react/recommended"
],
"env": {
"browser": true,
"commonjs": true,
"node": true,
"es6": true
"es6": true,
"jest": true
},
"parser": "babel-eslint",
"parserOptions": {
@ -23,9 +25,12 @@
"curly": "warn",
"quotes": ["warn", "single"],
"indent": ["warn", 4],
"no-multi-spaces": ["error", {"ignoreEOLComments": true} ]
"no-multi-spaces": ["warn", {"ignoreEOLComments": true} ],
"comma-spacing": ["warn"],
"camelcase": ["warn", {"ignoreDestructuring": true ,"properties": "never"}]
},
"plugins": [
"jest",
"react"
],
"settings": {

20
app/package-lock.json generated
View File

@ -4904,6 +4904,12 @@
}
}
},
"eslint-plugin-jest": {
"version": "22.6.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.6.4.tgz",
"integrity": "sha512-36OqnZR/uMCDxXGmTsqU4RwllR0IiB/XF8GW3ODmhsjiITKuI0GpgultWFt193ipN3HARkaIcKowpE6HBvRHNg==",
"dev": true
},
"eslint-plugin-react": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.13.0.tgz",
@ -12903,12 +12909,13 @@
}
},
"prop-types": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"proxy-addr": {
@ -13412,8 +13419,7 @@
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
"dev": true
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-leaflet": {
"version": "1.9.1",

View File

@ -25,6 +25,7 @@
"mapnik": "^4.2.1",
"node-fs": "^0.1.7",
"pg-promise": "^8.7.2",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-leaflet": "^1.0.1",
@ -36,6 +37,7 @@
"devDependencies": {
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-jest": "^22.6.4",
"eslint-plugin-react": "^7.13.0",
"razzle": "^3.0.0"
}

View File

@ -3,7 +3,7 @@
*
*/
import db from '../db';
import { remove_all_at_bbox } from '../tiles/cache';
import { removeAllAtBbox } from '../tiles/cache';
// data type note: PostgreSQL bigint (64-bit) is handled as string in JavaScript, because of
// JavaScript numerics are 64-bit double, giving only partial coverage.
@ -85,10 +85,10 @@ function getBuildingById(id) {
});
}
function getBuildingLikeById(building_id, user_id) {
function getBuildingLikeById(buildingId, userId) {
return db.oneOrNone(
'SELECT true as like FROM building_user_likes WHERE building_id = $1 and user_id = $2 LIMIT 1',
[building_id, user_id]
[buildingId, userId]
).then(res => {
return res && res.like
}).catch(function (error) {
@ -107,13 +107,7 @@ function getBuildingUPRNsById(id) {
});
}
function saveBuilding(building_id, building, user_id) {
// save building could fail if the revision seen by the user != the latest revision
// - any 'intuitive' retries would need to be handled by clients of this code
// revision id allows for a long user 'think time' between view-building, update-building
// (optimistic locking implemented using field-based row versioning)
// const previous_revision_id = building.revision_id;
function saveBuilding(buildingId, building, userId) {
// remove read-only fields from consideration
delete building.building_id;
delete building.revision_id;
@ -127,10 +121,10 @@ function saveBuilding(building_id, building, user_id) {
return db.tx(t => {
return t.one(
'SELECT * FROM buildings WHERE building_id = $1 FOR UPDATE;',
[building_id]
).then(old_building => {
const patches = compare(old_building, building, BUILDING_FIELD_WHITELIST);
console.log('Patching', building_id, patches)
[buildingId]
).then(oldBuilding => {
const patches = compare(oldBuilding, building, BUILDING_FIELD_WHITELIST);
console.log('Patching', buildingId, patches)
const forward = patches[0];
const reverse = patches[1];
if (Object.keys(forward).length === 0) {
@ -143,10 +137,10 @@ function saveBuilding(building_id, building, user_id) {
$1:json, $2:json, $3, $4
) RETURNING log_id
`,
[forward, reverse, building_id, user_id]
[forward, reverse, buildingId, userId]
).then(revision => {
const sets = db.$config.pgp.helpers.sets(forward);
console.log('Setting', building_id, sets)
console.log('Setting', buildingId, sets)
return t.one(
`UPDATE
buildings
@ -158,9 +152,9 @@ function saveBuilding(building_id, building, user_id) {
RETURNING
*
`,
[revision.log_id, sets, building_id]
[revision.log_id, sets, buildingId]
).then((data) => {
expireBuildingTileCache(building_id)
expireBuildingTileCache(buildingId)
return data
})
});
@ -172,7 +166,7 @@ function saveBuilding(building_id, building, user_id) {
}
function likeBuilding(building_id, user_id) {
function likeBuilding(buildingId, userId) {
// start transaction around save operation
// - insert building-user like
// - count total likes
@ -182,11 +176,11 @@ function likeBuilding(building_id, user_id) {
return db.tx({ serializable }, t => {
return t.none(
'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);',
[building_id, user_id]
[buildingId, userId]
).then(() => {
return t.one(
'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;',
[building_id]
[buildingId]
).then(building => {
return t.one(
`INSERT INTO logs (
@ -195,7 +189,7 @@ function likeBuilding(building_id, user_id) {
$1:json, $2, $3
) RETURNING log_id
`,
[{ likes_total: building.likes }, building_id, user_id]
[{ likes_total: building.likes }, buildingId, userId]
).then(revision => {
return t.one(
`UPDATE buildings
@ -207,9 +201,9 @@ function likeBuilding(building_id, user_id) {
RETURNING
*
`,
[revision.log_id, building.likes, building_id]
[revision.log_id, building.likes, buildingId]
).then((data) => {
expireBuildingTileCache(building_id)
expireBuildingTileCache(buildingId)
return data
})
})
@ -227,7 +221,7 @@ function likeBuilding(building_id, user_id) {
}
function unlikeBuilding(building_id, user_id) {
function unlikeBuilding(buildingId, userId) {
// start transaction around save operation
// - insert building-user like
// - count total likes
@ -237,11 +231,11 @@ function unlikeBuilding(building_id, user_id) {
return db.tx({ serializable }, t => {
return t.none(
'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;',
[building_id, user_id]
[buildingId, userId]
).then(() => {
return t.one(
'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;',
[building_id]
[buildingId]
).then(building => {
return t.one(
`INSERT INTO logs (
@ -250,7 +244,7 @@ function unlikeBuilding(building_id, user_id) {
$1:json, $2, $3
) RETURNING log_id
`,
[{ likes_total: building.likes }, building_id, user_id]
[{ likes_total: building.likes }, buildingId, userId]
).then(revision => {
return t.one(
`UPDATE buildings
@ -262,9 +256,9 @@ function unlikeBuilding(building_id, user_id) {
RETURNING
*
`,
[revision.log_id, building.likes, building_id]
[revision.log_id, building.likes, buildingId]
).then((data) => {
expireBuildingTileCache(building_id)
expireBuildingTileCache(buildingId)
return data
})
})
@ -281,7 +275,7 @@ function unlikeBuilding(building_id, user_id) {
});
}
function privateQueryBuildingBBOX(building_id){
function privateQueryBuildingBBOX(buildingId){
return db.one(
`SELECT
ST_XMin(envelope) as xmin,
@ -297,14 +291,14 @@ function privateQueryBuildingBBOX(building_id){
AND
b.building_id = $1
) as envelope`,
[building_id]
[buildingId]
)
}
function expireBuildingTileCache(building_id) {
privateQueryBuildingBBOX(building_id).then((bbox) => {
const building_bbox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin]
remove_all_at_bbox(building_bbox);
function expireBuildingTileCache(buildingId) {
privateQueryBuildingBBOX(buildingId).then((bbox) => {
const buildingBbox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin]
removeAllAtBbox(buildingBbox);
})
}
@ -361,21 +355,21 @@ const BUILDING_FIELD_WHITELIST = new Set([
* - forward patch is object with {keys: new_values}
* - reverse patch is object with {keys: old_values}
*
* @param {object} old_obj
* @param {object} new_obj
* @param {object} oldObj
* @param {object} newObj
* @param {Set} whitelist
* @returns {[object, object]}
*/
function compare(old_obj, new_obj, whitelist) {
const reverse_patch = {}
const forward_patch = {}
for (const [key, value] of Object.entries(new_obj)) {
if (old_obj[key] !== value && whitelist.has(key)) {
reverse_patch[key] = old_obj[key];
forward_patch[key] = value;
function compare(oldObj, newObj, whitelist) {
const reverse = {}
const forward = {}
for (const [key, value] of Object.entries(newObj)) {
if (oldObj[key] !== value && whitelist.has(key)) {
reverse[key] = oldObj[key];
forward[key] = value;
}
}
return [forward_patch, reverse_patch]
return [forward, reverse]
}
export {

View File

@ -9,7 +9,7 @@
import db from '../db';
function queryLocation(term) {
const max_results = 5;
const limit = 5;
return db.manyOrNone(
`SELECT
search_str, search_class, ST_AsGeoJSON(center), zoom,
@ -19,7 +19,7 @@ function queryLocation(term) {
ORDER BY
dist
LIMIT $2;`,
[term, max_results]
[term, limit]
).catch((error) => {
console.error(error);
return undefined;

View File

@ -70,7 +70,7 @@ function authUser(username, password) {
})
}
function getUserById(user_id) {
function getUserById(id) {
return db.one(
`SELECT
username, email, registered, api_key
@ -79,7 +79,7 @@ function getUserById(user_id) {
WHERE
user_id = $1
`, [
user_id
id
]
).catch(function (error) {
console.error('Error:', error)
@ -87,7 +87,7 @@ function getUserById(user_id) {
});
}
function getNewUserAPIKey(user_id) {
function getNewUserAPIKey(id) {
return db.one(
`UPDATE
users
@ -98,7 +98,7 @@ function getNewUserAPIKey(user_id) {
RETURNING
api_key
`, [
user_id
id
]
).catch(function (error) {
console.error('Error:', error)
@ -106,7 +106,7 @@ function getNewUserAPIKey(user_id) {
});
}
function authAPIUser(api_key) {
function authAPIUser(key) {
return db.one(
`SELECT
user_id
@ -115,7 +115,7 @@ function authAPIUser(api_key) {
WHERE
api_key = $1
`, [
api_key
key
]
).catch(function (error) {
console.error('Error:', error)

View File

@ -1,5 +1,6 @@
import React, { Fragment } from 'react';
import { Route, Switch, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
import './app.css';
@ -170,6 +171,12 @@ class App extends React.Component {
}
}
App.propTypes = {
user: PropTypes.object,
building: PropTypes.object,
building_like: PropTypes.bool
}
/**
* Component to fall back on in case of 404 or no other match
*/

View File

@ -1,5 +1,6 @@
import React, { Component, Fragment } from 'react';
import { Link, NavLink, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import ErrorBox from './error-box';
import InfoBox from './info-box';
@ -32,16 +33,22 @@ const BuildingEdit = (props) => {
title={'You are editing'}
back={`/edit/${cat}.html`}>
{
CONFIG.map((conf_props) => {
CONFIG.map((section) => {
return <EditForm
{...conf_props} {...props}
cat={cat} key={conf_props.slug} />
{...section} {...props}
cat={cat} key={section.slug} />
})
}
</Sidebar>
);
}
BuildingEdit.propTypes = {
user: PropTypes.object,
match: PropTypes.object,
building_id: PropTypes.string,
}
class EditForm extends Component {
constructor(props) {
super(props);
@ -167,7 +174,7 @@ class EditForm extends Component {
render() {
const match = this.props.cat === this.props.slug;
const building_like = this.props.building_like;
const buildingLike = this.props.building_like;
return (
<section className={(this.props.inactive)? 'data-section inactive': 'data-section'}>
<header className={`section-header edit ${this.props.slug} ${(match? 'active' : '')}`}>
@ -235,7 +242,7 @@ class EditForm extends Component {
value={this.state[props.slug]} key={props.slug} />
case 'like':
return <LikeButton {...props} handleLike={this.handleLike}
building_like={building_like}
building_like={buildingLike}
value={this.state[props.slug]} key={props.slug} />
default:
return null
@ -260,6 +267,20 @@ class EditForm extends Component {
}
}
EditForm.propTypes = {
title: PropTypes.string,
slug: PropTypes.string,
cat: PropTypes.string,
help: PropTypes.string,
error: PropTypes.object,
like: PropTypes.bool,
building_like: PropTypes.bool,
selectBuilding: PropTypes.func,
building_id: PropTypes.string,
inactive: PropTypes.bool,
fields: PropTypes.array
}
const TextInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
@ -274,6 +295,17 @@ const TextInput = (props) => (
</Fragment>
);
TextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.string,
max_length: PropTypes.number,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
handleChange: PropTypes.func
}
const LongTextInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
@ -286,6 +318,15 @@ const LongTextInput = (props) => (
</Fragment>
)
LongTextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.string,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
handleChange: PropTypes.func
}
class MultiTextInput extends Component {
constructor(props) {
@ -301,11 +342,11 @@ class MultiTextInput extends Component {
}
edit(event) {
const edit_i = +event.target.dataset.index;
const edit_item = event.target.value;
const old_values = this.getValues();
const values = old_values.map((item, i) => {
return i === edit_i ? edit_item : item;
const editIndex = +event.target.dataset.index;
const editItem = event.target.value;
const oldValues = this.getValues();
const values = oldValues.map((item, i) => {
return i === editIndex ? editItem : item;
});
this.props.handleChange(this.props.slug, values);
}
@ -317,9 +358,9 @@ class MultiTextInput extends Component {
}
remove(event){
const remove_i = +event.target.dataset.index;
const removeIndex = +event.target.dataset.index;
const values = this.getValues().filter((_, i) => {
return i !== remove_i;
return i !== removeIndex;
});
this.props.handleChange(this.props.slug, values);
}
@ -355,6 +396,16 @@ class MultiTextInput extends Component {
}
}
MultiTextInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
placeholder: PropTypes.string,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
const TextListInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
@ -374,6 +425,16 @@ const TextListInput = (props) => (
</Fragment>
)
TextListInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.string),
value: PropTypes.string,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
const NumberInput = (props) => (
<Fragment>
<Label slug={props.slug} title={props.title} tooltip={props.tooltip} />
@ -386,6 +447,16 @@ const NumberInput = (props) => (
</Fragment>
);
NumberInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
step: PropTypes.number,
value: PropTypes.number,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
class YearEstimator extends Component {
constructor(props) {
super(props);
@ -408,6 +479,18 @@ class YearEstimator extends Component {
}
}
YearEstimator.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
date_year: PropTypes.number,
date_upper: PropTypes.number,
date_lower: PropTypes.number,
value: PropTypes.number,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
const CheckboxInput = (props) => (
<div className="form-check">
<input className="form-check-input" type="checkbox"
@ -423,6 +506,15 @@ const CheckboxInput = (props) => (
</div>
)
CheckboxInput.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.bool,
disabled: PropTypes.bool,
handleChange: PropTypes.func
}
const LikeButton = (props) => (
<Fragment>
<p className="likes">{(props.value)? props.value : 0} likes</p>
@ -441,11 +533,27 @@ const LikeButton = (props) => (
</Fragment>
);
LikeButton.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.number,
building_like: PropTypes.bool,
disabled: PropTypes.bool,
handleLike: PropTypes.func
}
const Label = (props) => (
<label htmlFor={props.slug}>
{props.title}
{ props.tooltip? <Tooltip text={ props.tooltip } /> : null }
</label>
)
);
Label.propTypes = {
slug: PropTypes.string,
title: PropTypes.string,
tooltip: PropTypes.string
}
export default BuildingEdit;

View File

@ -1,6 +1,7 @@
import urlapi from 'url';
import React, { Fragment } from 'react';
import { Link, NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import Sidebar from './sidebar';
import Tooltip from './tooltip';
@ -26,40 +27,40 @@ const BuildingView = (props) => {
return (
<Sidebar title={'Data available for this building'} back={`/view/${cat}.html`}>
{
CONFIG.map(section_props => (
CONFIG.map(section => (
<DataSection
key={section_props.slug} cat={cat}
key={section.slug} cat={cat}
building_id={props.building_id}
{...section_props}>
{...section}>
{
section_props.fields.map(field_props => {
section.fields.map(field => {
switch (field_props.type) {
switch (field.type) {
case 'uprn_list':
return <UPRNsDataEntry
key={field_props.slug}
title={field_props.title}
key={field.slug}
title={field.title}
value={props.uprns}
tooltip={field_props.tooltip} />
tooltip={field.tooltip} />
case 'text_multi':
return <MultiDataEntry
key={field_props.slug}
title={field_props.title}
value={props[field_props.slug]}
tooltip={field_props.tooltip} />
key={field.slug}
title={field.title}
value={props[field.slug]}
tooltip={field.tooltip} />
case 'like':
return <LikeDataEntry
key={field_props.slug}
title={field_props.title}
value={props[field_props.slug]}
key={field.slug}
title={field.title}
value={props[field.slug]}
user_building_like={props.building_like}
tooltip={field_props.tooltip} />
tooltip={field.tooltip} />
default:
return <DataEntry
key={field_props.slug}
title={field_props.title}
value={props[field_props.slug]}
tooltip={field_props.tooltip} />
key={field.slug}
title={field.title}
value={props[field.slug]}
tooltip={field.tooltip} />
}
})
}
@ -70,6 +71,15 @@ const BuildingView = (props) => {
);
}
BuildingView.propTypes = {
building_id: PropTypes.string,
match: PropTypes.object,
uprns: PropTypes.arrayOf(PropTypes.shape({
uprn: PropTypes.string.isRequired,
parent_uprn: PropTypes.string
})),
building_like: PropTypes.bool
}
const DataSection = (props) => {
const match = props.cat === props.slug;
@ -113,6 +123,17 @@ const DataSection = (props) => {
);
}
DataSection.propTypes = {
title: PropTypes.string,
cat: PropTypes.string,
slug: PropTypes.string,
intro: PropTypes.string,
help: PropTypes.string,
inactive: PropTypes.bool,
building_id: PropTypes.string,
children: PropTypes.node
}
const DataEntry = (props) => (
<Fragment>
<dt>
@ -128,6 +149,12 @@ const DataEntry = (props) => (
</Fragment>
);
DataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.any
}
const LikeDataEntry = (props) => (
<Fragment>
<dt>
@ -149,13 +176,20 @@ const LikeDataEntry = (props) => (
</Fragment>
);
LikeDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.any,
user_building_like: PropTypes.bool
}
const MultiDataEntry = (props) => {
let content;
if (props.value && props.value.length) {
content = <ul>{
props.value.map((item, index) => {
return <li key={index}><a href={sanitise_url(item)}>{item}</a></li>
return <li key={index}><a href={sanitiseURL(item)}>{item}</a></li>
})
}</ul>
} else {
@ -173,7 +207,13 @@ const MultiDataEntry = (props) => {
);
}
function sanitise_url(string){
MultiDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string)
}
function sanitiseURL(string){
let url_
// http or https
@ -211,8 +251,8 @@ function sanitise_url(string){
const UPRNsDataEntry = (props) => {
const uprns = props.value || [];
const no_parent = uprns.filter(uprn => uprn.parent_uprn == null);
const with_parent = uprns.filter(uprn => uprn.parent_uprn != null);
const noParent = uprns.filter(uprn => uprn.parent_uprn == null);
const withParent = uprns.filter(uprn => uprn.parent_uprn != null);
return (
<Fragment>
@ -222,18 +262,18 @@ const UPRNsDataEntry = (props) => {
</dt>
<dd><ul className="uprn-list">
<Fragment>{
no_parent.length?
no_parent.map(uprn => (
noParent.length?
noParent.map(uprn => (
<li key={uprn.uprn}>{uprn.uprn}</li>
))
: '\u00A0'
}</Fragment>
{
with_parent.length?
withParent.length?
<details>
<summary>Children</summary>
{
with_parent.map(uprn => (
withParent.map(uprn => (
<li key={uprn.uprn}>{uprn.uprn} (child of {uprn.parent_uprn})</li>
))
}
@ -245,4 +285,13 @@ const UPRNsDataEntry = (props) => {
)
}
UPRNsDataEntry.propTypes = {
title: PropTypes.string,
tooltip: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.shape({
uprn: PropTypes.string.isRequired,
parent_uprn: PropTypes.string
}))
}
export default BuildingView;

View File

@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
function ErrorBox(props){
if (props.msg) {
@ -22,4 +23,8 @@ function ErrorBox(props){
);
}
ErrorBox.propTypes = {
msg: PropTypes.string
}
export default ErrorBox;

View File

@ -1,5 +1,6 @@
import React, { Fragment } from 'react';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import Logo from './logo';
import './header.css';
@ -100,4 +101,10 @@ class Header extends React.Component {
}
}
Header.propTypes = {
user: PropTypes.shape({
username: PropTypes.string
})
}
export default Header;

View File

@ -1,4 +1,5 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
const InfoBox = (props) => (
<Fragment>
@ -20,4 +21,9 @@ const InfoBox = (props) => (
</Fragment>
);
InfoBox.propTypes = {
msg: PropTypes.string,
children: PropTypes.node
}
export default InfoBox;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import './legend.css';
@ -136,19 +137,23 @@ const Legend = (props) => {
}
{
elements.length?
(<ul className="data-legend">
<ul className="data-legend">
{
elements.map((data_item) => (
<LegendItem {...data_item} key={data_item.color} />
elements.map((item) => (
<LegendItem {...item} key={item.color} />
))
}
</ul>)
: (<p className="data-intro">Coming soon</p>)
</ul>
: <p className="data-intro">Coming soon</p>
}
</div>
);
}
Legend.propTypes = {
slug: PropTypes.string
}
const LegendItem = (props) => (
<li>
<span className="key" style={ { background: props.color } }>-</span>
@ -156,4 +161,9 @@ const LegendItem = (props) => (
</li>
);
LegendItem.propTypes = {
color: PropTypes.string,
text: PropTypes.string
}
export default Legend;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Redirect, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import ErrorBox from './error-box';
import InfoBox from './info-box';
@ -126,4 +127,9 @@ class Login extends Component {
}
}
Login.propTypes = {
login: PropTypes.func,
user: PropTypes.object
}
export default Login;

View File

@ -1,4 +1,5 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Map, TileLayer, ZoomControl, AttributionControl } from 'react-leaflet-universal';
import '../../node_modules/leaflet/dist/leaflet.css'
@ -39,12 +40,12 @@ class ColouringMap extends Component {
}
handleClick(e) {
const is_edit = this.props.match.url.match('edit')
const mode = is_edit? 'edit': 'view';
const isEdit = this.props.match.url.match('edit')
const mode = isEdit? 'edit': 'view';
const lat = e.latlng.lat
const lng = e.latlng.lng
const new_cat = parseCategoryURL(this.props.match.url);
const map_cat = new_cat || 'age';
const newCat = parseCategoryURL(this.props.match.url);
const mapCat = newCat || 'age';
fetch(
'/buildings/locate?lat='+lat+'&lng='+lng
).then(
@ -53,11 +54,11 @@ class ColouringMap extends Component {
if (data && data.length){
const building = data[0];
this.props.selectBuilding(building);
this.props.history.push(`/${mode}/${map_cat}/building/${building.building_id}.html`);
this.props.history.push(`/${mode}/${mapCat}/building/${building.building_id}.html`);
} else {
// deselect but keep/return to expected colour theme
this.props.selectBuilding(undefined);
this.props.history.push(`/${mode}/${map_cat}.html`);
this.props.history.push(`/${mode}/${mapCat}.html`);
}
}.bind(this)).catch(
(err) => console.error(err)
@ -81,37 +82,37 @@ class ColouringMap extends Component {
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines.'
// colour-data tiles
const is_building = /building/.test(this.props.match.url);
const is_edit = /edit/.test(this.props.match.url);
const isBuilding = /building/.test(this.props.match.url);
const isEdit = /edit/.test(this.props.match.url);
const cat = parseCategoryURL(this.props.match.url);
const tileset_by_cat = {
const tilesetByCat = {
age: 'date_year',
size: 'size_storeys',
location: 'location',
like: 'likes',
planning: 'conservation_area',
}
const data_tileset = tileset_by_cat[cat];
const tileset = tilesetByCat[cat];
// pick revision id to bust browser cache
const rev = this.props.building? this.props.building.revision_id : '';
const dataLayer = data_tileset?
const dataLayer = tileset?
<TileLayer
key={data_tileset}
url={`/tiles/${data_tileset}/{z}/{x}/{y}.png?rev=${rev}`}
key={tileset}
url={`/tiles/${tileset}/{z}/{x}/{y}.png?rev=${rev}`}
minZoom={9} />
: null;
// highlight
const geometry_id = (this.props.building) ? this.props.building.geometry_id : undefined;
const highlight = `/tiles/highlight/{z}/{x}/{y}.png?highlight=${geometry_id}`
const highlightLayer = (is_building && this.props.building) ?
const geometryId = (this.props.building) ? this.props.building.geometry_id : undefined;
const highlight = `/tiles/highlight/{z}/{x}/{y}.png?highlight=${geometryId}`
const highlightLayer = (isBuilding && this.props.building) ?
<TileLayer
key={this.props.building.building_id}
url={highlight}
minZoom={14} />
: null;
const base_layer_url = (this.state.theme === 'light')?
const baseUrl = (this.state.theme === 'light')?
'/tiles/base_light/{z}/{x}/{y}.png'
: '/tiles/base_night/{z}/{x}/{y}.png'
@ -128,16 +129,16 @@ class ColouringMap extends Component {
onClick={this.handleClick}
>
<TileLayer url={url} attribution={attribution} />
<TileLayer url={base_layer_url} minZoom={14} />
<TileLayer url={baseUrl} minZoom={14} />
{ dataLayer }
{ highlightLayer }
<ZoomControl position="topright" />
<AttributionControl prefix="" />
</Map>
{
!is_building && this.props.match.url !== '/'? (
!isBuilding && this.props.match.url !== '/'? (
<div className="map-notice">
<HelpIcon /> {is_edit? 'Click a building to edit' : 'Click a building for details'}
<HelpIcon /> {isEdit? 'Click a building to edit' : 'Click a building for details'}
</div>
) : null
}
@ -146,7 +147,7 @@ class ColouringMap extends Component {
<Fragment>
<Legend slug={cat} />
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
<SearchBox onLocate={this.handleLocate} is_building={is_building} />
<SearchBox onLocate={this.handleLocate} isBuilding={isBuilding} />
</Fragment>
) : null
}
@ -155,4 +156,11 @@ class ColouringMap extends Component {
}
}
ColouringMap.propTypes = {
building: PropTypes.object,
selectBuilding: PropTypes.func,
match: PropTypes.object,
history: PropTypes.object
}
export default ColouringMap;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Link, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import ErrorBox from './error-box';
@ -109,4 +110,16 @@ class MyAccountPage extends Component {
}
}
MyAccountPage.propTypes = {
user: PropTypes.shape({
username: PropTypes.string,
email: PropTypes.string,
registered: PropTypes.string,
api_key: PropTypes.string,
error: PropTypes.object
}),
updateUser: PropTypes.func,
logout: PropTypes.func
}
export default MyAccountPage;

View File

@ -1,14 +1,15 @@
import React, { Fragment } from 'react';
import { NavLink, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import Sidebar from './sidebar';
import { EditIcon } from './icons';
import CONFIG from './fields-config.json';
const Overview = (props) => {
var data_layer = 'age'; // always default
var dataLayer = 'age'; // always default
if (props.match && props.match.params && props.match.params.cat) {
data_layer = props.match.params.cat;
dataLayer = props.match.params.cat;
}
if (props.mode === 'edit' && !props.user){
@ -16,22 +17,28 @@ const Overview = (props) => {
}
let title = (props.mode === 'view')? 'View maps' : 'Add or edit data';
let back = (props.mode === 'edit')? `/view/${data_layer}.html` : undefined;
let back = (props.mode === 'edit')? `/view/${dataLayer}.html` : undefined;
return (
<Sidebar title={title} back={back}>
{
CONFIG.map((data_group) => (
<OverviewSection {...data_group}
data_layer={data_layer} key={data_group.slug} mode={props.mode} />
CONFIG.map((dataGroup) => (
<OverviewSection {...dataGroup}
dataLayer={dataLayer} key={dataGroup.slug} mode={props.mode} />
))
}
</Sidebar>
);
}
Overview.propTypes = {
match: PropTypes.object,
mode: PropTypes.string,
user: PropTypes.object
}
const OverviewSection = (props) => {
const match = props.data_layer === props.slug;
const match = props.dataLayer === props.slug;
const inactive = props.inactive;
return (
@ -83,4 +90,18 @@ const OverviewSection = (props) => {
)
};
OverviewSection.propTypes = {
title: PropTypes.string,
slug: PropTypes.string,
intro: PropTypes.string,
help: PropTypes.string,
dataLayer: PropTypes.string,
mode: PropTypes.string,
inactive: PropTypes.bool,
fields: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
slug: PropTypes.string
}))
}
export default Overview;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './search-box.css';
@ -26,7 +27,7 @@ class SearchBox extends Component {
this.setState({
q: e.target.value
});
// If the clear icon has been clicked, clear results list as well
// If the clear icon has been clicked, clear results list as well
if(e.target.value === '') {
this.clearResults();
}
@ -107,7 +108,7 @@ class SearchBox extends Component {
this.props.onLocate(lat, lng, zoom);
}}
href={href}
>{`${label.substring(0,4)} ${label.substring(4, 7)}`}</a>
>{`${label.substring(0, 4)} ${label.substring(4, 7)}`}</a>
</li>
)
})
@ -115,7 +116,7 @@ class SearchBox extends Component {
</ul>
: null;
return (
<div className={`search-box ${this.props.is_building? 'building' : ''}`} onKeyDown={this.handleKeyPress}>
<div className={`search-box ${this.props.isBuilding? 'building' : ''}`} onKeyDown={this.handleKeyPress}>
<form action="/search" method="GET" onSubmit={this.search}
className="form-inline">
<input
@ -136,4 +137,9 @@ class SearchBox extends Component {
}
}
SearchBox.propTypes = {
onLocate: PropTypes.func,
isBuilding: PropTypes.bool
}
export default SearchBox;

View File

@ -1,5 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import './sidebar.css';
import { BackIcon } from './icons';
@ -20,4 +21,10 @@ const Sidebar = (props) => (
</div>
);
Sidebar.propTypes = {
back: PropTypes.string,
title: PropTypes.string.isRequired,
children: PropTypes.node
}
export default Sidebar;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Redirect, Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import ErrorBox from './error-box';
import InfoBox from './info-box';
@ -156,4 +157,9 @@ class SignUp extends Component {
}
}
SignUp.propTypes = {
login: PropTypes.func.isRequired,
user: PropTypes.object
}
export default SignUp;

View File

@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import './theme-switcher.css';
@ -11,4 +12,9 @@ const ThemeSwitcher = (props) => (
</form>
);
ThemeSwitcher.propTypes = {
currentTheme: PropTypes.string,
onSubmit: PropTypes.func.isRequired
}
export default ThemeSwitcher;

View File

@ -1,4 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './tooltip.css';
import { InfoIcon } from './icons';
@ -42,4 +43,9 @@ class Tooltip extends Component {
);
}
}
Tooltip.propTypes = {
text: PropTypes.string
}
export default Tooltip;

View File

@ -39,12 +39,12 @@ function parseBuildingURL(url) {
* @returns {String} [age]
*/
function parseCategoryURL(url) {
const default_cat = 'age';
const defaultCat = 'age';
if (url === '/') {
return default_cat
return defaultCat;
}
const matches = /^\/(view|edit)\/([^/.]+)/.exec(url);
const cat = (matches && matches.length >= 3) ? matches[2] : default_cat;
const cat = (matches && matches.length >= 3) ? matches[2] : defaultCat;
return cat;
}

View File

@ -76,29 +76,29 @@ function frontendRoute(req, res) {
const data = {};
context.status = 200;
const user_id = req.session.user_id;
const building_id = parseBuildingURL(req.url);
const is_building = (typeof (building_id) !== 'undefined');
if (is_building && isNaN(building_id)) {
const userId = req.session.user_id;
const buildingId = parseBuildingURL(req.url);
const isBuilding = (typeof (buildingId) !== 'undefined');
if (isBuilding && isNaN(buildingId)) {
context.status = 404;
}
Promise.all([
user_id ? getUserById(user_id) : undefined,
is_building ? getBuildingById(building_id) : undefined,
is_building ? getBuildingUPRNsById(building_id) : undefined,
(is_building && user_id) ? getBuildingLikeById(building_id, user_id) : false
userId ? getUserById(userId) : undefined,
isBuilding ? getBuildingById(buildingId) : undefined,
isBuilding ? getBuildingUPRNsById(buildingId) : undefined,
(isBuilding && userId) ? getBuildingLikeById(buildingId, userId) : false
]).then(function (values) {
const user = values[0];
const building = values[1];
const uprns = values[2];
const building_like = values[3];
if (is_building && typeof (building) === 'undefined') {
const buildingLike = values[3];
if (isBuilding && typeof (building) === 'undefined') {
context.status = 404
}
data.user = user;
data.building = building;
data.building_like = building_like;
data.building_like = buildingLike;
if (data.building != null) {
data.building.uprns = uprns;
}
@ -218,10 +218,10 @@ server.route('/building/:building_id.json')
}
})
function updateBuilding(req, res, user_id) {
function updateBuilding(req, res, userId) {
const { building_id } = req.params;
const building = req.body;
saveBuilding(building_id, building, user_id).then(building => {
saveBuilding(building_id, building, userId).then(building => {
if (building.error) {
res.send(building)
return
@ -384,8 +384,8 @@ server.post('/api/key', function (req, res) {
return
}
getNewUserAPIKey(req.session.user_id).then(function (api_key) {
res.send(api_key);
getNewUserAPIKey(req.session.user_id).then(function (apiKey) {
res.send(apiKey);
}).catch(function (error) {
res.send(error);
});
@ -393,14 +393,14 @@ server.post('/api/key', function (req, res) {
// GET search
server.get('/search', function (req, res) {
const search_term = req.query.q;
if (!search_term) {
const searchTerm = req.query.q;
if (!searchTerm) {
res.send({
error: 'Please provide a search term'
})
return
}
queryLocation(search_term).then((results) => {
queryLocation(searchTerm).then((results) => {
if (typeof (results) === 'undefined') {
res.send({
error: 'Database error'

View File

@ -18,7 +18,7 @@
// and then use stdlib `import fs from 'fs';`
import fs from 'node-fs';
import { get_xyz } from './tile';
import { getXYZ } from './tile';
// Use an environment variable to configure the cache location, somewhere we can read/write to.
const CACHE_PATH = process.env.TILECACHE_PATH
@ -32,10 +32,10 @@ const CACHE_PATH = process.env.TILECACHE_PATH
* @param {number} y
*/
function get(tileset, z, x, y) {
if (!should_try_cache(tileset, z)) {
if (!shouldTryCache(tileset, z)) {
return Promise.reject(`Skip cache get ${tileset}/${z}/${x}/${y}`);
}
const location = cache_location(tileset, z, x, y);
const location = cacheLocation(tileset, z, x, y);
return new Promise((resolve, reject) => {
fs.readFile(location.fname, (err, data) => {
if (err) {
@ -57,10 +57,10 @@ function get(tileset, z, x, y) {
* @param {number} y
*/
function put(im, tileset, z, x, y) {
if (!should_try_cache(tileset, z)) {
if (!shouldTryCache(tileset, z)) {
return Promise.reject(`Skip cache put ${tileset}/${z}/${x}/${y}`);
}
const location = cache_location(tileset, z, x, y);
const location = cacheLocation(tileset, z, x, y);
return new Promise((resolve, reject) => {
fs.writeFile(location.fname, im, 'binary', (err) => {
if (err && err.code === 'ENOENT') {
@ -91,7 +91,7 @@ function put(im, tileset, z, x, y) {
* @param {number} y
*/
function remove(tileset, z, x, y) {
const location = cache_location(tileset, z, x, y)
const location = cacheLocation(tileset, z, x, y)
return new Promise(resolve => {
fs.unlink(location.fname, (err) => {
if(err){
@ -111,26 +111,26 @@ function remove(tileset, z, x, y) {
* @param {String} tileset
* @param {Array} bbox [w, s, e, n] in EPSG:3857 coordinates
*/
function remove_all_at_bbox(bbox) {
function removeAllAtBbox(bbox) {
// magic numbers for min/max zoom
const min_zoom = 9;
const max_zoom = 18;
const minZoom = 9;
const maxZoom = 18;
// magic list of tilesets - see tileserver, other cache rules
const tilesets = ['date_year', 'size_storeys', 'location', 'likes', 'conservation_area'];
let tile_bounds;
const remove_promises = [];
let tileBounds;
const removePromises = [];
for (let ti = 0; ti < tilesets.length; ti++) {
const tileset = tilesets[ti];
for (let z = min_zoom; z <= max_zoom; z++) {
tile_bounds = get_xyz(bbox, z)
for (let x = tile_bounds.minX; x <= tile_bounds.maxX; x++){
for (let y = tile_bounds.minY; y <= tile_bounds.maxY; y++){
remove_promises.push(remove(tileset, z, x, y))
for (let z = minZoom; z <= maxZoom; z++) {
tileBounds = getXYZ(bbox, z)
for (let x = tileBounds.minX; x <= tileBounds.maxX; x++){
for (let y = tileBounds.minY; y <= tileBounds.maxY; y++){
removePromises.push(remove(tileset, z, x, y))
}
}
}
}
Promise.all(remove_promises)
Promise.all(removePromises)
}
/**
@ -142,7 +142,7 @@ function remove_all_at_bbox(bbox) {
* @param {number} y
* @returns {object} { dir: <directory>, fname: <full filepath> }
*/
function cache_location(tileset, z, x, y) {
function cacheLocation(tileset, z, x, y) {
const dir = `${CACHE_PATH}/${tileset}/${z}/${x}`
const fname = `${dir}/${y}.png`
return {dir, fname}
@ -155,7 +155,7 @@ function cache_location(tileset, z, x, y) {
* @param {number} z zoom level
* @returns {boolean} whether to use the cache (or not)
*/
function should_try_cache(tileset, z) {
function shouldTryCache(tileset, z) {
if (tileset === 'date_year') {
// cache high zoom because of front page hits
return z <= 16
@ -168,4 +168,4 @@ function should_try_cache(tileset, z) {
return z <= 13
}
export { get, put, remove, remove_all_at_bbox };
export { get, put, remove, removeAllAtBbox };

View File

@ -33,11 +33,11 @@ const PROJ4_STRING = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x
// Mapnik uses table definitions to query geometries and attributes from PostGIS.
// The queries here are eventually used as subqueries when Mapnik fetches data to render a
// tile - so given a table_definition like:
// (SELECT geometry_geom FROM geometries) as my_table_definition
// tile - so given a table definition like:
// (SELECT geometry_geom FROM geometries) as def
// Mapnik will wrap it in a bbox query and PostGIS will eventually see something like:
// SELECT AsBinary("geometry") AS geom from
// (SELECT geometry_geom FROM geometries) as my_table_definition
// (SELECT geometry_geom FROM geometries) as def
// WHERE "geometry" && SetSRID('BOX3D(0,1,2,3)'::box3d, 3857)
// see docs: https://github.com/mapnik/mapnik/wiki/OptimizeRenderingWithPostGIS
const MAP_STYLE_TABLE_DEFINITIONS = {
@ -139,26 +139,26 @@ const mercator = new SphericalMercator({
size: TILE_SIZE
});
function get_bbox(z, x, y) {
function getBbox(z, x, y) {
return mercator.bbox(x, y, z, false, '900913');
}
function get_xyz(bbox, z) {
function getXYZ(bbox, z) {
return mercator.xyz(bbox, z, false, '900913')
}
function render_tile(tileset, z, x, y, geometry_id, cb) {
const bbox = get_bbox(z, x, y)
function renderTile(tileset, z, x, y, geometryId, cb) {
const bbox = getBbox(z, x, y)
const map = new mapnik.Map(TILE_SIZE, TILE_SIZE, PROJ4_STRING);
map.bufferSize = TILE_BUFFER_SIZE;
const layer = new mapnik.Layer('tile', PROJ4_STRING);
const table_def = (tileset === 'highlight') ?
get_highlight_table_def(geometry_id)
const tableDefinition = (tileset === 'highlight') ?
getHighlightTableDefinition(geometryId)
: MAP_STYLE_TABLE_DEFINITIONS[tileset];
const conf = Object.assign({ table: table_def }, DATASOURCE_CONFIG)
const conf = Object.assign({ table: tableDefinition }, DATASOURCE_CONFIG)
var postgis;
try {
@ -186,17 +186,17 @@ function render_tile(tileset, z, x, y, geometry_id, cb) {
}
}
// highlight single geometry, requires geometry_id in the table query
function get_highlight_table_def(geometry_id) {
// highlight single geometry, requires geometryId in the table query
function getHighlightTableDefinition(geometryId) {
return `(
SELECT
g.geometry_geom
FROM
geometries as g
WHERE
g.geometry_id = ${geometry_id}
g.geometry_id = ${geometryId}
) as highlight`
}
export { get_bbox, get_xyz, render_tile, TILE_SIZE };
export { getBbox, getXYZ, renderTile, TILE_SIZE };

View File

@ -9,7 +9,7 @@ import express from 'express';
import sharp from 'sharp';
import { get, put } from './cache';
import { render_tile, get_bbox, get_xyz, TILE_SIZE } from './tile';
import { renderTile, getBbox, getXYZ, TILE_SIZE } from './tile';
import { strictParseInt } from '../parse';
// zoom level when we switch from rendering direct from database to instead composing tiles
@ -23,48 +23,48 @@ const EXTENT_BBOX = [-61149.622628, 6667754.851372, 28128.826409, 6744803.375884
// tiles router
const router = express.Router()
router.get('/highlight/:z/:x/:y.png', handle_highlight_tile_request);
router.get('/highlight/:z/:x/:y.png', handleHighlightTileRequest);
router.get('/base_light/:z/:x/:y.png', (req, res) => {
handle_tile_request('base_light', req, res)
handleTileRequest('base_light', req, res)
});
router.get('/base_night/:z/:x/:y.png', (req, res) => {
handle_tile_request('base_night', req, res)
handleTileRequest('base_night', req, res)
});
router.get('/date_year/:z/:x/:y.png', (req, res) => {
handle_tile_request('date_year', req, res)
handleTileRequest('date_year', req, res)
});
router.get('/size_storeys/:z/:x/:y.png', (req, res) => {
handle_tile_request('size_storeys', req, res)
handleTileRequest('size_storeys', req, res)
});
router.get('/location/:z/:x/:y.png', (req, res) => {
handle_tile_request('location', req, res)
handleTileRequest('location', req, res)
});
router.get('/likes/:z/:x/:y.png', (req, res) => {
handle_tile_request('likes', req, res)
handleTileRequest('likes', req, res)
});
router.get('/conservation_area/:z/:x/:y.png', (req, res) => {
handle_tile_request('conservation_area', req, res)
handleTileRequest('conservation_area', req, res)
});
function handle_tile_request(tileset, req, res) {
function handleTileRequest(tileset, req, res) {
const { z, x, y } = req.params
const int_z = strictParseInt(z);
const int_x = strictParseInt(x);
const int_y = strictParseInt(y);
const intZ = strictParseInt(z);
const intX = strictParseInt(x);
const intY = strictParseInt(y);
if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)) {
if (isNaN(intX) || isNaN(intY) || isNaN(intZ)) {
console.error('Missing x or y or z')
return { error: 'Bad parameter' }
}
load_tile(tileset, int_z, int_x, int_y).then((im) => {
loadTile(tileset, intZ, intX, intY).then((im) => {
res.writeHead(200, { 'Content-Type': 'image/png' })
res.end(im)
}).catch((err) => {
@ -73,21 +73,21 @@ function handle_tile_request(tileset, req, res) {
})
}
function load_tile(tileset, z, x, y) {
if (outside_extent(z, x, y)) {
return empty_tile()
function loadTile(tileset, z, x, y) {
if (outsideExtent(z, x, y)) {
return emptyTile()
}
return get(tileset, z, x, y).then((im) => {
console.log(`From cache ${tileset}/${z}/${x}/${y}`)
return im
}).catch(() => {
return render_or_stitch_tile(tileset, z, x, y)
return renderOrStitchTile(tileset, z, x, y)
})
}
function render_or_stitch_tile(tileset, z, x, y) {
function renderOrStitchTile(tileset, z, x, y) {
if (z <= STITCH_THRESHOLD) {
return stitch_tile(tileset, z, x, y).then(im => {
return StitchTile(tileset, z, x, y).then(im => {
return put(im, tileset, z, x, y).then(() => {
console.log(`Stitch ${tileset}/${z}/${x}/${y}`)
return im
@ -99,7 +99,7 @@ function render_or_stitch_tile(tileset, z, x, y) {
} else {
return new Promise((resolve, reject) => {
render_tile(tileset, z, x, y, undefined, (err, im) => {
renderTile(tileset, z, x, y, undefined, (err, im) => {
if (err) {
reject(err)
return
@ -116,12 +116,12 @@ function render_or_stitch_tile(tileset, z, x, y) {
}
}
function outside_extent(z, x, y) {
const xy = get_xyz(EXTENT_BBOX, z);
function outsideExtent(z, x, y) {
const xy = getXYZ(EXTENT_BBOX, z);
return xy.minY > y || xy.maxY < y || xy.minX > x || xy.maxX < x;
}
function empty_tile() {
function emptyTile() {
return sharp({
create: {
width: 1,
@ -132,22 +132,22 @@ function empty_tile() {
}).png().toBuffer()
}
function stitch_tile(tileset, z, x, y) {
const bbox = get_bbox(z, x, y)
const next_z = z + 1
const next_xy = get_xyz(bbox, next_z)
function StitchTile(tileset, z, x, y) {
const bbox = getBbox(z, x, y)
const nextZ = z + 1
const nextXY = getXYZ(bbox, nextZ)
return Promise.all([
// recurse down through zoom levels, using cache if available...
load_tile(tileset, next_z, next_xy.minX, next_xy.minY),
load_tile(tileset, next_z, next_xy.maxX, next_xy.minY),
load_tile(tileset, next_z, next_xy.minX, next_xy.maxY),
load_tile(tileset, next_z, next_xy.maxX, next_xy.maxY)
loadTile(tileset, nextZ, nextXY.minX, nextXY.minY),
loadTile(tileset, nextZ, nextXY.maxX, nextXY.minY),
loadTile(tileset, nextZ, nextXY.minX, nextXY.maxY),
loadTile(tileset, nextZ, nextXY.maxX, nextXY.maxY)
]).then(([
top_left,
top_right,
bottom_left,
bottom_right
topLeft,
topRight,
bottomLeft,
bottomRight
]) => {
// not possible to chain overlays in a single pipeline, but there may still be a better
// way to create image buffer here (four tiles resize to one at the next zoom level)
@ -160,18 +160,18 @@ function stitch_tile(tileset, z, x, y) {
background: { r: 0, g: 0, b: 0, alpha: 0 }
}
}).overlayWith(
top_left, { gravity: sharp.gravity.northwest }
topLeft, { gravity: sharp.gravity.northwest }
).png().toBuffer().then((buf) => {
return sharp(buf).overlayWith(
top_right, { gravity: sharp.gravity.northeast }
topRight, { gravity: sharp.gravity.northeast }
).png().toBuffer()
}).then((buf) => {
return sharp(buf).overlayWith(
bottom_left, { gravity: sharp.gravity.southwest }
bottomLeft, { gravity: sharp.gravity.southwest }
).png().toBuffer()
}).then((buf) => {
return sharp(buf).overlayWith(
bottom_right, { gravity: sharp.gravity.southeast }
bottomRight, { gravity: sharp.gravity.southeast }
).png().toBuffer()
}).then((buf) => {
return sharp(buf
@ -182,30 +182,30 @@ function stitch_tile(tileset, z, x, y) {
}
function handle_highlight_tile_request(req, res) {
function handleHighlightTileRequest(req, res) {
const { z, x, y } = req.params
const int_z = strictParseInt(z);
const int_x = strictParseInt(x);
const int_y = strictParseInt(y);
const intZ = strictParseInt(z);
const intX = strictParseInt(x);
const intY = strictParseInt(y);
if (isNaN(int_x) || isNaN(int_y) || isNaN(int_z)) {
if (isNaN(intX) || isNaN(intY) || isNaN(intZ)) {
console.error('Missing x or y or z')
return { error: 'Bad parameter' }
}
// highlight layer uses geometry_id to outline a single building
const { highlight } = req.query
const geometry_id = strictParseInt(highlight);
if (isNaN(geometry_id)) {
const geometryId = strictParseInt(highlight);
if (isNaN(geometryId)) {
res.status(400).send({ error: 'Bad parameter' })
return
}
if (outside_extent(z, x, y)) {
return empty_tile()
if (outsideExtent(z, x, y)) {
return emptyTile()
}
render_tile('highlight', int_z, int_x, int_y, geometry_id, function (err, im) {
renderTile('highlight', intZ, intX, intY, geometryId, function (err, im) {
if (err) {throw err}
res.writeHead(200, { 'Content-Type': 'image/png' })