Skip to content

Commit 2f859bf

Browse files
committed
extract socrata fetch and query logic out of use-citations hook
1 parent 302ad39 commit 2f859bf

2 files changed

Lines changed: 93 additions & 42 deletions

File tree

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,23 @@
11
import { useQuery } from "@tanstack/react-query";
2-
import { multiPolygon } from "@turf/turf";
32
import _ from "lodash";
4-
import { type GeoJSONGeometry, stringify } from "wellknown";
53
import { usePublicConfig } from "@/hooks/use-public-config";
6-
import { ParkingCitationFeatureCollection, ParkingCitationProperties } from "@/types";
4+
import { fetchParkingCitations } from "@/lib/socrata/parking-citations";
75
import { useStore } from "./use-store";
86

9-
const GEO_LOCATION_COLUMN = "geocodelocation";
10-
const ISSUE_DATE_COLUMN = "issue_date";
11-
12-
const EMPTY_PARKING_CITATION_FEATURE_COLLECTION = {
13-
type: "FeatureCollection",
14-
features: [] satisfies ParkingCitationProperties[],
15-
} satisfies ParkingCitationFeatureCollection;
16-
177
export const useCitations = () => {
188
const { data } = usePublicConfig();
199
const { places, range } = useStore((state) => ({ places: state.getPlaces(), range: state.range }));
2010

2111
const placeIds = _.chain(places).map("id").compact().uniq().sort().value();
2212

23-
const buildQuery = () => {
24-
const startDate = range.from.toISOString().split("T")[0];
25-
const endDate = range.to.toISOString().split("T")[0];
26-
const dateFilter = `${ISSUE_DATE_COLUMN} BETWEEN '${startDate}' AND '${endDate}'`;
27-
28-
if (!placeIds.length) return `SELECT * WHERE ${dateFilter}`;
29-
30-
const coordinates = _.chain(places).map("geometry.coordinates").compact().value();
31-
const multipolygon = multiPolygon(coordinates);
32-
const wkt = stringify(multipolygon.geometry as GeoJSONGeometry);
33-
const geoFilter = `within_polygon(${GEO_LOCATION_COLUMN}, '${wkt}')`;
34-
return `SELECT * WHERE ${dateFilter} AND ${geoFilter}`;
35-
};
36-
3713
return useQuery({
3814
queryKey: ["citations", range.from.toISOString(), range.to.toISOString(), placeIds],
39-
queryFn: async (): Promise<ParkingCitationFeatureCollection> => {
40-
if (!(placeIds.length && data?.socrataAppToken)) return EMPTY_PARKING_CITATION_FEATURE_COLLECTION;
41-
42-
const response = await fetch("https://data.lacity.org/api/v3/views/4f5p-udkv/query", {
43-
method: "POST",
44-
headers: {
45-
Accept: "application/vnd.geo+json",
46-
"Accept-Charset": "utf-8",
47-
"Content-Type": "application/json",
48-
"X-App-Token": data.socrataAppToken,
49-
},
50-
// FIXME: Remove limit (pagination) when implementing full data fetching
51-
body: JSON.stringify({ query: buildQuery(), page: { pageNumber: 1, pageSize: 50 } }),
52-
});
53-
54-
return response.ok ? response.json() : EMPTY_PARKING_CITATION_FEATURE_COLLECTION;
55-
},
15+
queryFn: () =>
16+
fetchParkingCitations({
17+
token: data?.socrataAppToken,
18+
places,
19+
range,
20+
}),
5621
staleTime: 60_000, // 1 minute
5722
});
5823
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { multiPolygon } from "@turf/turf";
2+
import _ from "lodash";
3+
import { type GeoJSONGeometry, stringify } from "wellknown";
4+
import { ParkingCitationFeatureCollection, ParkingCitationProperties } from "@/types";
5+
6+
type FetchParkingCitationsInput = {
7+
token?: string;
8+
places: Array<{
9+
id?: string | number | null;
10+
geometry?: {
11+
coordinates?: unknown;
12+
} | null;
13+
}>;
14+
range: {
15+
from: Date;
16+
to: Date;
17+
};
18+
};
19+
20+
const SOCRATA_PARKING_CITATIONS_URL = "https://data.lacity.org/api/v3/views/4f5p-udkv/query";
21+
22+
const GEO_LOCATION_COLUMN = "geocodelocation";
23+
const ISSUE_DATE_COLUMN = "issue_date";
24+
25+
const EMPTY_PARKING_CITATION_FEATURE_COLLECTION = {
26+
type: "FeatureCollection",
27+
features: [] satisfies ParkingCitationProperties[],
28+
} satisfies ParkingCitationFeatureCollection;
29+
30+
const toSocrataDate = (date: Date) => date.toISOString().split("T")[0];
31+
32+
const buildParkingCitationsQuery = ({ places, range }: Pick<FetchParkingCitationsInput, "places" | "range">) => {
33+
const startDate = toSocrataDate(range.from);
34+
const endDate = toSocrataDate(range.to);
35+
const dateFilter = `${ISSUE_DATE_COLUMN} BETWEEN '${startDate}' AND '${endDate}'`;
36+
37+
const placeIds = _.chain(places).map("id").compact().uniq().sort().value();
38+
39+
if (!placeIds.length) {
40+
return `SELECT * WHERE ${dateFilter}`;
41+
}
42+
43+
const coordinates = _.chain(places).map("geometry.coordinates").compact().value();
44+
45+
const multipolygon = multiPolygon(coordinates);
46+
const wkt = stringify(multipolygon.geometry as GeoJSONGeometry);
47+
const geoFilter = `within_polygon(${GEO_LOCATION_COLUMN}, '${wkt}')`;
48+
49+
return `SELECT * WHERE ${dateFilter} AND ${geoFilter}`;
50+
};
51+
52+
export const fetchParkingCitations = async ({
53+
token,
54+
places,
55+
range,
56+
}: FetchParkingCitationsInput): Promise<ParkingCitationFeatureCollection> => {
57+
const placeIds = _.chain(places).map("id").compact().uniq().sort().value();
58+
59+
if (!(placeIds.length && token)) {
60+
return EMPTY_PARKING_CITATION_FEATURE_COLLECTION;
61+
}
62+
63+
const response = await fetch(SOCRATA_PARKING_CITATIONS_URL, {
64+
method: "POST",
65+
headers: {
66+
Accept: "application/vnd.geo+json",
67+
"Accept-Charset": "utf-8",
68+
"Content-Type": "application/json",
69+
"X-App-Token": token,
70+
},
71+
body: JSON.stringify({
72+
query: buildParkingCitationsQuery({ places, range }),
73+
// FIXME: Remove limit (pagination) when implementing full data fetching
74+
page: {
75+
pageNumber: 1,
76+
pageSize: 50,
77+
},
78+
}),
79+
});
80+
81+
if (!response.ok) {
82+
return EMPTY_PARKING_CITATION_FEATURE_COLLECTION;
83+
}
84+
85+
return response.json();
86+
};

0 commit comments

Comments
 (0)