Commit 048d0ef1 authored by Heisenberg5124's avatar Heisenberg5124
Browse files

Initial commit

parent 36353266
This diff is collapsed.
...@@ -12,7 +12,9 @@ ...@@ -12,7 +12,9 @@
}, },
"dependencies": { "dependencies": {
"@napi-rs/canvas": "^0.1.80", "@napi-rs/canvas": "^0.1.80",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slider": "^1.3.6",
...@@ -31,6 +33,7 @@ ...@@ -31,6 +33,7 @@
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-map-gl": "^8.1.0", "react-map-gl": "^8.1.0",
"recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.14" "tailwindcss": "^4.1.14"
}, },
......
...@@ -4,9 +4,31 @@ import {useState} from "react"; ...@@ -4,9 +4,31 @@ import {useState} from "react";
import {SingleMap} from "@/components/map/single-map.tsx"; import {SingleMap} from "@/components/map/single-map.tsx";
import {SplitMap} from "@/components/map/split-map.tsx"; import {SplitMap} from "@/components/map/split-map.tsx";
import {SplitMapCard} from "@/components/map/split-map-card.tsx"; import {SplitMapCard} from "@/components/map/split-map-card.tsx";
import KpiDialog from "@/components/map/kpi-dialog.tsx";
type Mode = 'single' | 'split'; type Mode = 'single' | 'split';
const kpiMock = {
left: {
year: 2020,
median: 0.22810404002666473,
p95: 0.6100596785545349,
frac_gt_0_4: 0.18748562226823096,
frac_gt_0_6: 0.05244996549344375,
},
right: {
year: 2025,
median: 0.15449757874011993,
p95: 0.4455701410770416,
frac_gt_0_4: 0.07292385553255118,
frac_gt_0_6: 0.0006901311249137336,
},
change: {
fracLoss: 0.3400046008741661, // Δ ≤ -0.1
fracGain: 0.006211180124223602, // Δ ≥ +0.1
},
} as const;
function App() { function App() {
const MIN_YEAR = 2020; const MIN_YEAR = 2020;
const MAX_YEAR = 2025; const MAX_YEAR = 2025;
...@@ -19,6 +41,7 @@ function App() { ...@@ -19,6 +41,7 @@ function App() {
<div className="absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3"> <div className="absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3">
<MapLayerCard/> <MapLayerCard/>
<SplitMapCard mode={mode} setMode={setMode}/> <SplitMapCard mode={mode} setMode={setMode}/>
{mode === 'split' && (<KpiDialog {...kpiMock} />)}
</div> </div>
<div className="h-full w-full"> <div className="h-full w-full">
......
...@@ -7,7 +7,7 @@ An API that returns a satellite image file for a given year and computes NDVI & ...@@ -7,7 +7,7 @@ An API that returns a satellite image file for a given year and computes NDVI &
- Swagger UI: `/docs` - Swagger UI: `/docs`
- ReDoc: `/redoc` - ReDoc: `/redoc`
* OpenAPI spec version: 1.3.0 * OpenAPI spec version: 1.4.0
*/ */
import { import {
useMutation, useMutation,
...@@ -192,6 +192,31 @@ clip_to_aoi?: boolean; ...@@ -192,6 +192,31 @@ clip_to_aoi?: boolean;
compare_res_m?: number; compare_res_m?: number;
}; };
export type NdviChangeValueNdviChangeValueGetParams = {
/**
* @minimum 2013
*/
start_year: number;
/**
* @minimum 2013
*/
end_year: number;
/**
* NDVI summary to compare: mean | median | p05 | p95
*/
metric?: string;
/**
* @minimum 0
* @maximum 100
*/
max_cloud?: number;
/**
* Optional AOI buffer (m)
* @minimum 0
*/
buffer_meters?: number;
};
/** /**
* Returns a GeoTIFF image file for the requested `year`. * Returns a GeoTIFF image file for the requested `year`.
By default returns the best (least cloudy) June–August image intersecting your AOI. By default returns the best (least cloudy) June–August image intersecting your AOI.
...@@ -534,3 +559,96 @@ export function useNdviChangeNdviChangeGet<TData = Awaited<ReturnType<typeof ndv ...@@ -534,3 +559,96 @@ export function useNdviChangeNdviChangeGet<TData = Awaited<ReturnType<typeof ndv
return query; return query;
} }
/**
* @summary Fast NDVI change value (in-memory, no file writes)
*/
export const ndviChangeValueNdviChangeValueGet = (
params: NdviChangeValueNdviChangeValueGetParams,
signal?: AbortSignal
) => {
return apiFetch<unknown>(
{url: `/ndvi-change/value`, method: 'GET',
params, signal
},
);
}
export const getNdviChangeValueNdviChangeValueGetQueryKey = (params?: NdviChangeValueNdviChangeValueGetParams,) => {
return [
`/ndvi-change/value`, ...(params ? [params]: [])
] as const;
}
export const getNdviChangeValueNdviChangeValueGetQueryOptions = <TData = Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError = HTTPValidationError>(params: NdviChangeValueNdviChangeValueGetParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError, TData>>, }
) => {
const {query: queryOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getNdviChangeValueNdviChangeValueGetQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>> = ({ signal }) => ndviChangeValueNdviChangeValueGet(params, signal);
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type NdviChangeValueNdviChangeValueGetQueryResult = NonNullable<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>>
export type NdviChangeValueNdviChangeValueGetQueryError = HTTPValidationError
export function useNdviChangeValueNdviChangeValueGet<TData = Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError = HTTPValidationError>(
params: NdviChangeValueNdviChangeValueGetParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>,
TError,
Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>
> , 'initialData'
>, }
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useNdviChangeValueNdviChangeValueGet<TData = Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError = HTTPValidationError>(
params: NdviChangeValueNdviChangeValueGetParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>,
TError,
Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>
> , 'initialData'
>, }
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useNdviChangeValueNdviChangeValueGet<TData = Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError = HTTPValidationError>(
params: NdviChangeValueNdviChangeValueGetParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError, TData>>, }
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Fast NDVI change value (in-memory, no file writes)
*/
export function useNdviChangeValueNdviChangeValueGet<TData = Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError = HTTPValidationError>(
params: NdviChangeValueNdviChangeValueGetParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof ndviChangeValueNdviChangeValueGet>>, TError, TData>>, }
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getNdviChangeValueNdviChangeValueGetQueryOptions(params,options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {Button} from "@/components/ui/button";
import {Badge} from "@/components/ui/badge";
import {Progress} from "@/components/ui/progress";
import {
ResponsiveContainer,
ComposedChart,
XAxis,
YAxis,
Line,
Scatter,
} from "recharts";
import {BarChart3} from "lucide-react";
import {cn} from "@/lib/utils";
type YearStats = {
year: number;
median: number;
p95: number;
frac_gt_0_4?: number; // 0..1
frac_gt_0_6?: number; // 0..1
};
type ChangeStats = {
fracLoss: number; // ≤ -0.1 (0..1)
fracGain: number; // ≥ +0.1 (0..1)
};
type Props = {
left: YearStats;
right: YearStats;
change?: ChangeStats;
title?: string;
className?: string; // for the trigger button container
open?: boolean;
onOpenChange?: (open: boolean) => void;
triggerLabel?: string;
};
const fmtNDVI = (v: number) => (Number.isFinite(v) ? v.toFixed(3) : "");
const fmtPct = (f?: number) =>
f == null || !Number.isFinite(f) ? "" : `${(f * 100).toFixed(1)}%`;
// tiny slopegraph
function SlopeMini({
a,
b,
domain,
width = 180,
height = 26,
}: {
a: number;
b: number;
domain?: [number, number];
width?: number;
height?: number;
}) {
const both = [
{x: "A", v: a},
{x: "B", v: b},
];
const up = b > a;
const lineColor = up ? "#22c55e" /* emerald-500 */ : "#ef4444" /* rose-500 */;
return (
<ResponsiveContainer width={width} height={height}>
<ComposedChart data={both} margin={{top: 6, bottom: 6, left: 6, right: 6}}>
<XAxis dataKey="x" type="category" hide/>
<YAxis
type="number"
domain={domain ?? [Math.min(a, b), Math.max(a, b) + 1e-6]}
hide
/>
<Line data={both} dataKey="v" type="linear" stroke={lineColor} strokeWidth={2} dot={false}/>
<Scatter data={[both[0]]} dataKey="v" fill="#9ca3af"/> {/* left year */}
<Scatter data={[both[1]]} dataKey="v" fill="#3b82f6"/> {/* right year */}
</ComposedChart>
</ResponsiveContainer>
);
}
function Row({
label,
a,
b,
fmt,
domain,
}: {
label: string;
a: number;
b: number;
fmt: (v: number) => string;
domain?: [number, number];
}) {
return (
<div className="grid grid-cols-[1fr_auto] items-center gap-2">
<div className="text-sm text-muted-foreground">{label}</div>
<div className="flex items-center gap-2">
<span className="text-xs tabular-nums text-muted-foreground">{fmt(a)}</span>
<SlopeMini a={a} b={b} domain={domain}/>
<span
className={cn(
"text-xs font-medium tabular-nums",
b - a >= 0 ? "text-emerald-600" : "text-rose-600"
)}
title="Delta (right - left)"
>
{(b - a >= 0 ? "+" : "") + fmt(b - a)}
</span>
</div>
</div>
);
}
export default function KpiDialog({
left,
right,
change,
title = "NDVI comparison",
className,
open,
onOpenChange,
triggerLabel = "KPIs",
}: Props) {
const ndviDomain: [number, number] = [-0.2, 0.8]; // adjust if needed
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<div className={cn("flex justify-end", className)}>
<DialogTrigger asChild>
<Button size="icon" variant="secondary" className="rounded-full shadow">
<BarChart3 className="h-4 w-4"/>
<span className="sr-only">{triggerLabel}</span>
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-[420px] w-[min(92vw,420px)]">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span>{title}</span>
<span className="flex items-center gap-3">
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<span className="inline-block h-2.5 w-2.5 rounded-full bg-gray-400"/>
{left.year}
</span>
<span className="flex items-center gap-1 text-xs">
<span className="inline-block h-2.5 w-2.5 rounded-full bg-blue-500"/>
{right.year}
</span>
</span>
</DialogTitle>
</DialogHeader>
<div className="space-y-3 pt-1">
<Row label="Median NDVI" a={left.median} b={right.median} fmt={fmtNDVI} domain={ndviDomain}/>
<Row label="p95 NDVI" a={left.p95} b={right.p95} fmt={fmtNDVI} domain={ndviDomain}/>
<Row
label="% area > 0.4"
a={left.frac_gt_0_4 ?? NaN}
b={right.frac_gt_0_4 ?? NaN}
fmt={(v) => fmtPct(v)}
domain={[0, 1]}
/>
<Row
label="% area > 0.6"
a={left.frac_gt_0_6 ?? NaN}
b={right.frac_gt_0_6 ?? NaN}
fmt={(v) => fmtPct(v)}
domain={[0, 1]}
/>
<div className="pt-2 flex items-center justify-between">
<div className="text-sm text-muted-foreground">Δ strong change</div>
<div className="flex gap-2">
<Badge className="bg-rose-50 text-rose-600 border-rose-200" variant="secondary">
Loss {fmtPct(change?.fracLoss)}
</Badge>
<Badge className="bg-emerald-50 text-emerald-600 border-emerald-200" variant="secondary">
Gain {fmtPct(change?.fracGain)}
</Badge>
</div>
</div>
<div className="space-y-2">
<div className="text-xs text-muted-foreground">Loss ≤ −0.1</div>
<Progress
value={Math.max(0, Math.min(100, (change?.fracLoss ?? 0) * 100))}
className="h-2 [&>div]:bg-rose-500"
/>
<div className="text-xs text-muted-foreground pt-2">Gain ≥ +0.1</div>
<Progress
value={Math.max(0, Math.min(100, (change?.fracGain ?? 0) * 100))}
className="h-2 [&>div]:bg-emerald-500"
/>
</div>
</div>
</DialogContent>
</Dialog>
);
}
...@@ -70,13 +70,10 @@ function SplitMap({ ...@@ -70,13 +70,10 @@ function SplitMap({
{/* Bottom-centered selector (robust centering, like slider) */} {/* Bottom-centered selector (robust centering, like slider) */}
<div <div
className="absolute inset-x-0 bottom-5 z-50 flex justify-center pointer-events-none pb-[env(safe-area-inset-bottom)]"> className="absolute inset-x-0 opacity-80 bottom-5 z-50 flex pointer-events-none pb-[env(safe-area-inset-bottom)]">
<div className="pointer-events-auto w-[90%] max-w-sm"> <div className="pointer-events-auto w-[20%] max-w-sm">
<Card className="bg-white/90 backdrop-blur"> <Card className="bg-white/90 backdrop-blur">
<CardContent> <CardContent>
<Label htmlFor="leftYear" className="text-xs text-muted-foreground mb-1 block">
Left year
</Label>
<Select value={String(leftYear)} onValueChange={(v) => setLeftYear(Number(v))}> <Select value={String(leftYear)} onValueChange={(v) => setLeftYear(Number(v))}>
<SelectTrigger id="leftYear" className="h-9"> <SelectTrigger id="leftYear" className="h-9">
<SelectValue placeholder="Select year"/> <SelectValue placeholder="Select year"/>
...@@ -108,13 +105,10 @@ function SplitMap({ ...@@ -108,13 +105,10 @@ function SplitMap({
{/* Bottom-centered selector */} {/* Bottom-centered selector */}
<div <div
className="absolute inset-x-0 bottom-5 z-50 flex justify-center pointer-events-none pb-[env(safe-area-inset-bottom)]"> className="absolute inset-x-0 opacity-80 bottom-5 z-50 flex pointer-events-none pb-[env(safe-area-inset-bottom)]">
<div className="pointer-events-auto w-[90%] max-w-sm"> <div className="pointer-events-auto w-[20%] max-w-sm">
<Card className="bg-white/90 backdrop-blur"> <Card className="bg-white/90 backdrop-blur">
<CardContent> <CardContent>
<Label htmlFor="rightYear" className="text-xs text-muted-foreground mb-1 block">
Right year
</Label>
<Select value={String(rightYear)} onValueChange={(v) => setRightYear(Number(v))}> <Select value={String(rightYear)} onValueChange={(v) => setRightYear(Number(v))}>
<SelectTrigger id="rightYear" className="h-9"> <SelectTrigger id="rightYear" className="h-9">
<SelectValue placeholder="Select year"/> <SelectValue placeholder="Select year"/>
......
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment