Rework split between app/map-app/map
This commit is contained in:
parent
6625099c03
commit
b9648c47af
@ -1,31 +1,30 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Route, Switch, Link } from 'react-router-dom';
|
import { Route, Switch, Link } from 'react-router-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { parse } from 'query-string';
|
|
||||||
|
|
||||||
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
import '../../node_modules/bootstrap/dist/css/bootstrap.min.css';
|
||||||
import './app.css';
|
import './app.css';
|
||||||
|
|
||||||
import BuildingView from './building/building-view';
|
|
||||||
import ColouringMap from './map/map';
|
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import MultiEdit from './building/multi-edit';
|
|
||||||
import Categories from './building/categories';
|
|
||||||
import Sidebar from './building/sidebar';
|
|
||||||
|
|
||||||
import AboutPage from './pages/about';
|
import AboutPage from './pages/about';
|
||||||
import ContributorAgreementPage from './pages/contributor-agreement';
|
import ContributorAgreementPage from './pages/contributor-agreement';
|
||||||
import PrivacyPolicyPage from './pages/privacy-policy';
|
import PrivacyPolicyPage from './pages/privacy-policy';
|
||||||
import Welcome from './pages/welcome';
|
|
||||||
|
|
||||||
import Login from './user/login';
|
import Login from './user/login';
|
||||||
import MyAccountPage from './user/my-account';
|
import MyAccountPage from './user/my-account';
|
||||||
import SignUp from './user/signup';
|
import SignUp from './user/signup';
|
||||||
|
|
||||||
import ForgottenPassword from './user/forgotten-password';
|
import ForgottenPassword from './user/forgotten-password';
|
||||||
import PasswordReset from './user/password-reset';
|
import PasswordReset from './user/password-reset';
|
||||||
|
|
||||||
import { parseCategoryURL } from '../parse';
|
import MapApp from './map-app';
|
||||||
|
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
user?: any;
|
||||||
|
building?: any;
|
||||||
|
building_like?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App component
|
* App component
|
||||||
@ -39,35 +38,28 @@ import { parseCategoryURL } from '../parse';
|
|||||||
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
* map or other pages are rendered, based on the URL. Use a react-router-dom <Link /> in
|
||||||
* child components to navigate without a full page reload.
|
* child components to navigate without a full page reload.
|
||||||
*/
|
*/
|
||||||
class App extends React.Component<any, any> { // TODO: add proper types
|
class App extends React.Component<AppProps, any> { // TODO: add proper types
|
||||||
static propTypes = { // TODO: generate propTypes from TS
|
static propTypes = { // TODO: generate propTypes from TS
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
building: PropTypes.object,
|
building: PropTypes.object,
|
||||||
building_like: PropTypes.bool
|
building_like: PropTypes.bool
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: Readonly<AppProps>) {
|
||||||
super(props);
|
super(props);
|
||||||
// set building revision id, default 0
|
|
||||||
const rev = (props.building)? +props.building.revision_id : 0;
|
|
||||||
this.state = {
|
this.state = {
|
||||||
user: props.user,
|
user: props.user
|
||||||
building: props.building,
|
|
||||||
building_like: props.building_like,
|
|
||||||
revision_id: rev
|
|
||||||
};
|
};
|
||||||
this.login = this.login.bind(this);
|
this.login = this.login.bind(this);
|
||||||
this.updateUser = this.updateUser.bind(this);
|
this.updateUser = this.updateUser.bind(this);
|
||||||
this.logout = this.logout.bind(this);
|
this.logout = this.logout.bind(this);
|
||||||
this.selectBuilding = this.selectBuilding.bind(this);
|
|
||||||
this.colourBuilding = this.colourBuilding.bind(this);
|
|
||||||
this.increaseRevision = this.increaseRevision.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
login(user) {
|
login(user) {
|
||||||
if (user.error) {
|
if (user.error) {
|
||||||
this.logout();
|
this.logout();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
this.setState({user: user});
|
this.setState({user: user});
|
||||||
}
|
}
|
||||||
@ -80,171 +72,12 @@ class App extends React.Component<any, any> { // TODO: add proper types
|
|||||||
this.setState({user: undefined});
|
this.setState({user: undefined});
|
||||||
}
|
}
|
||||||
|
|
||||||
increaseRevision(revisionId) {
|
|
||||||
revisionId = +revisionId;
|
|
||||||
// bump revision id, only ever increasing
|
|
||||||
if (revisionId > this.state.revision_id){
|
|
||||||
this.setState({revision_id: revisionId})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectBuilding(building) {
|
|
||||||
this.increaseRevision(building.revision_id);
|
|
||||||
// get UPRNs and update
|
|
||||||
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then((res) => {
|
|
||||||
if (res.error) {
|
|
||||||
console.error(res);
|
|
||||||
} else {
|
|
||||||
building.uprns = res.uprns;
|
|
||||||
this.setState({building: building});
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({building: building});
|
|
||||||
});
|
|
||||||
|
|
||||||
// get if liked and update
|
|
||||||
fetch(`/api/buildings/${building.building_id}/like.json`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then((res) => {
|
|
||||||
if (res.error) {
|
|
||||||
console.error(res);
|
|
||||||
} else {
|
|
||||||
this.setState({building_like: res.like});
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err)
|
|
||||||
this.setState({building_like: false});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Colour building
|
|
||||||
*
|
|
||||||
* Used in multi-edit mode to colour buildings on map click
|
|
||||||
*
|
|
||||||
* Pulls data from URL to form update
|
|
||||||
*
|
|
||||||
* @param {object} building
|
|
||||||
*/
|
|
||||||
colourBuilding(building) {
|
|
||||||
const cat = parseCategoryURL(window.location.pathname);
|
|
||||||
const q = parse(window.location.search);
|
|
||||||
const data = (cat === 'like')? {like: true}: JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
|
|
||||||
if (cat === 'like'){
|
|
||||||
this.likeBuilding(building.building_id)
|
|
||||||
} else {
|
|
||||||
this.updateBuilding(building.building_id, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
likeBuilding(buildingId) {
|
|
||||||
fetch(`/api/buildings/${buildingId}/like.json`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({like: true})
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then(function(res){
|
|
||||||
if (res.error) {
|
|
||||||
console.error({error: res.error})
|
|
||||||
} else {
|
|
||||||
this.increaseRevision(res.revision_id);
|
|
||||||
}
|
|
||||||
}.bind(this)).catch(
|
|
||||||
(err) => console.error({error: err})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBuilding(buildingId, data){
|
|
||||||
fetch(`/api/buildings/${buildingId}.json`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
headers:{
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin'
|
|
||||||
}).then(
|
|
||||||
res => res.json()
|
|
||||||
).then(res => {
|
|
||||||
if (res.error) {
|
|
||||||
console.error({error: res.error})
|
|
||||||
} else {
|
|
||||||
this.increaseRevision(res.revision_id);
|
|
||||||
}
|
|
||||||
}).catch(
|
|
||||||
(err) => console.error({error: err})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const building_id = (this.state.building)?
|
|
||||||
this.state.building.building_id
|
|
||||||
: 2503371 // Default to UCL main building. TODO use last selected if any
|
|
||||||
const building = this.state.building;
|
|
||||||
const building_like = this.state.building_like;
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Header user={this.state.user} />
|
<Header user={this.state.user} />
|
||||||
<main>
|
<main>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/">
|
|
||||||
<Welcome />
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/view/categories.html">
|
|
||||||
<Sidebar>
|
|
||||||
<Categories mode="view" building_id={building_id} />
|
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/edit/categories.html">
|
|
||||||
<Sidebar>
|
|
||||||
<Categories mode="edit" building_id={building_id} />
|
|
||||||
</Sidebar>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/multi-edit/:cat.html" render={(props) => (
|
|
||||||
<MultiEdit
|
|
||||||
{...props}
|
|
||||||
user={this.state.user}
|
|
||||||
/>
|
|
||||||
) } />
|
|
||||||
<Route exact path="/:mode/:cat/building/:building.html" render={(props) => (
|
|
||||||
<BuildingView
|
|
||||||
mode={props.match.params.mode}
|
|
||||||
cat={props.match.params.cat}
|
|
||||||
building={this.state.building}
|
|
||||||
building_like={this.state.building_like}
|
|
||||||
selectBuilding={this.selectBuilding}
|
|
||||||
user={this.state.user}
|
|
||||||
/>
|
|
||||||
) } />
|
|
||||||
</Switch>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/(multi-edit.*|edit.*|view.*)?" render={(props) => (
|
|
||||||
<ColouringMap
|
|
||||||
{...props}
|
|
||||||
building={this.state.building}
|
|
||||||
revision_id={this.state.revision_id}
|
|
||||||
selectBuilding={this.selectBuilding}
|
|
||||||
colourBuilding={this.colourBuilding}
|
|
||||||
/>
|
|
||||||
) } />
|
|
||||||
<Route exact path="/about.html" component={AboutPage} />
|
<Route exact path="/about.html" component={AboutPage} />
|
||||||
<Route exact path="/login.html">
|
<Route exact path="/login.html">
|
||||||
<Login user={this.state.user} login={this.login} />
|
<Login user={this.state.user} login={this.login} />
|
||||||
@ -263,6 +96,14 @@ class App extends React.Component<any, any> { // TODO: add proper types
|
|||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
<Route exact path="/privacy-policy.html" component={PrivacyPolicyPage} />
|
||||||
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
<Route exact path="/contributor-agreement.html" component={ContributorAgreementPage} />
|
||||||
|
<Route exact path="/:mode?/:category?/:building?" render={(props) => (
|
||||||
|
<MapApp
|
||||||
|
{...props}
|
||||||
|
building={this.props.building}
|
||||||
|
building_like={this.props.building_like}
|
||||||
|
user={this.state.user}
|
||||||
|
/>
|
||||||
|
)} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
|
250
app/src/frontend/map-app.tsx
Normal file
250
app/src/frontend/map-app.tsx
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { Switch, Route, RouteComponentProps, Redirect } from 'react-router-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Welcome from './pages/welcome';
|
||||||
|
import Sidebar from './building/sidebar';
|
||||||
|
import Categories from './building/categories';
|
||||||
|
import MultiEdit from './building/multi-edit';
|
||||||
|
import BuildingView from './building/building-view';
|
||||||
|
import ColouringMap from './map/map';
|
||||||
|
import { parse } from 'query-string';
|
||||||
|
|
||||||
|
interface MapAppRouteParams {
|
||||||
|
mode: 'view' | 'edit' | 'multi-edit';
|
||||||
|
category: string;
|
||||||
|
building?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapAppProps extends RouteComponentProps<MapAppRouteParams> {
|
||||||
|
building: any;
|
||||||
|
building_like: boolean;
|
||||||
|
user: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapAppState {
|
||||||
|
category: string;
|
||||||
|
revision_id: number;
|
||||||
|
building: any;
|
||||||
|
building_like: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapApp extends React.Component<MapAppProps, MapAppState> {
|
||||||
|
static propTypes = {
|
||||||
|
category: PropTypes.string,
|
||||||
|
revision_id: PropTypes.number,
|
||||||
|
building: PropTypes.object,
|
||||||
|
building_like: PropTypes.bool,
|
||||||
|
user: PropTypes.object
|
||||||
|
};
|
||||||
|
constructor(props: Readonly<MapAppProps>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// set building revision id, default 0
|
||||||
|
const rev = props.building != undefined ? +props.building.revision_id : 0;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
category: this.getCategory(props.match.params.category),
|
||||||
|
revision_id: rev,
|
||||||
|
building: props.building,
|
||||||
|
building_like: props.building_like
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectBuilding = this.selectBuilding.bind(this);
|
||||||
|
this.colourBuilding = this.colourBuilding.bind(this);
|
||||||
|
this.increaseRevision = this.increaseRevision.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(props: Readonly<MapAppProps>) {
|
||||||
|
const newCategory = this.getCategory(props.match.params.category);
|
||||||
|
if (newCategory != undefined) {
|
||||||
|
this.setState({ category: newCategory });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategory(category: string) {
|
||||||
|
if (category === 'categories') return undefined;
|
||||||
|
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseRevision(revisionId) {
|
||||||
|
revisionId = +revisionId;
|
||||||
|
// bump revision id, only ever increasing
|
||||||
|
if (revisionId > this.state.revision_id) {
|
||||||
|
this.setState({ revision_id: revisionId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectBuilding(building) {
|
||||||
|
const mode = this.props.match.params.mode || 'view';
|
||||||
|
const category = this.props.match.params.category || 'age';
|
||||||
|
|
||||||
|
if (building == undefined ||
|
||||||
|
(this.state.building != undefined &&
|
||||||
|
building.building_id === this.state.building.building_id)) {
|
||||||
|
this.setState({ building: undefined });
|
||||||
|
this.props.history.push(`/${mode}/${category}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.increaseRevision(building.revision_id);
|
||||||
|
// get UPRNs and update
|
||||||
|
fetch(`/api/buildings/${building.building_id}/uprns.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error(res);
|
||||||
|
} else {
|
||||||
|
building.uprns = res.uprns;
|
||||||
|
this.setState({ building: building });
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
this.setState({ building: building });
|
||||||
|
});
|
||||||
|
|
||||||
|
// get if liked and update
|
||||||
|
fetch(`/api/buildings/${building.building_id}/like.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then((res) => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error(res);
|
||||||
|
} else {
|
||||||
|
this.setState({ building_like: res.like });
|
||||||
|
this.props.history.push(`/${mode}/${category}/${building.building_id}`);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
this.setState({ building_like: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colour building
|
||||||
|
*
|
||||||
|
* Used in multi-edit mode to colour buildings on map click
|
||||||
|
*
|
||||||
|
* Pulls data from URL to form update
|
||||||
|
*
|
||||||
|
* @param {object} building
|
||||||
|
*/
|
||||||
|
colourBuilding(building) {
|
||||||
|
const cat = this.props.match.params.category;
|
||||||
|
const q = parse(window.location.search);
|
||||||
|
const data = (cat === 'like') ? { like: true } : JSON.parse(q.data as string); // TODO: verify what happens if data is string[]
|
||||||
|
if (cat === 'like') {
|
||||||
|
this.likeBuilding(building.building_id)
|
||||||
|
} else {
|
||||||
|
this.updateBuilding(building.building_id, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
likeBuilding(buildingId) {
|
||||||
|
fetch(`/api/buildings/${buildingId}/like.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({ like: true })
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then(function (res) {
|
||||||
|
if (res.error) {
|
||||||
|
console.error({ error: res.error })
|
||||||
|
} else {
|
||||||
|
this.increaseRevision(res.revision_id);
|
||||||
|
}
|
||||||
|
}.bind(this)).catch(
|
||||||
|
(err) => console.error({ error: err })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBuilding(buildingId, data) {
|
||||||
|
fetch(`/api/buildings/${buildingId}.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(
|
||||||
|
res => res.json()
|
||||||
|
).then(res => {
|
||||||
|
if (res.error) {
|
||||||
|
console.error({ error: res.error })
|
||||||
|
} else {
|
||||||
|
this.increaseRevision(res.revision_id);
|
||||||
|
}
|
||||||
|
}).catch(
|
||||||
|
(err) => console.error({ error: err })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const mode = this.props.match.params.mode || 'basic';
|
||||||
|
|
||||||
|
let category = this.state.category || 'age';
|
||||||
|
|
||||||
|
const building_id = this.state.building && this.state.building.building_id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/">
|
||||||
|
<Welcome />
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/:mode/categories/:building?">
|
||||||
|
<Sidebar>
|
||||||
|
<Categories mode={mode} building_id={building_id} />
|
||||||
|
</Sidebar>
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/multi-edit/:cat" render={(props) => (
|
||||||
|
<MultiEdit
|
||||||
|
{...props}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
)} />
|
||||||
|
<Route exact path="/:mode/:cat/:building?">
|
||||||
|
<Sidebar>
|
||||||
|
<BuildingView
|
||||||
|
mode={mode}
|
||||||
|
cat={category}
|
||||||
|
building={this.state.building}
|
||||||
|
building_like={this.state.building_like}
|
||||||
|
selectBuilding={this.selectBuilding}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
</Sidebar>
|
||||||
|
</Route>
|
||||||
|
<Route exact path="/(view|edit|multi-edit)">
|
||||||
|
<Redirect to="/view/categories" />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
<ColouringMap
|
||||||
|
building={this.state.building}
|
||||||
|
mode={mode}
|
||||||
|
category={category}
|
||||||
|
revision_id={this.state.revision_id}
|
||||||
|
selectBuilding={this.selectBuilding}
|
||||||
|
colourBuilding={this.colourBuilding}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapApp;
|
@ -14,6 +14,15 @@ import ThemeSwitcher from './theme-switcher';
|
|||||||
|
|
||||||
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
const OS_API_KEY = 'NVUxtY5r8eA6eIfwrPTAGKrAAsoeI9E9';
|
||||||
|
|
||||||
|
interface ColouringMapProps {
|
||||||
|
building: any;
|
||||||
|
mode: 'basic' | 'view' | 'edit' | 'multi-edit';
|
||||||
|
category: string;
|
||||||
|
revision_id: number;
|
||||||
|
selectBuilding: any;
|
||||||
|
colourBuilding: any;
|
||||||
|
}
|
||||||
|
|
||||||
interface ColouringMapState {
|
interface ColouringMapState {
|
||||||
theme: 'light' | 'night';
|
theme: 'light' | 'night';
|
||||||
lat: number;
|
lat: number;
|
||||||
@ -23,14 +32,14 @@ interface ColouringMapState {
|
|||||||
/**
|
/**
|
||||||
* Map area
|
* Map area
|
||||||
*/
|
*/
|
||||||
class ColouringMap extends Component<any, ColouringMapState> { // TODO: add proper types
|
class ColouringMap extends Component<ColouringMapProps, ColouringMapState> { // TODO: add proper types
|
||||||
static propTypes = { // TODO: generate propTypes from TS
|
static propTypes = { // TODO: generate propTypes from TS
|
||||||
building: PropTypes.object,
|
building: PropTypes.object,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
category: PropTypes.string,
|
||||||
revision_id: PropTypes.number,
|
revision_id: PropTypes.number,
|
||||||
selectBuilding: PropTypes.func,
|
selectBuilding: PropTypes.func,
|
||||||
colourBuilding: PropTypes.func,
|
colourBuilding: PropTypes.func
|
||||||
match: PropTypes.object,
|
|
||||||
history: PropTypes.object
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -44,7 +53,6 @@ class ColouringMap extends Component<any, ColouringMapState> { // TODO: add prop
|
|||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
this.handleLocate = this.handleLocate.bind(this);
|
this.handleLocate = this.handleLocate.bind(this);
|
||||||
this.themeSwitch = this.themeSwitch.bind(this);
|
this.themeSwitch = this.themeSwitch.bind(this);
|
||||||
this.getMode = this.getMode.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLocate(lat, lng, zoom){
|
handleLocate(lat, lng, zoom){
|
||||||
@ -52,21 +60,13 @@ class ColouringMap extends Component<any, ColouringMapState> { // TODO: add prop
|
|||||||
lat: lat,
|
lat: lat,
|
||||||
lng: lng,
|
lng: lng,
|
||||||
zoom: zoom
|
zoom: zoom
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
getMode() {
|
|
||||||
const isEdit = this.props.match.url.match('edit')
|
|
||||||
const isMulti = this.props.match.url.match('multi')
|
|
||||||
return isEdit? (isMulti? 'multi' : 'edit') : 'view';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e) {
|
handleClick(e) {
|
||||||
const mode = this.getMode()
|
const mode = this.props.mode;
|
||||||
const lat = e.latlng.lat
|
const lat = e.latlng.lat;
|
||||||
const lng = e.latlng.lng
|
const lng = e.latlng.lng;
|
||||||
const newCat = parseCategoryURL(this.props.match.url);
|
|
||||||
const mapCat = newCat || 'age';
|
|
||||||
fetch(
|
fetch(
|
||||||
'/api/buildings/locate?lat='+lat+'&lng='+lng
|
'/api/buildings/locate?lat='+lat+'&lng='+lng
|
||||||
).then(
|
).then(
|
||||||
@ -74,17 +74,15 @@ class ColouringMap extends Component<any, ColouringMapState> { // TODO: add prop
|
|||||||
).then(function(data){
|
).then(function(data){
|
||||||
if (data && data.length){
|
if (data && data.length){
|
||||||
const building = data[0];
|
const building = data[0];
|
||||||
if (mode === 'multi') {
|
if (mode === 'multi-edit') {
|
||||||
// colour building directly
|
// colour building directly
|
||||||
this.props.colourBuilding(building);
|
this.props.colourBuilding(building);
|
||||||
} else {
|
} else {
|
||||||
this.props.selectBuilding(building);
|
this.props.selectBuilding(building);
|
||||||
this.props.history.push(`/${mode}/${mapCat}/building/${building.building_id}.html`);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// deselect but keep/return to expected colour theme
|
// deselect but keep/return to expected colour theme
|
||||||
this.props.selectBuilding(undefined);
|
this.props.selectBuilding(undefined);
|
||||||
this.props.history.push(`/${mode}/${mapCat}.html`);
|
|
||||||
}
|
}
|
||||||
}.bind(this)).catch(
|
}.bind(this)).catch(
|
||||||
(err) => console.error(err)
|
(err) => console.error(err)
|
||||||
@ -101,46 +99,49 @@ class ColouringMap extends Component<any, ColouringMapState> { // TODO: add prop
|
|||||||
const position: LatLngExpression = [this.state.lat, this.state.lng];
|
const position: LatLngExpression = [this.state.lat, this.state.lng];
|
||||||
|
|
||||||
// baselayer
|
// baselayer
|
||||||
const key = OS_API_KEY
|
const key = OS_API_KEY;
|
||||||
const tilematrixSet = 'EPSG:3857'
|
const tilematrixSet = 'EPSG:3857';
|
||||||
const layer = (this.state.theme === 'light')? 'Light 3857' : 'Night 3857';
|
const layer = (this.state.theme === 'light')? 'Light 3857' : 'Night 3857';
|
||||||
const url = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`
|
const baseUrl = `https://api2.ordnancesurvey.co.uk/mapping_api/v1/service/zxy/${tilematrixSet}/${layer}/{z}/{x}/{y}.png?key=${key}`;
|
||||||
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines.'
|
const attribution = 'Building attribute data is © Colouring London contributors. Maps contain OS data © Crown copyright: OS Maps baselayers and building outlines.';
|
||||||
|
const baseLayer = <TileLayer
|
||||||
|
url={baseUrl}
|
||||||
|
attribution={attribution}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
const buildingsBaseUrl = `/tiles/base_${this.state.theme}/{z}/{x}/{y}.png`;
|
||||||
|
const buildingBaseLayer = <TileLayer url={buildingsBaseUrl} minZoom={14} />;
|
||||||
|
|
||||||
// colour-data tiles
|
// colour-data tiles
|
||||||
const isBuilding = /building/.test(this.props.match.url);
|
const cat = this.props.category;
|
||||||
const isEdit = /edit/.test(this.props.match.url);
|
|
||||||
const cat = parseCategoryURL(this.props.match.url);
|
|
||||||
const tilesetByCat = {
|
const tilesetByCat = {
|
||||||
age: 'date_year',
|
age: 'date_year',
|
||||||
size: 'size_storeys',
|
size: 'size_storeys',
|
||||||
location: 'location',
|
location: 'location',
|
||||||
like: 'likes',
|
like: 'likes',
|
||||||
planning: 'conservation_area',
|
planning: 'conservation_area',
|
||||||
}
|
};
|
||||||
const tileset = tilesetByCat[cat];
|
const tileset = tilesetByCat[cat];
|
||||||
// pick revision id to bust browser cache
|
// pick revision id to bust browser cache
|
||||||
const rev = this.props.revision_id;
|
const rev = this.props.revision_id;
|
||||||
const dataLayer = tileset?
|
const dataLayer = tileset != undefined ?
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={tileset}
|
key={tileset}
|
||||||
url={`/tiles/${tileset}/{z}/{x}/{y}.png?rev=${rev}`}
|
url={`/tiles/${tileset}/{z}/{x}/{y}.png?rev=${rev}`}
|
||||||
minZoom={9} />
|
minZoom={9}
|
||||||
|
/>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// highlight
|
// highlight
|
||||||
const geometryId = (this.props.building) ? this.props.building.geometry_id : undefined;
|
const highlightLayer = this.props.building != undefined ?
|
||||||
const highlight = `/tiles/highlight/{z}/{x}/{y}.png?highlight=${geometryId}`
|
|
||||||
const highlightLayer = (isBuilding && this.props.building) ?
|
|
||||||
<TileLayer
|
<TileLayer
|
||||||
key={this.props.building.building_id}
|
key={this.props.building.building_id}
|
||||||
url={highlight}
|
url={`/tiles/highlight/{z}/{x}/{y}.png?highlight=${this.props.building.geometry_id}`}
|
||||||
minZoom={14} />
|
minZoom={14}
|
||||||
|
/>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const baseUrl = (this.state.theme === 'light')?
|
const isEdit = ['edit', 'multi-edit'].includes(this.props.mode);
|
||||||
'/tiles/base_light/{z}/{x}/{y}.png'
|
|
||||||
: '/tiles/base_night/{z}/{x}/{y}.png'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="map-container">
|
<div className="map-container">
|
||||||
@ -154,23 +155,23 @@ class ColouringMap extends Component<any, ColouringMapState> { // TODO: add prop
|
|||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
<TileLayer url={url} attribution={attribution} />
|
{ baseLayer }
|
||||||
<TileLayer url={baseUrl} minZoom={14} />
|
{ buildingBaseLayer }
|
||||||
{ dataLayer }
|
{ dataLayer }
|
||||||
{ highlightLayer }
|
{ highlightLayer }
|
||||||
<ZoomControl position="topright" />
|
<ZoomControl position="topright" />
|
||||||
<AttributionControl prefix="" />
|
<AttributionControl />
|
||||||
</Map>
|
</Map>
|
||||||
{
|
{
|
||||||
!isBuilding && this.props.match.url !== '/'? (
|
this.props.mode !== 'basic'? (
|
||||||
<div className="map-notice">
|
|
||||||
<HelpIcon /> {isEdit? 'Click a building to edit' : 'Click a building for details'}
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.props.match.url !== '/'? (
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
{
|
||||||
|
this.props.building == undefined ?
|
||||||
|
<div className="map-notice">
|
||||||
|
<HelpIcon /> {isEdit ? 'Click a building to edit' : 'Click a building for details'}
|
||||||
|
</div>
|
||||||
|
: null
|
||||||
|
}
|
||||||
<Legend slug={cat} />
|
<Legend slug={cat} />
|
||||||
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
|
<ThemeSwitcher onSubmit={this.themeSwitch} currentTheme={this.state.theme} />
|
||||||
<SearchBox onLocate={this.handleLocate} />
|
<SearchBox onLocate={this.handleLocate} />
|
||||||
|
Loading…
Reference in New Issue
Block a user