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
32 changes: 32 additions & 0 deletions src/components/ConditionalAccordionItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import { AccordionItem } from "flowbite-svelte";
import type { Snippet } from "svelte";
import type { ClassValue } from "svelte/elements";

interface Props {
disabled: boolean;
open: boolean;
title: string;
classes: ClassValue;
children: Snippet;
}

let { disabled, open = $bindable(), title, classes, children }: Props = $props();
</script>

{#if !disabled}
<AccordionItem bind:open class={classes}>
{#snippet header()}{title}{/snippet}
{@render children()}
</AccordionItem>
{:else}
<h2 class={["group", classes]}>
<div
class="flex w-full items-center justify-between border-s border-e border-b border-gray-200 bg-gray-100
p-5 text-left font-medium text-gray-400 group-first:rounded-t-xl group-first:border-t dark:border-gray-700
dark:bg-gray-800 dark:text-white"
>
{title}
</div>
</h2>
{/if}
3 changes: 2 additions & 1 deletion src/components/ProjectSetup/LoadFile.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { Fileupload, Label, Helper } from "flowbite-svelte";
import { DataFile } from "$lib/DataFile";
import { store } from "../../store.svelte";
import { store, updateSettingsForNewDataFile } from "../../store.svelte";

const loadFile = async (e) => {
const file = e.target.files[0];
Expand All @@ -10,6 +10,7 @@
store.dataFile = dataFile.loadError ? null : dataFile;
store.errors.loadFile = dataFile.loadError;
store.warnings.loadFile = dataFile.loadWarning;
updateSettingsForNewDataFile();
};
</script>

Expand Down
29 changes: 25 additions & 4 deletions src/components/ProjectSetup/ProjectSetupDialog.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<script lang="ts">
import { Modal, Accordion, AccordionItem } from "flowbite-svelte";
import { Modal, Accordion, AccordionItem, Button } from "flowbite-svelte";
import { store } from "../../store.svelte";
import { ProjectDialog } from "../../types";
import { ProjectDialog, ProjectSetupAccItem } from "../../types";
import LoadFile from "./LoadFile.svelte";
import SetAreas from "./SetAreas.svelte";
import ConditionalAccordionItem from "../ConditionalAccordionItem.svelte";

let isOpen = $derived(store.openProjectDialog == ProjectDialog.Setup);
const openAccItems = $state({
[ProjectSetupAccItem.OpenFile]: true,
[ProjectSetupAccItem.SetAreas]: false
});

const handleClose = () => {
store.openProjectDialog = null;
};
Expand All @@ -19,9 +26,23 @@
onclose={handleClose}
>
<Accordion>
<AccordionItem open>
<AccordionItem bind:open={openAccItems[ProjectSetupAccItem.OpenFile]}>
{#snippet header()}1. Open file{/snippet}
<LoadFile />
<div class="grid">
<LoadFile />
<Button
disabled={!store.dataFile}
onclick={() => (openAccItems[ProjectSetupAccItem.SetAreas] = true)}
class="justify-self-end">Next</Button
>
</div>
</AccordionItem>
<ConditionalAccordionItem
disabled={!store.dataFile}
title="2. Set areas"
bind:open={openAccItems[ProjectSetupAccItem.SetAreas]}
>
<SetAreas />
</ConditionalAccordionItem>
</Accordion>
</Modal>
110 changes: 110 additions & 0 deletions src/components/ProjectSetup/SetAreas.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<script lang="ts">
import { Label, Radio, Select } from "flowbite-svelte";
import { store } from "../../store.svelte.ts";
import {
CountryIdType,
CountrySettingsType,
type MultiCountrySettings,
RegionAdminLevel,
RegionIdType,
type SingleCountrySettings
} from "../../types.ts";
import { stringEnumToSelectItems } from "$lib/utils";

const dataFileColumnItems = $derived(
store.dataFile.columns.map((s) => ({
name: s,
value: s
}))
);

const countryIdTypeItems = stringEnumToSelectItems(CountryIdType);

const countryItems = $derived(
store.countries.map((c) => ({
name: c.name,
value: c.id
}))
);

// When select new country settings type, need to refresh settings object to default for type
let countrySettingsType = {
get value() {
return store.countrySettingsType;
},
set value(v) {
if (v !== store.countrySettingsType) {
store.countrySettingsType = v;
if (v === CountrySettingsType.SingleCountry) {
store.countrySettings = {
countryISO: null
} as SingleCountrySettings;
} else {
store.countrySettings = {
countryIdType: CountryIdType.Name,
countryIdColumn: null
} as MultiCountrySettings;
}
}
}
};

const adminLevelItems = stringEnumToSelectItems(RegionAdminLevel);
const regionIdTypeItems = stringEnumToSelectItems(RegionIdType);
</script>

<Label for="country-settings-type" class="pb-2">The file contains areas for</Label>
<div class="flex">
<Radio
name="country-settings-type"
bind:group={countrySettingsType.value}
value={CountrySettingsType.SingleCountry}
>
A single country
</Radio>
<Radio
name="country-settings-type"
class="ms-4"
bind:group={countrySettingsType.value}
value={CountrySettingsType.MultiCountry}
>
Multiple countries
</Radio>
</div>
{#if countrySettingsType.value === CountrySettingsType.SingleCountry}
<Label for="country" class="mt-4 py-2">Country</Label>
<Select
id="country"
items={countryItems}
bind:value={
() =>
store.countrySettings.countryISO ||
"" /* Need to map null to empty string to show placeholder option */,
(v) => (store.countrySettings.countryISO = v)
}
/>
{:else}
<Label for="country-id-column" class="mt-4 py-2">Country column</Label>
<Select
id="country-id-column"
items={dataFileColumnItems}
bind:value={
() => store.countrySettings.countryIdColumn || "", (v) => (store.countrySettings.countryIdColumn = v)
}
/>

<Label for="country-id-type" class="mt-4 py-2">Countries are identified by</Label>
<Select id="country-id-type" items={countryIdTypeItems} bind:value={store.countrySettings.countryIdType} />
{/if}
<Label for="admin-level" class="mt-4 py-2">Region admin level</Label>
<Select id="admin-level" items={adminLevelItems} bind:value={store.regionSettings.adminLevel} />

<Label for="region-id-column" class="mt-4 py-2">Region column</Label>
<Select
id="region-id-column"
items={dataFileColumnItems}
bind:value={() => store.regionSettings.regionIdColumn || "", (v) => (store.regionSettings.regionIdColumn = v)}
/>

<Label for="region-id-type" class="mt-4 py-2">Regions are identified by</Label>
<Select id="region-id-type" items={regionIdTypeItems} bind:value={store.regionSettings.regionIdType} />
14 changes: 8 additions & 6 deletions src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { ClientInit } from "@sveltejs/kit";
import { store } from "./store.svelte";
import type { AppConfig } from "./types";
import { doFetch } from "$lib/utils";

export const init: ClientInit = async () => {
// TODO: consider fetch wrapper with generic error handling
const response = await fetch("./easymap.config.json");
if (!response.ok) {
store.errors.fetch = "Error fetching app config";
} else {
store.appConfig = (await response.json()) as AppConfig;
const configRes = await doFetch("./easymap.config.json");
if (!configRes.ok) return;
store.appConfig = configRes.json as AppConfig;
const { groutUrl, groutDataset } = store.appConfig;
const countriesRes = await doFetch(`${groutUrl}/region-metadata/${groutDataset}/admin0`);
if (countriesRes.ok) {
store.countries = countriesRes.json.data;
}
};
16 changes: 16 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { store } from "../store.svelte";

export const doFetch = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
store.errors.fetch = `Error fetching from ${url}`;
}
const json = await response.json();
return { ok: response.ok, json };
};

// convert an enum with string values into a list of items for a select item
// NB this will not work for enum with numeric values as entries are duplicated
export const stringEnumToSelectItems = (senum: object) => {
return Object.entries(senum).map((e) => ({ name: e[0], value: e[1] }));
};
50 changes: 47 additions & 3 deletions src/store.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AppConfig } from "./types";
import { ProjectDialog } from "./types";
import type { AppConfig, CountrySettings, MultiCountrySettings, RegionSettings } from "./types";
import { CountrySettingsType, ProjectDialog, RegionAdminLevel, RegionIdType } from "./types";
import type { DataFile } from "$lib/DataFile";
import { SvelteSet } from "svelte/reactivity";

Expand All @@ -8,13 +8,29 @@ export interface StoreProblems {
loadFile?: string;
}

export interface LatLng {
lat: number;
lng: number;
}

export interface Store {
errors: StoreProblems;
warnings: StoreProblems;
appConfig: null | AppConfig;
countries: {
id: string;
name: string;
bounds: {
min: LatLng;
max: LatLng;
};
}[];
openProjectDialog: null | ProjectDialog;
enabledProjectDialogs: Set<ProjectDialog>;
dataFile: null | DataFile;
countrySettingsType: CountrySettingsType;
countrySettings: CountrySettings;
regionSettings: RegionSettings;
}

export const store: Store = $state({
Expand All @@ -23,5 +39,33 @@ export const store: Store = $state({
appConfig: null,
openProjectDialog: ProjectDialog.Setup,
enabledProjectDialogs: new SvelteSet<ProjectDialog>([ProjectDialog.Setup]),
dataFile: null
dataFile: null,
countrySettingsType: CountrySettingsType.SingleCountry,
countrySettings: {
countryISO: null
},
regionSettings: {
adminLevel: RegionAdminLevel.Admin1,
regionIdType: RegionIdType.Name,
regionIdColumn: null
}
});

// When the user uploads a new data file having already chosen settings in the context of a previous
// file, we will try to retain any column references from the previous file, but will remove any which
// are no longer possible because the new file does not have those columns
export const updateSettingsForNewDataFile = () => {
const newColumns = store.dataFile?.columns;
if (store.countrySettingsType == CountrySettingsType.MultiCountry) {
const multiCountrySettings = store.countrySettings as MultiCountrySettings;
const countryIdCol = multiCountrySettings.countryIdColumn;
if (countryIdCol && !newColumns?.includes(countryIdCol)) {
multiCountrySettings.countryIdColumn = null;
}
}

const regionIdCol = store.regionSettings.regionIdColumn;
if (regionIdCol && !newColumns?.includes(regionIdCol)) {
store.regionSettings.regionIdColumn = null;
}
};
44 changes: 44 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
export interface AppConfig {
appTitle: string;
groutUrl: string;
groutDataset: string;
}

export enum ProjectDialog {
Setup,
Download
}

export enum ProjectSetupAccItem {
OpenFile,
SetAreas
}

export enum CountrySettingsType {
SingleCountry,
MultiCountry
}

export enum CountryIdType {
ISO3 = "iso3",
Name = "name"
}

export interface SingleCountrySettings {
countryISO: string | null;
}

export interface MultiCountrySettings {
countryIdType: CountryIdType;
countryIdColumn: string | null;
}

export type CountrySettings = SingleCountrySettings | MultiCountrySettings;

export enum RegionAdminLevel {
Admin1 = "admin1",
Admin2 = "admin2"
}

export enum RegionIdType {
GADM = "gadm",
Name = "name"
}

export interface RegionSettings {
adminLevel: RegionAdminLevel;
regionIdType: RegionIdType;
regionIdColumn: string | null;
}
4 changes: 3 additions & 1 deletion static/easymap.config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"appTitle": "EasyMap"
"appTitle": "EasyMap",
"groutUrl": "https://mrcdata.dide.ic.ac.uk/grout",
"groutDataset": "gadm41"
}
Loading