Skip to content

Commit 8b026f8

Browse files
committed
organize code
1 parent f43888c commit 8b026f8

File tree

4 files changed

+147
-104
lines changed

4 files changed

+147
-104
lines changed

client/components/HousesMap.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Map } from "react-map-gl/maplibre";
2+
import { useEffect, useMemo, useState, type ReactNode } from "react";
3+
import DeckGL, { ScatterplotLayer, type MapViewState } from "deck.gl";
4+
import {
5+
type House,
6+
} from "@/api";
7+
8+
9+
export function HousesMap({
10+
houses,
11+
mapStyle,
12+
children,
13+
}: {
14+
houses: House[],
15+
mapStyle: string,
16+
children?: ReactNode,
17+
}) {
18+
19+
const gradientStops = useMemo<{ t: number; color: [number, number, number] }[]>(() => [
20+
{ t: 0.00, color: [255, 255, 255] }, // white
21+
{ t: 0.15, color: [200, 255, 200] }, // mint green
22+
{ t: 0.20, color: [120, 220, 120] }, // light green
23+
{ t: 0.35, color: [0, 180, 0] }, // green
24+
{ t: 0.5, color: [255, 255, 100] }, // yellow
25+
{ t: 0.65, color: [255, 180, 0] }, // orange
26+
{ t: 0.75, color: [255, 90, 0] }, // red
27+
{ t: 0.88, color: [255, 0, 0] }, // dark red
28+
{ t: 1.00, color: [140, 0, 0] }, // maroon
29+
], []);
30+
31+
const { min, max, data, latitude, longitude } = useMemo(() => {
32+
const data = houses.filter(x => typeof x.unitPrice === 'number' && x.location !== null && typeof x.location.lat === 'number' && typeof x.location.lng === 'number') ?? [];
33+
const sortedPrices = data.map(x => x.unitPrice!).sort((a, b) => a - b);
34+
const center: NonNullable<House['location']> = {
35+
lat: data.reduce((p, c)=>p + c.location!.lat, 0) / data.length,
36+
lng: data.reduce((p, c)=>p + c.location!.lng, 0) / data.length,
37+
};
38+
return {
39+
data,
40+
min: sortedPrices[Math.floor((sortedPrices.length - 1) * gradientStops[1].t)],
41+
max: sortedPrices[Math.floor((sortedPrices.length - 1) * gradientStops[gradientStops.length - 2].t)],
42+
latitude: isNaN(center.lat) ? 0 : center.lat,
43+
longitude: isNaN(center.lng) ? 0 : center.lng,
44+
};
45+
}, [houses, gradientStops]);
46+
47+
48+
const [viewState, setViewState] = useState<MapViewState>({
49+
latitude: 0,
50+
longitude: 0,
51+
zoom: 13,
52+
});
53+
54+
useEffect(() => {
55+
setViewState(p => ({ ...p, latitude, longitude }))
56+
}, [latitude, longitude]);
57+
58+
59+
const heatLayer = new ScatterplotLayer({
60+
id: "house-layer",
61+
data,
62+
getPosition: (d: House) => [d.location?.lng ?? 0, d.location?.lat ?? 0],
63+
getRadius: () => {
64+
return 50;
65+
},
66+
radiusUnits: "meters",
67+
getFillColor: (d: House) => {
68+
const price = d.unitPrice ?? 0;
69+
const t = (Math.max(min, Math.min(price, max)) - min) / (max - min);
70+
for (let i = 0; i < gradientStops.length - 1; i++) {
71+
const curr = gradientStops[i];
72+
const next = gradientStops[i + 1];
73+
74+
if (t >= curr.t && t <= next.t) {
75+
const localT = (t - curr.t) / (next.t - curr.t);
76+
const r = Math.round(curr.color[0] + (next.color[0] - curr.color[0]) * localT);
77+
const g = Math.round(curr.color[1] + (next.color[1] - curr.color[1]) * localT);
78+
const b = Math.round(curr.color[2] + (next.color[2] - curr.color[2]) * localT);
79+
return [r, g, b];
80+
}
81+
}
82+
83+
return gradientStops[gradientStops.length - 1].color;
84+
},
85+
onClick: ({ object }) => {
86+
alert(JSON.stringify(object, null, 4))
87+
},
88+
opacity: 0.9,
89+
stroked: false,
90+
pickable: true,
91+
updateTriggers: {
92+
getRadius: [viewState.zoom],
93+
},
94+
});
95+
96+
return (
97+
<DeckGL
98+
viewState={viewState}
99+
controller={true}
100+
onViewStateChange={({ viewState: newViewState }) => {
101+
if (data.length) {
102+
setViewState(newViewState as MapViewState)
103+
}
104+
}}
105+
layers={[heatLayer]}
106+
>
107+
<Map
108+
mapStyle={mapStyle}
109+
attributionControl={false}
110+
/>
111+
{children}
112+
</DeckGL>
113+
);
114+
}

client/components/MapStyleSelect.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { getMapStyles } from "@/api/map";
22
import { Select } from "@radix-ui/themes";
3-
import type { ComponentProps } from "react";
3+
import { useEffect, type ComponentProps } from "react";
44
import { useAsync } from "react-use";
55

66
export function MapStyleSelect({ disabled, ...props }: ComponentProps<typeof Select.Root>) {
77
const mapStyles = useAsync(() => getMapStyles());
8+
useEffect(() => {
9+
if (!props.value && mapStyles.value?.[0].value) {
10+
props.onValueChange?.(mapStyles.value?.[0].value);
11+
}
12+
}, [mapStyles.value, props, props.value]);
13+
814
return (
9-
<Select.Root disabled={disabled ?? (!!mapStyles.error || mapStyles.loading)} {...props}>
15+
<Select.Root disabled={disabled ?? (!!mapStyles.error || mapStyles.loading)} defaultValue={mapStyles.value?.[0].value} {...props}>
1016
<Select.Trigger />
1117
<Select.Content>
1218
{mapStyles.value?.map((mapStyle) => (

client/utils/colors.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

client/views/Root.tsx

Lines changed: 25 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { Button, Progress } from "@radix-ui/themes";
2-
import { Map } from "react-map-gl/maplibre";
32
import { useState } from "react";
43
import { VscRunAbove } from "react-icons/vsc";
5-
import DeckGL, { ScatterplotLayer, type MapViewState } from "deck.gl";
64
import {
75
getHouses,
86
type House,
97
type SearchHousesFilters,
108
} from "@/api";
119
import { useAsync, useAsyncFn, useLocalStorage } from "react-use";
1210
import { MapStyleSelect } from "@/components/MapStyleSelect";
13-
import { interpolateColor } from "@/utils/colors";
1411
import { ofetch } from "ofetch";
12+
import { HousesMap } from "@/components/HousesMap";
1513

1614

1715

@@ -22,87 +20,42 @@ export function Root() {
2220
}
2321
);
2422

25-
const [mapStyle, setMapStyle] = useLocalStorage('map-style', 'https://raw.githubusercontent.com/go2garret/maps/main/src/assets/json/openStreetMap.json');
23+
const [mapStyle, setMapStyle] = useLocalStorage<string>('map-style');
2624

2725
const [progress, setProgress] = useState<number>(0);
2826

29-
const lastSavedHouses = useAsync(() => ofetch<House[]>('./tehran.json'));
27+
const lastSavedHouses = useAsync(() => ofetch<House[]>(new URL('./tehran.json', window.location.origin + window.location.pathname).href));
3028
const [houses, startSearch] = useAsyncFn(() => {
3129
return getHouses(filters, (p) => {
3230
setProgress(p);
3331
console.log(p);
3432
});
3533
}, [filters]);
3634

37-
const [viewState, setViewState] = useState<MapViewState>({
38-
latitude: 35.695,
39-
longitude: 51.395,
40-
zoom: 13,
41-
bearing: 0,
42-
pitch: 0,
43-
});
44-
45-
const data = (houses.value ?? lastSavedHouses.value)?.filter(x=>typeof x.unitPrice === 'number') ?? [];
46-
const sortedPrices = data.map(x => x.unitPrice!).sort((a, b) => a - b);
47-
const min = sortedPrices[Math.floor((sortedPrices.length - 1) * 0.15)];
48-
const max = sortedPrices[Math.floor((sortedPrices.length - 1) * 0.88)];
49-
50-
const heatLayer = new ScatterplotLayer({
51-
id: "price-layer",
52-
data: data,
53-
getPosition: (d: House) => [d.location?.lng ?? 0, d.location?.lat ?? 0],
54-
getRadius: () => {
55-
return 50;
56-
},
57-
radiusUnits: "meters",
58-
getFillColor: (d: House) => {
59-
const price = d.unitPrice ?? 0;
60-
const t = (Math.max(min, Math.min(price, max)) - min) / (max - min);
61-
return interpolateColor(t);
62-
},
63-
onClick: ({ object }) => {
64-
alert(JSON.stringify(object, null, 4))
65-
},
66-
opacity: 0.9,
67-
stroked: false,
68-
pickable: true,
69-
updateTriggers: {
70-
getRadius: [viewState.zoom],
71-
},
72-
});
7335

7436
return (
75-
<div className="w-svw h-svh">
76-
<DeckGL
77-
viewState={viewState}
78-
controller={true}
79-
onViewStateChange={({ viewState }) =>
80-
setViewState(viewState as MapViewState)
81-
}
82-
layers={[heatLayer]}
83-
>
84-
<Map
85-
mapStyle={mapStyle}
86-
attributionControl={false}
87-
/>
88-
<div className="absolute top-0 left-0 w-full p-2 flex items-center justify-center gap-2 backdrop-contrast-50 backdrop-blur-sm">
89-
<Progress size="2" max={1} value={progress} color="green" />
90-
<Button
91-
onClick={() => {
92-
startSearch();
93-
}}
94-
color="green"
95-
size="2"
96-
variant="classic"
97-
loading={houses.loading}
98-
>
99-
<VscRunAbove />
100-
Start Crawl Divar
101-
</Button>
102-
|
103-
<MapStyleSelect value={mapStyle} onValueChange={setMapStyle} size="2" />
104-
</div>
105-
</DeckGL>
37+
<div className="w-svw h-svh relative">
38+
<HousesMap
39+
houses={houses.value ?? lastSavedHouses.value ?? []}
40+
mapStyle={mapStyle!}
41+
/>
42+
<div className="absolute top-0 left-0 w-full p-2 flex items-center justify-center gap-2 backdrop-contrast-50 backdrop-blur-sm">
43+
<Progress size="2" max={1} value={progress} color="green" />
44+
<Button
45+
onClick={() => {
46+
startSearch();
47+
}}
48+
color="green"
49+
size="2"
50+
variant="classic"
51+
loading={houses.loading}
52+
>
53+
<VscRunAbove />
54+
Start Crawl Divar
55+
</Button>
56+
|
57+
<MapStyleSelect value={mapStyle} onValueChange={setMapStyle} size="2" />
58+
</div>
10659
</div>
10760
);
10861
}

0 commit comments

Comments
 (0)