diff --git a/app/.eslintrc b/app/.eslintrc index d56722dc..28a06615 100644 --- a/app/.eslintrc +++ b/app/.eslintrc @@ -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": { diff --git a/app/package-lock.json b/app/package-lock.json index e301c4ce..08fbf6d2 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -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", diff --git a/app/package.json b/app/package.json index 62fce878..56533ff4 100644 --- a/app/package.json +++ b/app/package.json @@ -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" } diff --git a/app/src/api/building.js b/app/src/api/building.js index 72e0428c..1cf72d93 100644 --- a/app/src/api/building.js +++ b/app/src/api/building.js @@ -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 { diff --git a/app/src/api/search.js b/app/src/api/search.js index 1aed1835..08fbebe3 100644 --- a/app/src/api/search.js +++ b/app/src/api/search.js @@ -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; diff --git a/app/src/api/user.js b/app/src/api/user.js index 0e0995e0..cd653d0c 100644 --- a/app/src/api/user.js +++ b/app/src/api/user.js @@ -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) diff --git a/app/src/frontend/app.js b/app/src/frontend/app.js index d0ac4f3f..c6b1e98a 100644 --- a/app/src/frontend/app.js +++ b/app/src/frontend/app.js @@ -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 */ diff --git a/app/src/frontend/building-edit.js b/app/src/frontend/building-edit.js index 1c7f28e8..446560ab 100644 --- a/app/src/frontend/building-edit.js +++ b/app/src/frontend/building-edit.js @@ -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 + {...section} {...props} + cat={cat} key={section.slug} /> }) } ); } +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 (
@@ -235,7 +242,7 @@ class EditForm extends Component { value={this.state[props.slug]} key={props.slug} /> case 'like': return 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) => ( ); +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) => ( ) +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) => ( ) +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) => ( ); +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) => (
(
) +CheckboxInput.propTypes = { + slug: PropTypes.string, + title: PropTypes.string, + tooltip: PropTypes.string, + value: PropTypes.bool, + disabled: PropTypes.bool, + handleChange: PropTypes.func +} + const LikeButton = (props) => (

{(props.value)? props.value : 0} likes

@@ -441,11 +533,27 @@ const LikeButton = (props) => (
); +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.propTypes = { + slug: PropTypes.string, + title: PropTypes.string, + tooltip: PropTypes.string +} export default BuildingEdit; diff --git a/app/src/frontend/building-view.js b/app/src/frontend/building-view.js index 298a3ee9..24ff222f 100644 --- a/app/src/frontend/building-view.js +++ b/app/src/frontend/building-view.js @@ -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 ( { - CONFIG.map(section_props => ( + CONFIG.map(section => ( + {...section}> { - section_props.fields.map(field_props => { + section.fields.map(field => { - switch (field_props.type) { + switch (field.type) { case 'uprn_list': return + tooltip={field.tooltip} /> case 'text_multi': return + key={field.slug} + title={field.title} + value={props[field.slug]} + tooltip={field.tooltip} /> case 'like': return + tooltip={field.tooltip} /> default: return + 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) => (
@@ -128,6 +149,12 @@ const DataEntry = (props) => ( ); +DataEntry.propTypes = { + title: PropTypes.string, + tooltip: PropTypes.string, + value: PropTypes.any +} + const LikeDataEntry = (props) => (
@@ -149,13 +176,20 @@ const LikeDataEntry = (props) => ( ); +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 =
    { props.value.map((item, index) => { - return
  • {item}
  • + return
  • {item}
  • }) }
} 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 ( @@ -222,18 +262,18 @@ const UPRNsDataEntry = (props) => {
    { - no_parent.length? - no_parent.map(uprn => ( + noParent.length? + noParent.map(uprn => (
  • {uprn.uprn}
  • )) : '\u00A0' }
    { - with_parent.length? + withParent.length?
    Children { - with_parent.map(uprn => ( + withParent.map(uprn => (
  • {uprn.uprn} (child of {uprn.parent_uprn})
  • )) } @@ -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; diff --git a/app/src/frontend/error-box.js b/app/src/frontend/error-box.js index 9c558110..0e51e3f1 100644 --- a/app/src/frontend/error-box.js +++ b/app/src/frontend/error-box.js @@ -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; diff --git a/app/src/frontend/header.js b/app/src/frontend/header.js index 4aea0fce..c9dd5f7c 100644 --- a/app/src/frontend/header.js +++ b/app/src/frontend/header.js @@ -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; diff --git a/app/src/frontend/info-box.js b/app/src/frontend/info-box.js index db45b54d..0185b561 100644 --- a/app/src/frontend/info-box.js +++ b/app/src/frontend/info-box.js @@ -1,4 +1,5 @@ import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; const InfoBox = (props) => ( @@ -20,4 +21,9 @@ const InfoBox = (props) => ( ); +InfoBox.propTypes = { + msg: PropTypes.string, + children: PropTypes.node +} + export default InfoBox; diff --git a/app/src/frontend/legend.js b/app/src/frontend/legend.js index 50f7b427..363e7427 100644 --- a/app/src/frontend/legend.js +++ b/app/src/frontend/legend.js @@ -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? - (
      +
        { - elements.map((data_item) => ( - + elements.map((item) => ( + )) } -
      ) - : (

      Coming soon…

      ) +
    + :

    Coming soon…

    } ); } +Legend.propTypes = { + slug: PropTypes.string +} + const LegendItem = (props) => (
  • - @@ -156,4 +161,9 @@ const LegendItem = (props) => (
  • ); +LegendItem.propTypes = { + color: PropTypes.string, + text: PropTypes.string +} + export default Legend; diff --git a/app/src/frontend/login.js b/app/src/frontend/login.js index f32864e8..f1a6d909 100644 --- a/app/src/frontend/login.js +++ b/app/src/frontend/login.js @@ -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; diff --git a/app/src/frontend/map.js b/app/src/frontend/map.js index 7fc97e9a..2ce1f243 100644 --- a/app/src/frontend/map.js +++ b/app/src/frontend/map.js @@ -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? : 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) ? : 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} > - + { dataLayer } { highlightLayer } { - !is_building && this.props.match.url !== '/'? ( + !isBuilding && this.props.match.url !== '/'? (
    - {is_edit? 'Click a building to edit' : 'Click a building for details'} + {isEdit? 'Click a building to edit' : 'Click a building for details'}
    ) : null } @@ -146,7 +147,7 @@ class ColouringMap extends Component { - + ) : 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; diff --git a/app/src/frontend/my-account.js b/app/src/frontend/my-account.js index bf44f426..ba697e26 100644 --- a/app/src/frontend/my-account.js +++ b/app/src/frontend/my-account.js @@ -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; diff --git a/app/src/frontend/overview.js b/app/src/frontend/overview.js index 8d55ee63..52c2230b 100644 --- a/app/src/frontend/overview.js +++ b/app/src/frontend/overview.js @@ -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 ( { - CONFIG.map((data_group) => ( - + CONFIG.map((dataGroup) => ( + )) } ); } +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; diff --git a/app/src/frontend/search-box.js b/app/src/frontend/search-box.js index 237da3eb..fdb3a1c0 100644 --- a/app/src/frontend/search-box.js +++ b/app/src/frontend/search-box.js @@ -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)}`} + >{`${label.substring(0, 4)} ${label.substring(4, 7)}`} ) }) @@ -115,7 +116,7 @@ class SearchBox extends Component {
: null; return ( -
+
(
); +Sidebar.propTypes = { + back: PropTypes.string, + title: PropTypes.string.isRequired, + children: PropTypes.node +} + export default Sidebar; diff --git a/app/src/frontend/signup.js b/app/src/frontend/signup.js index 2dc83054..06575812 100644 --- a/app/src/frontend/signup.js +++ b/app/src/frontend/signup.js @@ -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; diff --git a/app/src/frontend/theme-switcher.js b/app/src/frontend/theme-switcher.js index 6252a999..3d87478d 100644 --- a/app/src/frontend/theme-switcher.js +++ b/app/src/frontend/theme-switcher.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import './theme-switcher.css'; @@ -11,4 +12,9 @@ const ThemeSwitcher = (props) => ( ); +ThemeSwitcher.propTypes = { + currentTheme: PropTypes.string, + onSubmit: PropTypes.func.isRequired +} + export default ThemeSwitcher; diff --git a/app/src/frontend/tooltip.js b/app/src/frontend/tooltip.js index fd4c1821..6bc30601 100644 --- a/app/src/frontend/tooltip.js +++ b/app/src/frontend/tooltip.js @@ -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; diff --git a/app/src/parse.js b/app/src/parse.js index 7eaedce8..87a20ff0 100644 --- a/app/src/parse.js +++ b/app/src/parse.js @@ -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; } diff --git a/app/src/server.js b/app/src/server.js index 889f0055..c02f2188 100644 --- a/app/src/server.js +++ b/app/src/server.js @@ -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' diff --git a/app/src/tiles/cache.js b/app/src/tiles/cache.js index c76b3f2e..f018e444 100644 --- a/app/src/tiles/cache.js +++ b/app/src/tiles/cache.js @@ -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: , fname: } */ -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 }; diff --git a/app/src/tiles/tile.js b/app/src/tiles/tile.js index 99beb819..1ff3d1b2 100644 --- a/app/src/tiles/tile.js +++ b/app/src/tiles/tile.js @@ -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 }; diff --git a/app/src/tiles/tileserver.js b/app/src/tiles/tileserver.js index 96d29dba..dd9a5ac3 100644 --- a/app/src/tiles/tileserver.js +++ b/app/src/tiles/tileserver.js @@ -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' })