diff --git a/web/src/App.svelte b/web/src/App.svelte index 137b8ae..12e16c9 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -5,7 +5,7 @@ import MapC from "src/lib/MapC.svelte"; import InitialErrorScreen from "src/lib/InitialErrorScreen.svelte"; import LoadingScreen from "src/lib/LoadingScreen.svelte"; - import { type LayerName } from "src/constants"; + import { type LayerName } from "src/types"; import { allScenarios, scenarioName, diff --git a/web/src/constants.ts b/web/src/constants.ts deleted file mode 100644 index 9cbc0c2..0000000 --- a/web/src/constants.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* Output indicators */ - -export type IndicatorName = "air_quality" | "house_price" | "job_accessibility" | "greenspace_accessibility"; - -export type Indicator = { short: string, less: string, more: string, less_diff: string, more_diff: string, colormap: string, colormapReversed: boolean }; - -export const allIndicators: Map = new Map([ - ["air_quality", { - "short": "Air pollution", - "less": "cleaner", - "more": "more polluted", - "less_diff": "improved", - "more_diff": "worsened", - "colormap": "magma", - "colormapReversed": false, - }], - ["house_price", { - "short": "House prices", - "less": "cheaper", - "more": "more expensive", - "less_diff": "decreased", - "more_diff": "increased", - "colormap": "viridis", - "colormapReversed": false, - }], - ["job_accessibility", { - "short": "Job accessibility", - "less": "lower", - "more": "higher", - "less_diff": "decreased", - "more_diff": "increased", - "colormap": "plasma", - "colormapReversed": false, - }], - ["greenspace_accessibility", { - "short": "Greenspace accessibility", - "less": "lower", - "more": "higher", - "less_diff": "decreased", - "more_diff": "increased", - "colormap": "chlorophyll", - "colormapReversed": true, - }], -]); - - -/* Model inputs (land use) */ - -export type InputName = "signature_type"; - -export type Input = { short: string }; - -export const allInputs: Map = new Map([ - ["signature_type", { "short": "Spatial signatures" }] -]); - -/* A generic type, handling all inputs and outputs which can be visualised as a - * map layer */ - -export type LayerName = InputName | IndicatorName; - -export type Layer = { short: string }; - -export const allLayers: Map = new Map( - [...allInputs.entries(), ...allIndicators.entries()] -); - -/* Scenarios */ -// Most of this code was shifted to scenarios.ts. - -export type OA = string; - -// Range to scale all indicator values to -export const GLOBALMIN = 0; -export const GLOBALMAX = 100; - -// Macro variables. Ideally, these would just be `Input`s, but we don't actually -// plot most of these yet (only signature types), so these have to be given a -// new type. -export type MacroVar = "signature_type" | "use" | "greenspace" | "job_types"; - - -// Scenarios - -export type ScenarioMetadata = { - name: string, // unique identifier - short: string, // short description (for dropdown box) - long: string, // long description (title in UI) - description: string, // full text description -}; -export type ScenarioChanges = Map>; -export type ScenarioValues = Map>; - -export type Scenario = { - metadata: ScenarioMetadata, // as described above - changes: ScenarioChanges, // inputs changed relative to baseline - values: ScenarioValues // values of all indicators and inputs -}; - -// Corresponding JSON types -export type ChangesObject = { [oa: string]: { [mv in MacroVar]: number } } -export type ValuesObject = { [oa: string]: { [ln in LayerName]: number } } -export type MetadataObject = { name: string, short: string, long: string, description: string } -export type ScenarioObject = { metadata: MetadataObject, changes: ChangesObject, values: ValuesObject } - -export type ScaleFactorMap = Map; - -export const signatures = [ - { name: "Wild countryside", color: "#d7ded1" }, - { name: "Countryside agriculture", color: "#f2e6c7" }, - { name: "Urban buffer", color: "#c2d0d9" }, - { name: "Warehouse/Park land", color: "#c3abaf" }, - { name: "Open sprawl", color: "#d7a59f" }, - { name: "Disconnected suburbia", color: "#f0d17d" }, - { name: "Accessible suburbia", color: "#8fa37e" }, - { name: "Connected residential neighbourhoods", color: "#94666e" }, - { name: "Dense residential neighbourhoods", color: "#678ea6" }, - { name: "Gridded residential quarters", color: "#e4cbc8" }, - { name: "Dense urban neighbourhoods", color: "#efc758" }, - { name: "Local urbanity", color: "#3b6e8c" }, - { name: "Regional urbanity", color: "#ab888e" }, - { name: "Metropolitan urbanity", color: "#bc5b4f" }, - { name: "Concentrated urbanity", color: "#333432" }, - { name: "Hyper concentrated urbanity", color: "#a7b799" }, -]; diff --git a/web/src/data/config.ts b/web/src/data/config.ts index 9aa77e9..ae23347 100644 --- a/web/src/data/config.ts +++ b/web/src/data/config.ts @@ -1,4 +1,32 @@ -import type { ScenarioObject } from "src/constants"; +import type { ScenarioObject, IndicatorName, Indicator, + InputName, Input, LayerName, Layer +} from "src/types"; + +/* --------------------------------- */ +/* GEOGRAPHY */ +/* --------------------------------- */ + +// This must be a FeatureCollection GeoJSON that covers the area of interest. +// Here, we use a .json extension so that TypeScript can properly import it. +import geography from "src/data/geography.json"; + +// Each feature in the GeoJSON file must contain a property that gives a unique +// identifier for each feature. The value of the identifier must be a string. +const featureIdentifier = "OA11CD"; + +// Initial latitude of the map +const initialLatitude = 54.94; +// Initial longitude of the map +const initialLongitude = -1.59; +// Initial zoom level of the map +const initialZoom = 10.05; + +/* --------------------------------- */ +/* SCENARIOS */ +/* --------------------------------- */ + +// These are JSON files containing the spatial signature and indicator values +// for each scenario. import baseline from "src/data/scenarios/baseline.json"; import scenario1 from "src/data/scenarios/scenario1.json"; import scenario2 from "src/data/scenarios/scenario2.json"; @@ -8,37 +36,152 @@ import scenario5 from "src/data/scenarios/scenario5.json"; import scenario6 from "src/data/scenarios/scenario6.json"; import scenario7 from "src/data/scenarios/scenario7.json"; +// One of the scenarios is used as the 'reference', against which values are +// scaled. +const referenceScenarioFile = baseline; + +// Range to scale all indicator values between (for the baseline). +const scale = { + min: 0, + max: 100, +} + +// List all the other scenarios here. +const otherScenarioFiles = [ + scenario1, + scenario1, + scenario2, + scenario3, + scenario4, + scenario5, + scenario6, + scenario7 +] + +/* --------------------------------- */ +/* INDICATORS */ +/* --------------------------------- */ + +const allIndicators: Map = new Map([ + // The IndicatorName here must match with the name of the indicator given in + // the JSON file. + ["air_quality", { + // This is an actual prose name of the indicator used in the UI. + "short": "Air pollution", + // 'less' and 'more' are used in the charts to describe what smaller and + // larger values mean respectively. + "less": "cleaner", + "more": "more polluted", + // 'less_diff' and 'more_diff' are used in charts that compare two + // scenarios. 'less_diff' should describe what happens for areas where + // the value of the indicator decreases, and vice versa for 'more_diff'. + "less_diff": "improved", + "more_diff": "worsened", + // The name of the colormap to use for this indicator, and whether to + // reverse it. A list of available names can be found at + // https://www.npmjs.com/package/colormap. + "colormap": "magma", + "colormapReversed": false, + }], + ["house_price", { + "short": "House prices", + "less": "cheaper", + "more": "more expensive", + "less_diff": "decreased", + "more_diff": "increased", + "colormap": "viridis", + "colormapReversed": false, + }], + ["job_accessibility", { + "short": "Job accessibility", + "less": "lower", + "more": "higher", + "less_diff": "decreased", + "more_diff": "increased", + "colormap": "plasma", + "colormapReversed": false, + }], + ["greenspace_accessibility", { + "short": "Greenspace accessibility", + "less": "lower", + "more": "higher", + "less_diff": "decreased", + "more_diff": "increased", + "colormap": "chlorophyll", + "colormapReversed": true, + }], +]); + +/* --------------------------------- */ +/* SPATIAL SIGNATURES */ +/* --------------------------------- */ + +// This probably does not need to be changed. Note that the order of the +// signatures is important: signatures are stored as integers in the JSON files +// and this list is indexed using those integers to get the correct signature. +const signatures = [ + { name: "Wild countryside", color: "#d7ded1" }, + { name: "Countryside agriculture", color: "#f2e6c7" }, + { name: "Urban buffer", color: "#c2d0d9" }, + { name: "Warehouse/Park land", color: "#c3abaf" }, + { name: "Open sprawl", color: "#d7a59f" }, + { name: "Disconnected suburbia", color: "#f0d17d" }, + { name: "Accessible suburbia", color: "#8fa37e" }, + { name: "Connected residential neighbourhoods", color: "#94666e" }, + { name: "Dense residential neighbourhoods", color: "#678ea6" }, + { name: "Gridded residential quarters", color: "#e4cbc8" }, + { name: "Dense urban neighbourhoods", color: "#efc758" }, + { name: "Local urbanity", color: "#3b6e8c" }, + { name: "Regional urbanity", color: "#ab888e" }, + { name: "Metropolitan urbanity", color: "#bc5b4f" }, + { name: "Concentrated urbanity", color: "#333432" }, + { name: "Hyper concentrated urbanity", color: "#a7b799" }, +]; + +/* --------------------------------- */ +/* Everything after this does not */ +/* need to be modified. */ +/* --------------------------------- */ + +const allInputs: Map = new Map([ + ["signature_type", { "short": "Spatial signatures" }] +]); + +const allLayers: Map = new Map( + [...allInputs.entries(), ...allIndicators.entries()] +); + interface Config { + geography: GeoJSON.FeatureCollection; featureIdentifier: string; initialLatitude: number; initialLongitude: number; initialZoom: number; referenceScenarioFile: ScenarioObject; otherScenarioFiles: ScenarioObject[]; + signatures: { name: string, color: string }[]; + allIndicators: Map; + allInputs: Map; + allLayers: Map; + scale: { + min: number; + max: number; + }; } const config: Config = { - // GeoJSON key which gives a unique identifier for each feature. The value - // of the identifier must be a string. - featureIdentifier: "OA11CD", - // Initial latitude of the map - initialLatitude: 54.94, - // Initial longitude of the map - initialLongitude: -1.59, - // Initial zoom level of the map - initialZoom: 10.05, - // Imported JSON object of reference scenario to scale values against - referenceScenarioFile: baseline, - // Imported JSON objects for all other scenarios - otherScenarioFiles: [ - scenario1, - scenario2, - scenario3, - scenario4, - scenario5, - scenario6, - scenario7 - ] + initialLatitude: initialLatitude, + initialLongitude: initialLongitude, + initialZoom: initialZoom, + geography: geography as GeoJSON.FeatureCollection, + featureIdentifier: featureIdentifier, + referenceScenarioFile: referenceScenarioFile, + otherScenarioFiles: otherScenarioFiles, + signatures: signatures, + allIndicators: allIndicators, + allInputs: allInputs, + allLayers: allLayers, + scale: scale, }; export default config; diff --git a/web/src/initialise.ts b/web/src/initialise.ts index 6629111..90e2f90 100644 --- a/web/src/initialise.ts +++ b/web/src/initialise.ts @@ -1,8 +1,7 @@ import { type Scenario, type ScaleFactorMap, - allLayers, -} from "src/constants"; +} from "src/types"; import { fromScenarioObject } from "src/utils/scenarios"; import config from "src/data/config"; @@ -32,7 +31,7 @@ export function setupAreaNames(referenceScenario: Scenario): Set { export function setupScaleFactors(referenceScenarioUnscaled: Scenario): ScaleFactorMap { // Calculate current minimum and maximum values const scaleFactors: ScaleFactorMap = new Map(); - for (const layerName of allLayers.keys()) { + for (const layerName of config.allLayers.keys()) { const allValues = []; for (const oaValues of referenceScenarioUnscaled.values.values()) { allValues.push(oaValues.get(layerName)); diff --git a/web/src/lib/MapC.svelte b/web/src/lib/MapC.svelte index 14fe626..ffa178c 100644 --- a/web/src/lib/MapC.svelte +++ b/web/src/lib/MapC.svelte @@ -8,7 +8,7 @@ getInputDiffBoundaries, } from "src/utils/geojson"; import { makePopup } from "src/utils/hover"; - import { allLayers, type LayerName } from "src/constants"; + import { type LayerName } from "src/types"; import { allScenarios, scenarioName, @@ -267,7 +267,7 @@ // The way around it is to use fill-opacity (which is not data-driven) // for four different layers. The only real drawback is (in principle) // performance, but I haven't really noticed any issues so far. - for (const layerName of allLayers.keys()) { + for (const layerName of config.allLayers.keys()) { const mapLayerId = `${layerName}-layer`; map.addLayer({ id: mapLayerId, @@ -337,7 +337,7 @@ // Fade in the layers that we want, after a small delay to allow for // loading. setTimeout(function () { - for (const layerName of allLayers.keys()) { + for (const layerName of config.allLayers.keys()) { const mapLayerId = `${layerName}-layer`; map.setPaintProperty( mapLayerId, @@ -366,7 +366,7 @@ // as the opacity slider. export function updateLayers() { if (map) { - for (const layerName of allLayers.keys()) { + for (const layerName of config.allLayers.keys()) { map.setPaintProperty(`${layerName}-layer`, "fill-color", [ "get", $compareScenarioName === null diff --git a/web/src/lib/RightSidebar.svelte b/web/src/lib/RightSidebar.svelte index 1a8f175..bf1711e 100644 --- a/web/src/lib/RightSidebar.svelte +++ b/web/src/lib/RightSidebar.svelte @@ -5,8 +5,8 @@ import Collapsible from "src/lib/reusable/Collapsible.svelte"; import { type LayerName, - allIndicators, - } from "src/constants"; + } from "src/types"; + import config from "src/data/config"; export let activeLayer: LayerName; export let opacity: number; @@ -33,7 +33,7 @@ /> - {#each [...allIndicators.entries()] as [indiName, indi]} + {#each [...config.allIndicators.entries()] as [indiName, indi]} - {#each [...signatures.entries()] as [signatureId, signature]} + {#each [...config.signatures.entries()] as [signatureId, signature]} @@ -289,7 +289,7 @@ {:else}