Merge pull request #666 from mz8i/feature/545-dynamics
Feature 545: simple dynamics
This commit is contained in:
commit
653282f0f3
@ -507,4 +507,34 @@
|
||||
<LineSymbolizer stroke="#888" stroke-width="3.0"/>
|
||||
</Rule>
|
||||
</Style>
|
||||
<Style name="dynamics_demolished_count">
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] >= 7</Filter>
|
||||
<PolygonSymbolizer fill="#bd0026" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] = 6</Filter>
|
||||
<PolygonSymbolizer fill="#e31a1c" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] = 5</Filter>
|
||||
<PolygonSymbolizer fill="#fc4e2a" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] = 4</Filter>
|
||||
<PolygonSymbolizer fill="#fd8d3c" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] = 3</Filter>
|
||||
<PolygonSymbolizer fill="#feb24c" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] = 2</Filter>
|
||||
<PolygonSymbolizer fill="#fed976" />
|
||||
</Rule>
|
||||
<Rule>
|
||||
<Filter>[demolished_buildings_count] = 1</Filter>
|
||||
<PolygonSymbolizer fill="#ffe8a9" />
|
||||
</Rule>
|
||||
</Style>
|
||||
</Map>
|
||||
|
@ -239,4 +239,10 @@ export const dataFieldsConfig = valueType<DataFieldConfig>()({ /* eslint-disable
|
||||
verify: false,
|
||||
},
|
||||
|
||||
demolished_buildings: {
|
||||
edit: true,
|
||||
verify: false,
|
||||
asJson: true,
|
||||
sqlCast: 'jsonb',
|
||||
},
|
||||
});
|
||||
|
@ -3,5 +3,64 @@ import { SomeJSONSchema } from 'ajv/dist/types/json-schema';
|
||||
import { dataFieldsConfig } from './dataFields';
|
||||
|
||||
export const fieldSchemaConfig: { [key in keyof typeof dataFieldsConfig]?: SomeJSONSchema} = { /*eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
demolished_buildings: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['year_constructed', 'year_demolished', 'overlap_present', 'links'],
|
||||
properties: {
|
||||
year_constructed: {
|
||||
type: 'object',
|
||||
required: ['min', 'max'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
min: {
|
||||
type: 'integer'
|
||||
},
|
||||
max: {
|
||||
type: 'integer'
|
||||
}
|
||||
}
|
||||
},
|
||||
year_demolished: {
|
||||
type: 'object',
|
||||
required: ['min', 'max'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
min: {
|
||||
type: 'integer'
|
||||
},
|
||||
max: {
|
||||
type: 'integer'
|
||||
}
|
||||
}
|
||||
},
|
||||
overlap_present: {
|
||||
type: 'string',
|
||||
enum: ['1%', '25%', '50%', '75%', '100%']
|
||||
},
|
||||
links: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
}
|
||||
} as JSONSchemaType<{
|
||||
year_constructed: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
year_demolished: {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
overlap_present: string;
|
||||
links: string[];
|
||||
}[]>,
|
||||
|
||||
} as const;
|
||||
|
@ -6,6 +6,9 @@ import { ArgumentError } from '../../errors/general';
|
||||
|
||||
import { updateLandUse } from './landUse';
|
||||
|
||||
/**
|
||||
* Process land use classifications - derive land use order from land use groups
|
||||
*/
|
||||
async function processCurrentLandUseClassifications(buildingId: number, buildingUpdate: any): Promise<any> {
|
||||
const currentBuildingData = await getBuildingData(buildingId);
|
||||
|
||||
@ -31,10 +34,26 @@ async function processCurrentLandUseClassifications(buildingId: number, building
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process Dynamics data - sort past buildings by construction date
|
||||
*/
|
||||
async function processDynamicsPastBuildings(buildingId: number, buildingUpdate: any): Promise<any> {
|
||||
buildingUpdate.demolished_buildings = buildingUpdate.demolished_buildings.sort((a, b) => b.year_constructed.min - a.year_constructed.min);
|
||||
return buildingUpdate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Define any custom processing logic for specific building attributes
|
||||
*/
|
||||
export async function processBuildingUpdate(buildingId: number, buildingUpdate: any): Promise<any> {
|
||||
if(hasAnyOwnProperty(buildingUpdate, ['current_landuse_group'])) {
|
||||
buildingUpdate = await processCurrentLandUseClassifications(buildingId, buildingUpdate);
|
||||
}
|
||||
if('demolished_buildings' in buildingUpdate) {
|
||||
buildingUpdate = await processDynamicsPastBuildings(buildingId, buildingUpdate);
|
||||
}
|
||||
|
||||
return buildingUpdate;
|
||||
}
|
||||
|
11
app/src/frontend/building/data-components/field-row.css
Normal file
11
app/src/frontend/building/data-components/field-row.css
Normal file
@ -0,0 +1,11 @@
|
||||
.field-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
justify-items: left;
|
||||
}
|
||||
|
||||
/* make all row elements the same width */
|
||||
.field-row * {
|
||||
flex: 1 1 0px;
|
||||
}
|
11
app/src/frontend/building/data-components/field-row.tsx
Normal file
11
app/src/frontend/building/data-components/field-row.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
import './field-row.css';
|
||||
|
||||
export function FieldRow({children}) {
|
||||
return (
|
||||
<div className="field-row">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import InfoBox from '../../components/info-box';
|
||||
import DataEntry from '../data-components/data-entry';
|
||||
import { DataEntryGroup } from '../data-components/data-entry-group';
|
||||
|
||||
import withCopyEdit from '../data-container';
|
||||
|
||||
import { CategoryViewProps } from './category-view-props';
|
||||
|
||||
/**
|
||||
* Dynamics view/edit section
|
||||
*/
|
||||
const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => (
|
||||
<Fragment>
|
||||
<InfoBox msg="This is what we're planning to collect in this section" />
|
||||
<DataEntryGroup collapsed={false} name="Historical constructions and demolitions" showCount={false}>
|
||||
<DataEntry
|
||||
title="Current building"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Past buildings"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<DataEntry
|
||||
title="Historical land use change"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Longitudinal historical footprints (raster) link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Longitudinal historical footprints (vector) link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
const DynamicsContainer = withCopyEdit(DynamicsView);
|
||||
|
||||
export default DynamicsContainer;
|
@ -0,0 +1,36 @@
|
||||
.dynamics-building-pane {
|
||||
position: relative;
|
||||
background-color: #f1f1f1;
|
||||
border: 1px solid #cccccc;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dynamics-building-pane.new-record {
|
||||
background-color: #f6f6f6;
|
||||
border: 1px dashed #cccccc;
|
||||
}
|
||||
|
||||
.h6 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.delete-record-button {
|
||||
position: absolute;
|
||||
top:10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.add-record-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.field-row * {
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.lifespan-entry {
|
||||
flex: 0 1 27%;
|
||||
}
|
@ -0,0 +1,283 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { BuildingAttributes } from '../../../models/building';
|
||||
import { FieldRow } from '../../data-components/field-row';
|
||||
import DataEntry, { BaseDataEntryProps } from '../../data-components/data-entry';
|
||||
import { dataFields } from '../../../config/data-fields-config';
|
||||
import SelectDataEntry from '../../data-components/select-data-entry';
|
||||
import MultiDataEntry from '../../data-components/multi-data-entry/multi-data-entry';
|
||||
import { NumberRangeDataEntry } from './number-range-data-entry';
|
||||
|
||||
import './dynamics-data-entry.css';
|
||||
|
||||
type DemolishedBuilding = (BuildingAttributes['demolished_buildings'][number]);
|
||||
|
||||
export const DynamicsBuildingPane: React.FC<{className?: string}> = ({children, className}) => (
|
||||
<div className={`dynamics-building-pane ${className ?? ''}`} >
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
function lifespan(a: number, b: number): number {
|
||||
if(a == undefined || b == undefined) return undefined;
|
||||
|
||||
const diff = a - b;
|
||||
|
||||
return Math.max(diff, 0);
|
||||
}
|
||||
|
||||
function formatRange(minSpan: number, maxSpan: number): string {
|
||||
if(minSpan == undefined || maxSpan == undefined) return '';
|
||||
|
||||
if(minSpan === maxSpan) return minSpan + '';
|
||||
|
||||
return `${minSpan}-${maxSpan}`;
|
||||
}
|
||||
|
||||
interface DynamicsDataRowProps {
|
||||
value: DemolishedBuilding;
|
||||
onChange?: (value: DemolishedBuilding) => void;
|
||||
disabled?: boolean;
|
||||
maxYear?: number;
|
||||
minYear?: number;
|
||||
mode?: 'view' | 'edit' | 'multi-edit';
|
||||
required?: boolean;
|
||||
validateForm?: boolean;
|
||||
index?: number;
|
||||
}
|
||||
const DynamicsDataRow: React.FC<DynamicsDataRowProps> = ({
|
||||
value = {} as DemolishedBuilding,
|
||||
onChange,
|
||||
disabled = false,
|
||||
maxYear,
|
||||
minYear,
|
||||
mode,
|
||||
required = false,
|
||||
validateForm = false,
|
||||
index
|
||||
}) => {
|
||||
|
||||
const onFieldChange = useCallback((key: string, val: any) => {
|
||||
const changedValue = {...value};
|
||||
changedValue[key] = val;
|
||||
onChange(changedValue);
|
||||
}, [value, onChange]);
|
||||
|
||||
const maxLifespan = lifespan(value.year_demolished?.max, value.year_constructed?.min);
|
||||
const minLifespan = lifespan(value.year_demolished?.min, value.year_constructed?.max);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldRow>
|
||||
<div>
|
||||
<NumberRangeDataEntry
|
||||
slug='year_constructed'
|
||||
slugModifier={index}
|
||||
title={dataFields.demolished_buildings.items.year_constructed.title}
|
||||
onChange={onFieldChange}
|
||||
value={value.year_constructed}
|
||||
disabled={disabled}
|
||||
max={value.year_demolished?.min ?? maxYear}
|
||||
min={minYear}
|
||||
placeholderMin='Earliest'
|
||||
placeholderMax='Latest'
|
||||
titleMin={`${dataFields.demolished_buildings.items.year_constructed.title}: earliest estimate`}
|
||||
titleMax={`${dataFields.demolished_buildings.items.year_constructed.title}: latest estimate`}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumberRangeDataEntry
|
||||
slug='year_demolished'
|
||||
slugModifier={index}
|
||||
title={dataFields.demolished_buildings.items.year_demolished.title}
|
||||
onChange={onFieldChange}
|
||||
value={value.year_demolished}
|
||||
disabled={disabled}
|
||||
max={maxYear}
|
||||
min={value.year_constructed?.max ?? minYear}
|
||||
placeholderMin='Earliest'
|
||||
placeholderMax='Latest'
|
||||
titleMin={`${dataFields.demolished_buildings.items.year_demolished.title}: earliest estimate`}
|
||||
titleMax={`${dataFields.demolished_buildings.items.year_demolished.title}: latest estimate`}
|
||||
required={required}
|
||||
/>
|
||||
</div>
|
||||
<div className='lifespan-entry'>
|
||||
<DataEntry
|
||||
slug='lifespan'
|
||||
slugModifier={index}
|
||||
title={dataFields.demolished_buildings.items.lifespan.title}
|
||||
value={formatRange(minLifespan, maxLifespan)}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
<SelectDataEntry
|
||||
slug='overlap_present'
|
||||
slugModifier={index}
|
||||
title={dataFields.demolished_buildings.items.overlap_present.title}
|
||||
onChange={onFieldChange}
|
||||
value={value.overlap_present}
|
||||
options={[
|
||||
{value: '1%', label: '1% - almost no overlap with current site'},
|
||||
'25%',
|
||||
'50%',
|
||||
'75%',
|
||||
{value: '100%', label: '100% - fully contained in current site'}
|
||||
]}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
/>
|
||||
<MultiDataEntry
|
||||
slug='links'
|
||||
slugModifier={index}
|
||||
title={dataFields.demolished_buildings.items.links.title}
|
||||
onChange={onFieldChange}
|
||||
value={value.links}
|
||||
disabled={disabled}
|
||||
editableEntries={true}
|
||||
mode={mode}
|
||||
required={required}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
interface DynamicsDataEntryProps extends BaseDataEntryProps {
|
||||
value: DemolishedBuilding[];
|
||||
editableEntries: boolean;
|
||||
maxYear: number;
|
||||
minYear: number;
|
||||
onSaveAdd: (slug: string, newItem: any) => void;
|
||||
hasEdits: boolean;
|
||||
}
|
||||
|
||||
function isValid(val: DemolishedBuilding) {
|
||||
if(val == undefined) return false;
|
||||
|
||||
if(typeof val.year_constructed?.min !== 'number') return false;
|
||||
if(typeof val.year_constructed?.max !== 'number') return false;
|
||||
|
||||
if(typeof val.year_demolished?.min !== 'number') return false;
|
||||
if(typeof val.year_demolished?.max !== 'number') return false;
|
||||
|
||||
if(val.overlap_present == undefined) return false;
|
||||
|
||||
if(val.links == undefined || val.links.length < 1) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const DynamicsDataEntry: React.FC<DynamicsDataEntryProps> = (props) => {
|
||||
const [newValue, setNewValue] = useState<DemolishedBuilding>();
|
||||
|
||||
const values: DemolishedBuilding[] = props.value ?? [];
|
||||
const isEditing = props.mode === 'edit';
|
||||
const isDisabled = !isEditing || props.disabled;
|
||||
|
||||
const isEdited = !_.isEmpty(newValue);
|
||||
const valid = isValid(newValue);
|
||||
|
||||
const addNew = useCallback(() => {
|
||||
const val = {...newValue};
|
||||
|
||||
setNewValue(undefined);
|
||||
props.onSaveAdd(props.slug, val);
|
||||
}, [values, newValue]);
|
||||
|
||||
const edit = useCallback((id: number, val: DemolishedBuilding) => {
|
||||
const editedValues = [...values];
|
||||
editedValues.splice(id, 1, val);
|
||||
|
||||
props.onChange(props.slug, editedValues);
|
||||
}, [values]);
|
||||
|
||||
const remove = useCallback((id: number) => {
|
||||
const editedValues = [...values];
|
||||
editedValues.splice(id, 1);
|
||||
|
||||
props.onChange(props.slug, editedValues);
|
||||
}, [values]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{
|
||||
isEditing &&
|
||||
<>
|
||||
<h6 className="h6">Existing records for demolished buildings</h6>
|
||||
<label>Please supply sources for any edits of existing records</label>
|
||||
</>
|
||||
}
|
||||
<ul className="data-link-list">
|
||||
{
|
||||
values.length === 0 &&
|
||||
<div className="input-group">
|
||||
<input className="form-control no-entries" type="text" value="No records so far" disabled={true} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
values.map((pastBuilding, id) => (
|
||||
<li key={id}>
|
||||
<DynamicsBuildingPane>
|
||||
<label>Demolished building</label>
|
||||
{
|
||||
!isDisabled &&
|
||||
<button type="button" className="btn btn-outline-dark delete-record-button"
|
||||
title="Delete Record"
|
||||
onClick={() => remove(id)}
|
||||
data-index={id}
|
||||
>x</button>
|
||||
}
|
||||
<DynamicsDataRow
|
||||
value={pastBuilding}
|
||||
disabled={!props.editableEntries || isDisabled}
|
||||
onChange={(value) => edit(id, value)}
|
||||
minYear={props.minYear}
|
||||
maxYear={props.maxYear}
|
||||
mode={props.mode}
|
||||
required={true}
|
||||
index={id}
|
||||
/>
|
||||
</DynamicsBuildingPane>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
!isDisabled &&
|
||||
<div className='new-record-section'>
|
||||
<h6 className="h6">Add a new demolished building record</h6>
|
||||
<DynamicsBuildingPane className='new-record'>
|
||||
<DynamicsDataRow
|
||||
value={newValue}
|
||||
onChange={setNewValue}
|
||||
disabled={isDisabled}
|
||||
minYear={props.minYear}
|
||||
maxYear={props.maxYear}
|
||||
mode={props.mode}
|
||||
/>
|
||||
<label>Please save all your edits before navigating away from the currently selected building - these will be erased otherwise.</label>
|
||||
<button type="button"
|
||||
className="btn btn-primary btn-block add-record-button"
|
||||
title="Add to list"
|
||||
onClick={addNew}
|
||||
disabled={!valid || props.hasEdits}
|
||||
>
|
||||
{
|
||||
props.hasEdits ?
|
||||
'Save or discard edits first to add a new record' :
|
||||
(isEdited && !valid) ?
|
||||
'Fill in all fields to save record' :
|
||||
'Save new record'
|
||||
}
|
||||
</button>
|
||||
</DynamicsBuildingPane>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
123
app/src/frontend/building/data-containers/dynamics/dynamics.tsx
Normal file
123
app/src/frontend/building/data-containers/dynamics/dynamics.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import InfoBox from '../../../components/info-box';
|
||||
|
||||
import { Category } from '../../../config/categories-config';
|
||||
import { dataFields } from '../../../config/data-fields-config';
|
||||
|
||||
import DataEntry from '../../data-components/data-entry';
|
||||
import { DataEntryGroup } from '../../data-components/data-entry-group';
|
||||
import { DynamicsBuildingPane, DynamicsDataEntry } from './dynamics-data-entry';
|
||||
import { FieldRow } from '../../data-components/field-row';
|
||||
import NumericDataEntry from '../../data-components/numeric-data-entry';
|
||||
import withCopyEdit from '../../data-container';
|
||||
|
||||
import { CategoryViewProps } from '../category-view-props';
|
||||
|
||||
/**
|
||||
* Dynamics view/edit section
|
||||
*/
|
||||
const DynamicsView: React.FunctionComponent<CategoryViewProps> = (props) => {
|
||||
const building = props.building;
|
||||
const thisYear = (new Date()).getFullYear();
|
||||
const currentBuildingConstructionYear = building.date_year || undefined;
|
||||
|
||||
const ageLinkUrl = `/${props.mode}/${Category.Age}/${props.building.building_id}`;
|
||||
|
||||
return (<>
|
||||
<DataEntryGroup collapsed={false} name="Constructions and demolitions on this site" showCount={false}>
|
||||
<DynamicsBuildingPane>
|
||||
<label>Current building (age data <Link to={ageLinkUrl}>editable here</Link>)</label>
|
||||
<FieldRow>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_constructed.title}
|
||||
value={currentBuildingConstructionYear}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<NumericDataEntry
|
||||
slug=''
|
||||
title={dataFields.demolished_buildings.items.year_demolished.title}
|
||||
value={undefined}
|
||||
placeholder='---'
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
<div style={{flex: '0 1 27%'}}>
|
||||
<DataEntry
|
||||
slug=''
|
||||
title='Lifespan to date'
|
||||
value={ (thisYear - currentBuildingConstructionYear) + ''}
|
||||
disabled={true}
|
||||
mode='view'
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</DynamicsBuildingPane>
|
||||
{
|
||||
currentBuildingConstructionYear != undefined ?
|
||||
<>
|
||||
<DynamicsDataEntry
|
||||
|
||||
/*
|
||||
Will clear the edits and new record data upon navigating to another building.
|
||||
Should get a better way to do this, plus a way to actually keep unsaved edits.
|
||||
*/
|
||||
key={building.building_id}
|
||||
|
||||
value={building.demolished_buildings}
|
||||
editableEntries={true}
|
||||
slug='demolished_buildings'
|
||||
title={undefined}
|
||||
mode={props.mode}
|
||||
onChange={props.onChange}
|
||||
onSaveAdd={props.onSaveAdd}
|
||||
hasEdits={props.edited}
|
||||
maxYear={currentBuildingConstructionYear}
|
||||
minYear={50}
|
||||
/>
|
||||
{
|
||||
props.mode === 'view' &&
|
||||
<InfoBox>Switch to edit mode to add/edit past building records</InfoBox>
|
||||
}
|
||||
</> :
|
||||
<InfoBox>To add historical records, fill in the <Link to={ageLinkUrl}>Age</Link> data first.</InfoBox>
|
||||
}
|
||||
|
||||
</DataEntryGroup>
|
||||
|
||||
<DataEntryGroup name="Future planned data collection" collapsed={false} showCount={false}>
|
||||
<DataEntry
|
||||
title="Historical land use change"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Longitudinal historical footprints (raster) link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
<DataEntry
|
||||
title="Longitudinal historical footprints (vector) link"
|
||||
slug=""
|
||||
value=""
|
||||
mode='view'
|
||||
/>
|
||||
</DataEntryGroup>
|
||||
<InfoBox>
|
||||
This section is under development in collaboration with the historic environment sector.
|
||||
Please let us know your suggestions on the <a href="https://discuss.colouring.london">discussion forum</a>! (external link - save your edits first)
|
||||
</InfoBox>
|
||||
</>)
|
||||
};
|
||||
|
||||
const DynamicsContainer = withCopyEdit(DynamicsView);
|
||||
|
||||
export default DynamicsContainer;
|
@ -0,0 +1,7 @@
|
||||
.min-number-input {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.max-number-input {
|
||||
border-left: 1px dashed #ccc;
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { BaseDataEntryProps } from '../../data-components/data-entry';
|
||||
import { DataTitleCopyable } from '../../data-components/data-title';
|
||||
import { FieldRow } from '../../data-components/field-row';
|
||||
|
||||
import './number-range-data-entry.css';
|
||||
|
||||
interface NumberRangeDataEntryProps extends BaseDataEntryProps {
|
||||
value?: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
placeholderMin?: string;
|
||||
placeholderMax?: string;
|
||||
titleMin?: string;
|
||||
titleMax?: string;
|
||||
step?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
export const NumberRangeDataEntry: React.FC<NumberRangeDataEntryProps> = (props) => {
|
||||
const {min: minValue, max: maxValue} = props.value ?? {};
|
||||
const [minEdited, setMinEdited] = useState(minValue != undefined);
|
||||
const [maxEdited, setMaxEdited] = useState(maxValue != undefined);
|
||||
|
||||
const isDisabled = props.mode === 'view' || props.disabled;
|
||||
|
||||
const slugWithModifier = props.slug + (props.slugModifier ?? '');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTitleCopyable
|
||||
slug={props.slug}
|
||||
slugModifier={props.slugModifier}
|
||||
title={props.title}
|
||||
tooltip={props.tooltip}
|
||||
disabled={props.disabled || props.value == undefined}
|
||||
copy={props.copy}
|
||||
/>
|
||||
<FieldRow>
|
||||
<input
|
||||
className="form-control min-number-input"
|
||||
type="number"
|
||||
id={slugWithModifier}
|
||||
name={slugWithModifier}
|
||||
value={minValue ?? ''}
|
||||
step={props.step ?? 1}
|
||||
max={(maxEdited ? maxValue : null) ?? props.max}
|
||||
min={props.min}
|
||||
disabled={isDisabled}
|
||||
placeholder={props.placeholderMin}
|
||||
title={props.titleMin}
|
||||
required={props.required}
|
||||
onChange={e => {
|
||||
const val = e.target.value === '' ? null : parseFloat(e.target.value);
|
||||
setMinEdited(val != undefined);
|
||||
props.onChange(
|
||||
props.slug,
|
||||
{
|
||||
min: val,
|
||||
max: maxEdited ? maxValue : val
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
/>
|
||||
<input
|
||||
className="form-control max-number-input"
|
||||
type="number"
|
||||
id={`${props.slug}-max`}
|
||||
name={`${props.slug}-max`}
|
||||
value={maxValue ?? ''}
|
||||
step={props.step ?? 1}
|
||||
max={props.max}
|
||||
min={(minEdited ? minValue : null) ?? props.min}
|
||||
disabled={isDisabled}
|
||||
placeholder={props.placeholderMax}
|
||||
title={props.titleMax}
|
||||
required={props.required}
|
||||
onChange={e => {
|
||||
const val = e.target.value === '' ? null : parseFloat(e.target.value);
|
||||
setMaxEdited(val != undefined);
|
||||
props.onChange(
|
||||
props.slug,
|
||||
{
|
||||
min: minEdited ? minValue : val,
|
||||
max: val
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -115,7 +115,6 @@ export const categoriesConfig: {[key in Category]: CategoryDefinition} = {
|
||||
intro: "What's the building's context? Coming soon…",
|
||||
},
|
||||
[Category.Dynamics]: {
|
||||
inactive: true,
|
||||
slug: 'dynamics',
|
||||
name: 'Dynamics',
|
||||
aboutUrl: 'https://pages.colouring.london/dynamics',
|
||||
|
@ -185,10 +185,34 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition} = {
|
||||
},
|
||||
},
|
||||
[Category.Dynamics]: {
|
||||
mapStyle: undefined,
|
||||
mapStyle: 'dynamics_demolished_count',
|
||||
legend: {
|
||||
title: 'Dynamics',
|
||||
elements: []
|
||||
description: 'Past (demolished) buildings on site',
|
||||
elements: [
|
||||
{
|
||||
text: '7+',
|
||||
color: '#bd0026',
|
||||
}, {
|
||||
text: '6',
|
||||
color: '#e31a1c',
|
||||
}, {
|
||||
text: '5',
|
||||
color: '#fc4e2a',
|
||||
}, {
|
||||
text: '4',
|
||||
color: '#fd8d3c',
|
||||
}, {
|
||||
text: '3',
|
||||
color: '#feb24c',
|
||||
}, {
|
||||
text: '2',
|
||||
color: '#fed976',
|
||||
}, {
|
||||
text: '1',
|
||||
color: '#ffe8a9',
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { Category } from './categories-config';
|
||||
import AgeContainer from '../building/data-containers/age';
|
||||
import CommunityContainer from '../building/data-containers/community';
|
||||
import ConstructionContainer from '../building/data-containers/construction';
|
||||
import DynamicsContainer from '../building/data-containers/dynamics';
|
||||
import DynamicsContainer from '../building/data-containers/dynamics/dynamics';
|
||||
import LocationContainer from '../building/data-containers/location';
|
||||
import PlanningContainer from '../building/data-containers/planning';
|
||||
import SizeContainer from '../building/data-containers/size';
|
||||
|
@ -411,4 +411,38 @@ export const dataFields = { /* eslint-disable @typescript-eslint/camelcase */
|
||||
title: "Total number of likes",
|
||||
example: 100,
|
||||
},
|
||||
|
||||
demolished_buildings: {
|
||||
category: Category.Dynamics,
|
||||
title: 'Past (demolished) buildings on this site',
|
||||
items: {
|
||||
year_constructed: {
|
||||
title: 'Construction year',
|
||||
example: { min: 1989, max: 1991 },
|
||||
},
|
||||
year_demolished: {
|
||||
title: 'Demolition year',
|
||||
example: { min: 1993, max: 1994 },
|
||||
},
|
||||
lifespan: {
|
||||
title: 'Lifespan',
|
||||
example: "2-5",
|
||||
},
|
||||
overlap_present: {
|
||||
title: 'Roughly what percentage of this building was inside the current site boundary?',
|
||||
tooltip: '',
|
||||
example: "25%"
|
||||
},
|
||||
links: {
|
||||
title: 'Links / sources',
|
||||
example: ["", ""]
|
||||
}
|
||||
},
|
||||
example: [
|
||||
{
|
||||
year_constructed: { min: 1989, max: 1991 },
|
||||
year_demolished: { min: 1993, max: 1994 },
|
||||
lifespan: "2-5", overlap_present: "50%", links: ["", ""]}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
@ -133,6 +133,13 @@ const LAYER_QUERIES = {
|
||||
buildings
|
||||
WHERE
|
||||
current_landuse_order IS NOT NULL`,
|
||||
dynamics_demolished_count: `
|
||||
SELECT
|
||||
geometry_id,
|
||||
jsonb_array_length(demolished_buildings) as demolished_buildings_count
|
||||
FROM
|
||||
buildings
|
||||
WHERE jsonb_array_length(demolished_buildings) > 0`,
|
||||
};
|
||||
|
||||
const GEOMETRY_FIELD = 'geometry_geom';
|
||||
|
2
migrations/019.simple-dynamics.down.sql
Normal file
2
migrations/019.simple-dynamics.down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE buildings
|
||||
DROP demolished_buildings;
|
2
migrations/019.simple-dynamics.up.sql
Normal file
2
migrations/019.simple-dynamics.up.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE buildings
|
||||
ADD column demolished_buildings JSONB DEFAULT '[]'::JSONB;
|
Loading…
Reference in New Issue
Block a user