Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app/map_styles/polygon.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,13 @@
<LineSymbolizer stroke="#95beba" stroke-width="2.5"/>
</Rule>
</Style>
<Style name="planning_world_heritage_buildings">
<Rule>
<Filter>[listing_type] = "In World Heritage Site"</Filter>
<PolygonSymbolizer fill="#0bbf12"/>
<LineSymbolizer stroke="#0bbf12" stroke-width="1.75" />
</Rule>
</Style>
<Style name="sust_dec">
<Rule>
<Filter>[sust_dec] = A</Filter>
Expand Down Expand Up @@ -2515,8 +2522,8 @@
</Rule>
<Rule>
<Filter>[sust_aggregate_estimate_epc] is null</Filter>
<PolygonSymbolizer fill="#c0c0c0" />
<LineSymbolizer stroke="#c0c0c0" stroke-width="2.0"/>
<PolygonSymbolizer fill="#909090" />
<LineSymbolizer stroke="#909090" stroke-width="2.0"/>
</Rule>

<Rule>
Expand Down
13 changes: 7 additions & 6 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions app/public/geometries/world_heritage_sites_simplified_1m.geojson

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ const PlanningConservationView: React.FunctionComponent<CategoryViewProps> = (pr
e.preventDefault();
props.onMapColourScale('planning_combined')
}
const switchToBuildingProtectionWorldHeritageMapStyle = (e) => {
e.preventDefault();
props.onMapColourScale('planning_world_heritage_buildings')
}
const switchToAllPlanningApplicationsMapStyle = (e) => {
e.preventDefault();
props.onMapColourScale('planning_applications_status_all')
Expand All @@ -80,7 +84,7 @@ const PlanningConservationView: React.FunctionComponent<CategoryViewProps> = (pr
props.onMapColourScale('community_local_significance_total')
}

const { housing, housingSwitchOnClick, creative, creativeSwitchOnClick, vista, vistaSwitchOnClick, conservation, conservationSwitchOnClick, darkLightTheme } = useDisplayPreferences();
const { housing, housingSwitchOnClick, creative, creativeSwitchOnClick, vista, vistaSwitchOnClick, conservation, conservationSwitchOnClick, worldHeritageSites, worldHeritageSitesSwitchOnClick, darkLightTheme } = useDisplayPreferences();

const communityLinkUrl = `/${props.mode}/${Category.Community}/${props.building.building_id}`;
const currentYear = new Date().getFullYear();
Expand Down Expand Up @@ -466,6 +470,16 @@ const PlanningConservationView: React.FunctionComponent<CategoryViewProps> = (pr
/>
</>
}
{props.mapColourScale != "planning_world_heritage_buildings" ?
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict inequality operator (!==) instead of loose inequality (!=). In TypeScript/JavaScript, it's best practice to use !== for comparisons to avoid type coercion issues.

Copilot uses AI. Check for mistakes.
<button className={`map-switcher-inline disabled-state btn btn-outline btn-outline-dark key-button`} onClick={switchToBuildingProtectionWorldHeritageMapStyle}>
{'Click to see buildings within World Heritage Sites'}
</button>
:
<></>
}
<button className={`map-switcher-inline ${worldHeritageSites}-state btn btn-outline btn-outline-dark ${darkLightTheme}`} onClick={worldHeritageSitesSwitchOnClick}>
{(worldHeritageSites === 'enabled')? 'Click to hide World Heritage Sites Boundaries' : 'Click to see World Heritage Sites Boundaries'}
</button>
<hr/>
<LogicalDataEntry
slug='planning_listed'
Expand Down
18 changes: 14 additions & 4 deletions app/src/frontend/config/category-maps-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
{
mapStyle: 'sust_aggregate_estimate_epc',
legend: {
title: 'Energy rating (EPC)',
title: 'Energy rating (residential)',
description: 'Residential energy rating (EPC Rating)',
disclaimer: 'This map shows official 2025 EPC data, required for new, sold and rented buildings. Please note EPC ratings may be out-of-date, as retrofit may have occurred since certification.',
elements: [
Expand All @@ -242,15 +242,15 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
{ color: "#f7af1d", text: 'E' },
{ color: "#ed6823", text: 'F' },
{ color: "#e31d23", text: 'G' },
{ color: "#c0c0c0", text: 'No EPC identified' },
{ color: "#909090", text: 'No EPC identified or non-residential.' },
]
},
},
{
mapStyle: 'sust_dec',
legend: {
title: 'Energy rating (DEC)',
description: 'Non-domestic energy rating (DEC Rating)',
title: 'Energy rating (non-residential)',
description: 'Non-residential energy rating (DEC Rating)',
elements: [
{ color: "#007f3d", text: 'A' },
{ color: "#2c9f29", text: 'B' },
Expand Down Expand Up @@ -355,6 +355,16 @@ export const categoryMapsConfig: {[key in Category]: CategoryMapDefinition[]} =
]
},
},
{
mapStyle: 'planning_world_heritage_buildings',
legend: {
title: 'Buildings in World Heritage Sites (official and crowdsourced data)',
disclaimer: 'All data relating to designated buildings should be checked against the National Heritage List for England and local authority websites. Designation data is currently incomplete.',
elements: [
{ color: '#0bbf12', text: 'In World Heritage Site'},
]
},
},
{
mapStyle: 'community_local_significance_total',
legend: {
Expand Down
1 change: 1 addition & 0 deletions app/src/frontend/config/tileserver-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type BuildingMapTileset =
'planning_applications_status_recent' |
'planning_applications_status_very_recent' |
'planning_combined' |
'planning_world_heritage_buildings' |
'sust_dec' |
'building_attachment_form' |
'landuse' |
Expand Down
32 changes: 32 additions & 0 deletions app/src/frontend/displayPreferences-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ interface DisplayPreferencesContextState {
conservationSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
conservationSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;

worldHeritageSites: LayerEnablementState;
worldHeritageSitesSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
worldHeritageSitesSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;

parcel: LayerEnablementState;
parcelSwitch: (e: React.FormEvent<HTMLFormElement>) => void;
parcelSwitchOnClick: React.MouseEventHandler<HTMLButtonElement>;
Expand Down Expand Up @@ -119,6 +123,10 @@ export const DisplayPreferencesContext = createContext<DisplayPreferencesContext
conservationSwitch: stub,
conservationSwitchOnClick: undefined,

worldHeritageSites: undefined,
worldHeritageSitesSwitch: stub,
worldHeritageSitesSwitchOnClick: undefined,

parcel: undefined,
parcelSwitch: stub,
parcelSwitchOnClick: undefined,
Expand Down Expand Up @@ -194,6 +202,7 @@ export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
const defaultBorough = 'enabled'
const defaultParcel = 'disabled'
const defaultConservation = 'disabled'
const defaultWorldHeritageSites = 'disabled'
const defaultHistoricData = 'disabled'
const defaultHistoricMap = 'disabled'
const defaultHistoricMapLeicestershire = 'disabled'
Expand All @@ -214,6 +223,7 @@ export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
const [borough, setBorough] = useState<LayerEnablementState>(defaultBorough);
const [parcel, setParcel] = useState<LayerEnablementState>(defaultParcel);
const [conservation, setConservation] = useState<LayerEnablementState>(defaultConservation);
const [worldHeritageSites, setWorldHeritageSites] = useState<LayerEnablementState>(defaultWorldHeritageSites);
const [historicData, setHistoricData] = useState<LayerEnablementState>(defaultHistoricData);
const [historicMap, setHistoricMap] = useState<LayerEnablementState>(defaultHistoricMap);
const [historicMapLeicestershire, setHistoricMapLeicestershire] = useState<LayerEnablementState>(defaultHistoricMapLeicestershire);
Expand Down Expand Up @@ -246,6 +256,7 @@ export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
setBorough(defaultBorough)
setParcel(defaultParcel);
setConservation(defaultConservation);
setWorldHeritageSites(defaultWorldHeritageSites);
setHistoricData(defaultHistoricData);
setHistoricMap(defaultHistoricMap);
setHistoricMapLeicestershire(defaultHistoricMapLeicestershire);
Expand Down Expand Up @@ -285,6 +296,9 @@ export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
if(conservation != defaultConservation) {
return true;
}
if(worldHeritageSites != defaultWorldHeritageSites) {
return true;
}
if(historicData != defaultHistoricData) {
return true;
}
Expand Down Expand Up @@ -431,6 +445,21 @@ export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
setConservation(newConservation);
}

const worldHeritageSitesSwitch = useCallback(
(e) => {
flipWorldHeritageSites(e)
},
[worldHeritageSites],
)
const worldHeritageSitesSwitchOnClick = (e) => {
flipWorldHeritageSites(e)
}
function flipWorldHeritageSites(e) {
e.preventDefault();
const newWorldHeritageSites = (worldHeritageSites === 'enabled')? 'disabled' : 'enabled';
setWorldHeritageSites(newWorldHeritageSites);
}

const historicDataSwitch = useCallback(
(e) => {
if (historicMap === 'enabled') {
Expand Down Expand Up @@ -697,6 +726,9 @@ export const DisplayPreferencesProvider: React.FC<{}> = ({children}) => {
conservation,
conservationSwitch,
conservationSwitchOnClick,
worldHeritageSites,
worldHeritageSitesSwitch,
worldHeritageSitesSwitchOnClick,
parcel,
parcelSwitch,
parcelSwitchOnClick,
Expand Down
27 changes: 27 additions & 0 deletions app/src/frontend/map/layers/world-heritage-sites-layer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

import { GeoJsonObject } from 'geojson';
import React, { useEffect, useState } from 'react';
import { GeoJSON } from 'react-leaflet';
import { apiGet } from '../../apiHelpers';
import { useDisplayPreferences } from '../../displayPreferences-context';

export function WorldHeritageSitesLayer() {
const [worldHeritageSitesGeojson, setWorldHeritageSitesGeojson] = useState<GeoJsonObject>(null);
const { worldHeritageSites } = useDisplayPreferences();

useEffect(() => {
apiGet('/geometries/world_heritage_sites_simplified_1m.geojson')
.then(data => setWorldHeritageSitesGeojson(data as GeoJsonObject));
}, []);
Comment on lines +12 to +15
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API call lacks error handling. If the fetch fails, the error will be silently ignored and worldHeritageSitesGeojson will remain null. Consider adding a .catch() handler to log errors or handle failures gracefully.

Copilot uses AI. Check for mistakes.

if(worldHeritageSites == "enabled") {
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality operator (===) instead of loose equality (==). In TypeScript/JavaScript, it's best practice to use === for comparisons to avoid type coercion issues.

Copilot uses AI. Check for mistakes.
return worldHeritageSitesGeojson &&
<GeoJSON
attribution='<a href="https://historicengland.org.uk/terms/website-terms-conditions/open-data-hub/">Historic England - Open Government Licence.</a>'
data={worldHeritageSitesGeojson}
style={{color: '#80ff80', fill: true, weight: 1, opacity: 1, fillOpacity: 0.5}}
/>;
} else if (worldHeritageSites == "disabled") {
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use strict equality operator (===) instead of loose equality (==). In TypeScript/JavaScript, it's best practice to use === for comparisons to avoid type coercion issues.

Copilot uses AI. Check for mistakes.
return <div></div>
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an empty div is unnecessary. When the layer should not be rendered, return null instead. This avoids adding an empty DOM element to the component tree.

Copilot uses AI. Check for mistakes.
}
}
Comment on lines +17 to +27
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component doesn't handle the case when worldHeritageSites is undefined (the default value in the context). Add handling for undefined state or ensure the component returns null when the state is neither "enabled" nor "disabled".

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion app/src/frontend/map/legend.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
font-size: 17px;
border: 1px solid;
border-radius: 4px;
min-height: 1.2em;
min-height: 2.2em;
}

.map-legend .h4,
Expand Down
4 changes: 4 additions & 0 deletions app/src/frontend/map/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AerialPhotosMapLayer } from './layers/aerial-photos-map-layer';
import { OpenStreetMapLayer } from './layers/openstreetmap-layer';
import { FloodBoundaryLayer } from './layers/flood-boundary-layer';
import { ConservationAreaBoundaryLayer } from './layers/conservation-boundary-layer';
import { WorldHeritageSitesLayer } from './layers/world-heritage-sites-layer';
import { VistaBoundaryLayer } from './layers/vista-boundary-layer';
import { CeremonialCountiesLayer } from './layers/ceremonial-counties-layer';
import { RegionsLayer } from './layers/regions-layer';
Expand All @@ -43,6 +44,7 @@ import { BoroughSwitcher } from './borough-switcher';
import { ParcelSwitcher } from './parcel-switcher';
import { FloodSwitcher } from './flood-switcher';
import { ConservationAreaSwitcher } from './conservation-switcher';
import { WorldHeritageSitesSwitcher } from './world-heritage-sites-switcher';
import { HistoricDataSwitcher } from './historic-data-switcher';
import { HistoricMapSwitcher } from './historic-map-switcher';
import { HistoricMapLeicestershireSwitcher } from './historic-map-leicestershire-switcher';
Expand Down Expand Up @@ -153,6 +155,7 @@ export const ColouringMap : FC<ColouringMapProps> = ({
name='cc-overlay-pane'
style={{zIndex: 300}}
>
<WorldHeritageSitesLayer/>
<ConservationAreaBoundaryLayer/>
<HistoricDataLayer revisionId={revisionId} />
<HistoricMapLayer />
Expand Down Expand Up @@ -205,6 +208,7 @@ export const ColouringMap : FC<ColouringMapProps> = ({
<ParcelSwitcher/>
<FloodSwitcher/>
<ConservationAreaSwitcher/>
<WorldHeritageSitesSwitcher/>
{ /* <HistoricMapSwitcher/> */ }
{ /* <HistoricDataSwitcher/> */ }
{ /* <HistoricMapLeicestershireSwitcher/> */ }
Expand Down
16 changes: 16 additions & 0 deletions app/src/frontend/map/world-heritage-sites-switcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

import './map-button.css';
import { useDisplayPreferences } from '../displayPreferences-context';

export const WorldHeritageSitesSwitcher: React.FC<{}> = () => {
const { worldHeritageSites, worldHeritageSitesSwitch, darkLightTheme } = useDisplayPreferences();
return (
<form className={`map-button ${worldHeritageSites}-state ${darkLightTheme}`} onSubmit={worldHeritageSitesSwitch}>
<button className="btn btn-outline btn-outline-dark"
type="submit">
{(worldHeritageSites === 'enabled')? 'World Heritage Sites [on]' : 'World Heritage Sites [off]'}
</button>
</form>
);
}
13 changes: 13 additions & 0 deletions app/src/tiles/dataDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,19 @@ const LAYER_QUERIES = {
OR planning_heritage_at_risk_url <> ''
OR planning_in_apa_url <> ''
`,
planning_world_heritage_buildings: `
SELECT
geometry_id,
(
CASE
WHEN planning_world_list_id IS NOT NULL THEN 'In World Heritage Site'
ELSE 'None'
END
) AS listing_type
FROM buildings
WHERE
planning_world_list_id IS NOT NULL
`,
team_known_designer: `
SELECT
geometry_id,
Expand Down