diff --git a/app/src/app/components/sidebar/ColorPicker.tsx b/app/src/app/components/sidebar/ColorPicker.tsx index 39f2b72f7..0d048e899 100644 --- a/app/src/app/components/sidebar/ColorPicker.tsx +++ b/app/src/app/components/sidebar/ColorPicker.tsx @@ -3,6 +3,7 @@ import {Button, Checkbox, CheckboxGroup} from '@radix-ui/themes'; import {styled} from '@stitches/react'; import * as RadioGroup from '@radix-ui/react-radio-group'; import {blackA} from '@radix-ui/colors'; +import {useMapStore} from '@/app/store/mapStore'; type ColorPickerProps = T extends true ? { @@ -27,6 +28,8 @@ export const ColorPicker = ({ colorArray, multiple, }: ColorPickerProps) => { + const mapDocument = useMapStore(state => state.mapDocument); + if (multiple) { return (
@@ -63,11 +66,12 @@ export const ColorPicker = ({ value={value !== undefined ? colorArray[value] : undefined} defaultValue={colorArray[defaultValue]} > - {colorArray.map((color, i) => ( - - - - ))} + {mapDocument && + colorArray.slice(0, mapDocument.num_districts ?? 0).map((color, i) => ( + + + + ))}
); diff --git a/app/src/app/components/sidebar/Evaluation.tsx b/app/src/app/components/sidebar/Evaluation.tsx index af31c3a09..c39cebf27 100644 --- a/app/src/app/components/sidebar/Evaluation.tsx +++ b/app/src/app/components/sidebar/Evaluation.tsx @@ -3,24 +3,17 @@ import {useMapStore} from '@/app/store/mapStore'; import {useQuery} from '@tanstack/react-query'; import { CleanedP1ZoneSummaryStats, - CleanedP1ZoneSummaryStatsKeys, getP1SummaryStats, P1ZoneSummaryStats, P1ZoneSummaryStatsKeys, } from '@/app/utils/api/apiHandlers'; -import {Button, Checkbox, CheckboxGroup} from '@radix-ui/themes'; -import {Heading, Flex, Spinner, Text} from '@radix-ui/themes'; +import {Button, CheckboxGroup} from '@radix-ui/themes'; +import {Flex, Spinner, Text} from '@radix-ui/themes'; import {queryClient} from '@utils/api/queryClient'; import {formatNumber, NumberFormats} from '@/app/utils/numbers'; import {colorScheme} from '@/app/constants/colors'; -import { - getEntryTotal, - getStdDevColor, - stdDevArray, - stdDevColors, - sumArray, -} from '@utils/summaryStats'; -import {interpolateBlues, interpolateGreys} from 'd3-scale-chromatic'; +import {getEntryTotal} from '@utils/summaryStats'; +import {interpolateGreys} from 'd3-scale-chromatic'; type EvalModes = 'share' | 'count' | 'totpop'; type ColumnConfiguration> = Array<{label: string; column: keyof T}>; @@ -28,23 +21,6 @@ type EvaluationProps = { columnConfig?: ColumnConfiguration; }; -// const calculateColumn = ( -// mode: EvalModes, -// entry: P1ZoneSummaryStats, -// totals: P1ZoneSummaryStats, -// column: keyof Omit -// ) => { -// const count = entry[column]; -// switch (mode) { -// case 'count': -// return count; -// case 'pct': -// return count / entry['total']; -// case 'share': -// return count / totals[column]; -// } -// }; - const defaultColumnConfig: ColumnConfiguration = [ { label: 'White', @@ -66,6 +42,10 @@ const defaultColumnConfig: ColumnConfiguration = [ label: 'Pacific Isl.', column: 'nhpi_pop', }, + { + label: 'Two or More Races', + column: 'two_or_more_races_pop', + }, { label: 'Other', column: 'other_pop', @@ -81,10 +61,6 @@ const modeButtonConfig: Array<{label: string; value: EvalModes}> = [ label: 'Population by Count', value: 'count', }, - // { - // label: "Population by Percent of Zone", - // value: 'totpop' - // } ]; const numberFormats: Record = { @@ -104,8 +80,6 @@ const getColConfig = (evalMode: EvalModes) => { const Evaluation: React.FC = ({columnConfig = defaultColumnConfig}) => { const [evalMode, setEvalMode] = useState('share'); - // const [showAverages, setShowAverages] = useState(true); - // const [showStdDev, setShowStdDev] = useState(false); const [colorBg, setColorBg] = useState(true); const [showUnassigned, setShowUnassigned] = useState(true); @@ -152,18 +126,10 @@ const Evaluation: React.FC = ({columnConfig = defaultColumnConf unassigned[`${key}_pct`] = total / unassigned[key]; unassigned[key] = total; }); - // const averages: Record = {}; - // const stdDevs: Record = {}; - // CleanedP1ZoneSummaryStatsKeys.forEach(key => { - // const values = data.results.map(row => row[key]); - // averages[key] = sumArray(values) / data.results.length; - // stdDevs[key] = stdDevArray(values); - // }); + return { unassigned, maxValues, - // averages, - // stdDevs }; }, [data?.results, totPop]); @@ -210,29 +176,9 @@ const Evaluation: React.FC = ({columnConfig = defaultColumnConf setShowUnassigned(v => !v)}> Show Unassigned Population - {/* setShowAverages(v => !v)}> - Show Zone Averages - - setShowStdDev(v => !v)}> - Show Zone Std. Dev. - */} setColorBg(v => !v)}>

Color Cells By Values

- {/* {colorByStdDev && ( - - {Object.entries(stdDevColors) - .sort((a, b) => +a[0] - +b[0]) - .map(([stdev, backgroundColor], i) => ( - - {+stdev > 0 ? `+${stdev}`: stdev} - - ))} - - )} */}
@@ -250,30 +196,6 @@ const Evaluation: React.FC = ({columnConfig = defaultColumnConf - {/* {!!(averages && showAverages) && ( - - - Zone Averages - - {columnConfig.map((f, i) => ( - - {formatNumber(averages[columnGetter(f.column)], numberFormat)} - - ))} - - )} - {!!(stdDevs && showStdDev) && ( - - - Zone Std. Dev. - - {columnConfig.map((f, i) => ( - - {formatNumber(stdDevs[columnGetter(f.column)], numberFormat)} - - ))} - - )} */} {rows .sort((a, b) => a.zone - b.zone) .map(row => { diff --git a/app/src/app/components/sidebar/charts/HorizontalBarChart.tsx b/app/src/app/components/sidebar/charts/HorizontalBarChart.tsx index 74ed8b4da..8fa91d975 100644 --- a/app/src/app/components/sidebar/charts/HorizontalBarChart.tsx +++ b/app/src/app/components/sidebar/charts/HorizontalBarChart.tsx @@ -1,7 +1,18 @@ import {useMapStore} from '@/app/store/mapStore'; import {Card, Flex, Heading, Text} from '@radix-ui/themes'; -import {BarChart, Bar, ResponsiveContainer, Tooltip, XAxis, YAxis, Cell} from 'recharts'; +import { + BarChart, + Bar, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, + Cell, + ReferenceLine, + Label, +} from 'recharts'; import {colorScheme} from '@/app/constants/colors'; +import {useState, useMemo} from 'react'; type TooltipInput = { active?: boolean; @@ -24,6 +35,38 @@ const CustomTooltip = ({active, payload: items}: TooltipInput) => { export const HorizontalBar = () => { const mapMetrics = useMapStore(state => state.mapMetrics); + const summaryStats = useMapStore(state => state.summaryStats); + const numDistricts = useMapStore(state => state.mapDocument?.num_districts); + const idealPopulation = summaryStats?.idealpop?.data; + const maxNumberOrderedBars = 40; // max number of zones to consider while keeping blank spaces for missing zones + const [totalExpectedBars, setTotalExpectedBars] = useState< + Array<{zone: number; total_pop: number}> + >([]); + + const calculateChartObject = () => { + if ((numDistricts ?? 0) < maxNumberOrderedBars) { + return mapMetrics && mapMetrics.data && numDistricts + ? Array.from({length: numDistricts}, (_, i) => i + 1).reduce( + (acc, district) => { + const totalPop = mapMetrics.data.reduce((acc, entry) => { + return entry.zone === district ? acc + entry.total_pop : acc; + }, 0); + return [...acc, {zone: district, total_pop: totalPop}]; + }, + [] as Array<{zone: number; total_pop: number}> + ) + : []; + } else { + return mapMetrics?.data ?? []; + } + }; + + useMemo(() => { + if (mapMetrics) { + const chartObject = calculateChartObject(); + setTotalExpectedBars(chartObject); + } + }, [mapMetrics]); if (mapMetrics?.isPending) { return
Loading...
; @@ -44,30 +87,49 @@ export const HorizontalBar = () => { return ( - Population by Zone + Population by district - - + + + idealPopulation + ? Math.round(Math.max(idealPopulation * 2, dataMax + 1000)) + : dataMax, + ]} tickFormatter={value => numberFormat.format(value)} /> - + } /> - {mapMetrics.data - .sort((a, b) => a.zone - b.zone) - .map((entry, index) => ( - - ))} + {totalExpectedBars && + totalExpectedBars + .sort((a, b) => a.zone - b.zone) + .map((entry, index) => ( + + ))} + + diff --git a/app/src/app/store/mapStore.ts b/app/src/app/store/mapStore.ts index dccfb99d7..d1864251b 100644 --- a/app/src/app/store/mapStore.ts +++ b/app/src/app/store/mapStore.ts @@ -23,19 +23,19 @@ import { getFeaturesInBbox, resetZoneColors, setZones, -} from "../utils/helpers"; -import { getRenderSubscriptions } from "./mapRenderSubs"; -import { getSearchParamsObersver } from "../utils/api/queryParamsListener"; -import { getMapMetricsSubs } from "./metricsSubs"; -import { getMapEditSubs } from "./mapEditSubs"; -import { getQueriesResultsSubs } from "../utils/api/queries"; -import { persistOptions } from "./persistConfig"; +} from '../utils/helpers'; +import {getRenderSubscriptions} from './mapRenderSubs'; +import {getSearchParamsObersver} from '../utils/api/queryParamsListener'; +import {getMapMetricsSubs} from './metricsSubs'; +import {getMapEditSubs} from './mapEditSubs'; +import {getQueriesResultsSubs} from '../utils/api/queries'; +import {persistOptions} from './persistConfig'; import {patchReset, patchShatter, patchUnShatter} from '../utils/api/mutations'; import bbox from '@turf/bbox'; import {BLOCK_SOURCE_ID} from '../constants/layers'; import {DistrictrMapOptions} from './types'; -import { onlyUnique } from '../utils/arrays'; -import { queryClient } from '../utils/api/queryClient'; +import {onlyUnique} from '../utils/arrays'; +import {queryClient} from '../utils/api/queryClient'; const combineSetValues = (setRecord: Record>, keys?: string[]) => { const combinedSet = new Set(); // Create a new set to hold combined values @@ -80,13 +80,16 @@ export interface MapStore { setMapDocument: (mapDocument: DocumentObject) => void; summaryStats: { totpop?: { - data: P1TotPopSummaryStats - } - }, + data: P1TotPopSummaryStats; + }; + idealpop?: { + data: number; + }; + }; setSummaryStat: ( stat: T, value: MapStore['summaryStats'][T] - ) => void, + ) => void; // SHATTERING /** * A subset of IDs that a user is working on in a focused view. @@ -375,20 +378,20 @@ export const useMapStore = create( setFreshMap, resetZoneAssignments, upsertUserMap, - mapOptions + mapOptions, } = get(); if (currentMapDocument?.document_id === mapDocument.document_id) { return; } - setFreshMap(true) - resetZoneAssignments() - upsertUserMap({mapDocument}) - + setFreshMap(true); + resetZoneAssignments(); + upsertUserMap({mapDocument}); + set({ mapDocument: mapDocument, mapOptions: { ...mapOptions, - bounds: mapDocument.extent + bounds: mapDocument.extent, }, shatterIds: {parents: new Set(), children: new Set()}, }); @@ -398,9 +401,9 @@ export const useMapStore = create( set({ summaryStats: { ...get().summaryStats, - [stat]: value - } - }) + [stat]: value, + }, + }); }, // TODO: Refactor to something like this // featureStates: { @@ -581,13 +584,16 @@ export const useMapStore = create( delete shatterMappings[parent.parentId]; newShatterIds.parents.delete(parent.parentId); newZoneAssignments.set(parent.parentId, parent.zone!); - mapRef?.setFeatureState({ - source: BLOCK_SOURCE_ID, - id: parent.parentId, - sourceLayer: mapDocument?.parent_layer, - }, { - broken: false - }); + mapRef?.setFeatureState( + { + source: BLOCK_SOURCE_ID, + id: parent.parentId, + sourceLayer: mapDocument?.parent_layer, + }, + { + broken: false, + } + ); }); set({ @@ -633,8 +639,12 @@ export const useMapStore = create( userMaps.splice(i, 1, userMapData); // Replace the map at index i with the new data } else { const urlParams = new URL(window.location.href).searchParams; - urlParams.delete("document_id"); // Remove the document_id parameter - window.history.pushState({}, '', window.location.pathname + '?' + urlParams.toString()); // Update the URL without document_id + urlParams.delete('document_id'); // Remove the document_id parameter + window.history.pushState( + {}, + '', + window.location.pathname + '?' + urlParams.toString() + ); // Update the URL without document_id userMaps.splice(i, 1); } } @@ -783,8 +793,8 @@ export const useMapStore = create( selectedZone: 1, setSelectedZone: zone => set({selectedZone: zone}), zoneAssignments: new Map(), - assignmentsHash: "", - setAssignmentsHash: (hash) => set({ assignmentsHash: hash }), + assignmentsHash: '', + setAssignmentsHash: hash => set({assignmentsHash: hash}), accumulatedGeoids: new Set(), setAccumulatedGeoids: accumulatedGeoids => set({accumulatedGeoids}), setZoneAssignments: (zone, geoids) => { diff --git a/app/src/app/utils/api/apiHandlers.ts b/app/src/app/utils/api/apiHandlers.ts index e9886b4ae..1f4828633 100644 --- a/app/src/app/utils/api/apiHandlers.ts +++ b/app/src/app/utils/api/apiHandlers.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import 'maplibre-gl'; import {useMapStore} from '@/app/store/mapStore'; -import { getEntryTotal } from '../summaryStats'; +import {getEntryTotal} from '../summaryStats'; export const FormatAssignments = () => { const assignments = Array.from(useMapStore.getState().zoneAssignments.entries()).map( @@ -171,8 +171,9 @@ export interface P1ZoneSummaryStats { nhpi_pop: number; black_pop: number; white_pop: number; + two_or_more_races_pop: number; } -export type P1TotPopSummaryStats = Omit +export type P1TotPopSummaryStats = Omit; export const P1ZoneSummaryStatsKeys = [ 'other_pop', @@ -180,8 +181,9 @@ export const P1ZoneSummaryStatsKeys = [ 'amin_pop', 'nhpi_pop', 'black_pop', - 'white_pop' -] as const + 'white_pop', + 'two_or_more_races_pop', +] as const; export const CleanedP1ZoneSummaryStatsKeys = [ ...P1ZoneSummaryStatsKeys, @@ -192,7 +194,8 @@ export const CleanedP1ZoneSummaryStatsKeys = [ 'nhpi_pop_pct', 'black_pop_pct', 'white_pop_pct', -] as const + 'two_or_more_races_pop_pct', +] as const; export interface CleanedP1ZoneSummaryStats extends P1ZoneSummaryStats { total: number; @@ -202,6 +205,7 @@ export interface CleanedP1ZoneSummaryStats extends P1ZoneSummaryStats { nhpi_pop_pct: number; black_pop_pct: number; white_pop_pct: number; + two_or_more_races_pop_pct: number; } /** @@ -214,23 +218,28 @@ export const getP1SummaryStats: ( ) => Promise> = async mapDocument => { if (mapDocument) { return await axios - .get>(`${process.env.NEXT_PUBLIC_API_URL}/api/document/${mapDocument.document_id}/P1`) + .get< + SummaryStatsResult + >(`${process.env.NEXT_PUBLIC_API_URL}/api/document/${mapDocument.document_id}/P1`) .then(res => { const results = res.data.results.map(row => { - const total = getEntryTotal(row) - return P1ZoneSummaryStatsKeys.reduce((acc, key) => { - acc[`${key}_pct`] = acc[key] / total; - return acc; - }, { - ...row, - total - }) as CleanedP1ZoneSummaryStats - }) - return { - ...res.data, - results - } - }) + const total = getEntryTotal(row); + return P1ZoneSummaryStatsKeys.reduce( + (acc, key) => { + acc[`${key}_pct`] = acc[key] / total; + return acc; + }, + { + ...row, + total, + } + ) as CleanedP1ZoneSummaryStats; + }); + return { + ...res.data, + results, + }; + }); } else { throw new Error('No document provided'); } @@ -246,8 +255,10 @@ export const getP1TotPopSummaryStats: ( ) => Promise> = async mapDocument => { if (mapDocument) { return await axios - .get>(`${process.env.NEXT_PUBLIC_API_URL}/api/districtrmap/summary_stats/P1/${mapDocument.parent_layer}`) - .then(res => res.data) + .get< + SummaryStatsResult + >(`${process.env.NEXT_PUBLIC_API_URL}/api/districtrmap/summary_stats/P1/${mapDocument.parent_layer}`) + .then(res => res.data); } else { throw new Error('No document provided'); } diff --git a/app/src/app/utils/api/queries.ts b/app/src/app/utils/api/queries.ts index 2549b3c0c..f4bbed4e6 100644 --- a/app/src/app/utils/api/queries.ts +++ b/app/src/app/utils/api/queries.ts @@ -13,6 +13,7 @@ import { getP1TotPopSummaryStats, P1TotPopSummaryStats, } from './apiHandlers'; +import {getEntryTotal} from '@/app/utils/summaryStats'; import {MapStore, useMapStore} from '@/app/store/mapStore'; const INITIAL_VIEW_LIMIT = 30; @@ -20,17 +21,20 @@ const INITIAL_VIEW_OFFSET = 0; /** * A utility function that returns a query function based on a nullable parameter. - * + * * @param callback - A function that takes a parameter of type ParamT and returns a Promise of type ResultT. * @param nullableParam - An optional parameter of type ParamT. If this parameter is not provided or is falsy, the function will return a function that returns null. - * - * @returns A function that, when called, will either return null (if nullableParam is not provided) + * + * @returns A function that, when called, will either return null (if nullableParam is not provided) * or call the callback function with the nullableParam and return its result. - * + * * @template ParamT - The type of the parameter that the callback function accepts. * @template ResultT - The type of the result that the callback function returns. */ -const getNullableParamQuery = (callback: (param: ParamT) => Promise, nullableParam?: ParamT) => { +const getNullableParamQuery = ( + callback: (param: ParamT) => Promise, + nullableParam?: ParamT +) => { if (!nullableParam) return () => null; return async () => await callback(nullableParam); }; @@ -71,9 +75,15 @@ export const getQueriesResultsSubs = (_useMapStore: typeof useMapStore) => { }); fetchTotPop.subscribe(response => { if (response?.data?.results) { - useMapStore.getState().setSummaryStat('totpop', { data: response.data.results}); + console.log(response?.data?.results); + useMapStore.getState().setSummaryStat('totpop', {data: response.data.results}); + useMapStore.getState().setSummaryStat('idealpop', { + data: + getEntryTotal(response.data.results) / + (useMapStore.getState().mapDocument?.num_districts ?? 1), + }); } else { - useMapStore.getState().setSummaryStat('totpop', undefined) + useMapStore.getState().setSummaryStat('totpop', undefined); } }); }; @@ -110,7 +120,7 @@ updateDocumentFromId.subscribe(mapDocument => { export const fetchAssignments = new QueryObserver(queryClient, { queryKey: ['assignments'], - queryFn: getNullableParamQuery(getAssignments) + queryFn: getNullableParamQuery(getAssignments), }); export const updateAssignments = (mapDocument: DocumentObject) => { @@ -126,10 +136,16 @@ fetchAssignments.subscribe(assignments => { } }); -export const fetchTotPop = new QueryObserver | null>(queryClient, { - queryKey: ['gerrydb_tot_pop'], - queryFn: getNullableParamQuery>(getP1TotPopSummaryStats), -}); +export const fetchTotPop = new QueryObserver | null>( + queryClient, + { + queryKey: ['gerrydb_tot_pop'], + queryFn: getNullableParamQuery< + MapStore['mapDocument'], + SummaryStatsResult + >(getP1TotPopSummaryStats), + } +); export const updateTotPop = (mapDocument: DocumentObject | null) => { fetchTotPop.setOptions({ @@ -137,4 +153,3 @@ export const updateTotPop = (mapDocument: DocumentObject | null) => { queryKey: ['gerrydb_tot_pop', mapDocument?.gerrydb_table], }); }; - diff --git a/backend/app/alembic/versions/65a4fc0a727d_add_unshatter_udf.py b/backend/app/alembic/versions/65a4fc0a727d_add_unshatter_udf.py index ab9041a44..6477c689e 100644 --- a/backend/app/alembic/versions/65a4fc0a727d_add_unshatter_udf.py +++ b/backend/app/alembic/versions/65a4fc0a727d_add_unshatter_udf.py @@ -19,7 +19,6 @@ revision: str = "65a4fc0a727d" down_revision: Union[str, None] = "dc391733e10a" branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: diff --git a/backend/app/alembic/versions/c3541f016d35_add_available_stats_to_districtrmap.py b/backend/app/alembic/versions/c3541f016d35_add_available_stats_to_districtrmap.py index 091aa3bf3..89844bac1 100644 --- a/backend/app/alembic/versions/c3541f016d35_add_available_stats_to_districtrmap.py +++ b/backend/app/alembic/versions/c3541f016d35_add_available_stats_to_districtrmap.py @@ -26,7 +26,8 @@ def upgrade() -> None: ) op.execute( - sa.text(""" + sa.text( + """ UPDATE districtrmap d SET available_summary_stats = ( SELECT @@ -40,7 +41,8 @@ def upgrade() -> None: (SELECT ARRAY_AGG(summary_stat) FROM get_available_summary_stats(d.parent_layer)) END ) - """) + """ + ) ) diff --git a/backend/app/alembic/versions/d90c9a1a246b_missing_two_or_more_races.py b/backend/app/alembic/versions/d90c9a1a246b_missing_two_or_more_races.py new file mode 100644 index 000000000..204c8eaeb --- /dev/null +++ b/backend/app/alembic/versions/d90c9a1a246b_missing_two_or_more_races.py @@ -0,0 +1,60 @@ +"""missing two or more races + +Revision ID: d90c9a1a246b +Revises: 2494caf34886 +Create Date: 2024-11-18 23:56:24.881723 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from app.constants import SQL_DIR +from pathlib import Path + + +# revision identifiers, used by Alembic. +revision: str = "d90c9a1a246b" +down_revision: Union[str, None] = "2494caf34886" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + for udf in [ + "available_summary_stat_udf.sql", + "summary_stats_p1.sql", + "summary_stats_p1_totals.sql", + "summary_stats_p4.sql", + "summary_stats_p4_totals.sql", + ]: + with Path(SQL_DIR, udf).open() as f: + sql = f.read() + op.execute(sql) + + op.execute( + sa.text( + """ + UPDATE districtrmap d + SET available_summary_stats = ( + SELECT + CASE WHEN d.child_layer IS NOT NULL THEN + ( + SELECT ARRAY_AGG(summary_stat) FROM get_available_summary_stats(d.child_layer) + INTERSECT + SELECT ARRAY_AGG(summary_stat) FROM get_available_summary_stats(d.parent_layer) + ) + ELSE + (SELECT ARRAY_AGG(summary_stat) FROM get_available_summary_stats(d.parent_layer)) + END + ) + """ + ) + ) + + +def downgrade() -> None: + # Since the previous migraiton touching this logic was buggy, not going to + # to write a downgrade for it. + pass diff --git a/backend/app/main.py b/backend/app/main.py index 104ab2be1..f7313a370 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -94,6 +94,7 @@ async def create_document( {"gerrydb_table_name": data.gerrydb_table}, ) document_id = results.one()[0] # should be only one row, one column of results + stmt = ( select( Document.document_id, @@ -119,7 +120,7 @@ async def create_document( # Document id has a unique constraint so I'm not sure we need to hit the DB again here # more valuable would be to check that the assignments table doc = session.exec( - stmt + stmt, ).one() # again if we've got more than one, we have problems. if not doc.map_uuid: session.rollback() @@ -134,6 +135,7 @@ async def create_document( detail="Document creation failed", ) session.commit() + return doc @@ -218,10 +220,12 @@ async def reset_map(document_id: str, session: Session = Depends(get_session)): # Recreate the partition session.execute( - text(f""" + text( + f""" CREATE TABLE {partition_name} PARTITION OF document.assignments FOR VALUES IN ('{document_id}'); - """) + """ + ) ) session.commit() diff --git a/backend/app/models.py b/backend/app/models.py index 16c2d07d7..0a84c0938 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -96,6 +96,17 @@ class DistrictrMapPublic(BaseModel): available_summary_stats: list[str] | None = None +class DistrictrMapUpdate(BaseModel): + gerrydb_table_name: str + name: str | None = None + parent_layer: str | None = None + child_layer: str | None = None + tiles_s3_path: str | None = None + num_districts: int | None = None + visible: bool | None = None + available_summary_stats: list[str] | None = None + + class GerryDBTable(TimeStampMixin, SQLModel, table=True): uuid: str = Field(sa_column=Column(UUIDType, unique=True, primary_key=True)) # Must correspond to the layer name in the tileset @@ -207,6 +218,7 @@ class PopulationStatsP1(BaseModel): nhpi_pop: int black_pop: int white_pop: int + two_or_more_races_pop: int class SummaryStatsP1(PopulationStatsP1): @@ -222,6 +234,7 @@ class PopulationStatsP4(BaseModel): non_hispanic_black_vap: int non_hispanic_white_vap: int non_hispanic_other_vap: int + non_hispanic_two_or_more_races_vap: int class SummaryStatsP4(PopulationStatsP4): diff --git a/backend/app/sql/available_summary_stat_udf.sql b/backend/app/sql/available_summary_stat_udf.sql index 7992c03e5..b5e4b2ec3 100644 --- a/backend/app/sql/available_summary_stat_udf.sql +++ b/backend/app/sql/available_summary_stat_udf.sql @@ -6,7 +6,7 @@ DECLARE p3 BOOLEAN; p4 BOOLEAN; BEGIN - SELECT count(column_name) = 6 INTO p1 + SELECT count(column_name) = 7 INTO p1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = gerrydb_table_name AND table_schema = 'gerrydb' @@ -15,10 +15,11 @@ BEGIN 'amin_pop', 'nhpi_pop', 'black_pop', - 'white_pop') + 'white_pop', + 'two_or_more_races_pop') ; - SELECT count(column_name) = 6 INTO p3 + SELECT count(column_name) = 7 INTO p3 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = gerrydb_table_name AND table_schema = 'gerrydb' @@ -27,10 +28,11 @@ BEGIN 'amin_vap', 'nhpi_vap', 'black_vap', - 'white_vap') + 'white_vap', + 'two_or_more_races_vap') ; - SELECT count(column_name) = 7 INTO p2 + SELECT count(column_name) = 8 INTO p2 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = gerrydb_table_name AND table_schema = 'gerrydb' @@ -40,11 +42,11 @@ BEGIN 'non_hispanic_nhpi_pop', 'non_hispanic_black_pop', 'non_hispanic_white_pop', - 'non_hispanic_other_pop' - ) + 'non_hispanic_other_pop', + 'non_hispanic_two_or_more_races_pop') ; - SELECT count(column_name) = 7 INTO p4 + SELECT count(column_name) = 8 INTO p4 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = gerrydb_table_name AND table_schema = 'gerrydb' @@ -54,8 +56,8 @@ BEGIN 'non_hispanic_nhpi_vap', 'non_hispanic_black_vap', 'non_hispanic_white_vap', - 'non_hispanic_other_vap' - ) + 'non_hispanic_other_vap', + 'non_hispanic_two_or_more_races_vap') ; RETURN QUERY diff --git a/backend/app/sql/summary_stats_p1.sql b/backend/app/sql/summary_stats_p1.sql index bc9fa994e..2bd3d0067 100644 --- a/backend/app/sql/summary_stats_p1.sql +++ b/backend/app/sql/summary_stats_p1.sql @@ -1,4 +1,6 @@ -CREATE OR REPLACE FUNCTION get_summary_stats_p1(document_id UUID) +DROP FUNCTION IF EXISTS get_summary_stats_p1(uuid); + +CREATE FUNCTION get_summary_stats_p1(document_id UUID) RETURNS TABLE ( zone TEXT, other_pop BIGINT, @@ -6,7 +8,8 @@ RETURNS TABLE ( amin_pop BIGINT, nhpi_pop BIGINT, black_pop BIGINT, - white_pop BIGINT + white_pop BIGINT, + two_or_more_races_pop BIGINT ) AS $$ DECLARE doc_districtrmap RECORD; @@ -30,7 +33,8 @@ BEGIN SUM(COALESCE(blocks.amin_pop, 0))::BIGINT AS amin_pop, SUM(COALESCE(blocks.nhpi_pop, 0))::BIGINT AS nhpi_pop, SUM(COALESCE(blocks.black_pop, 0))::BIGINT AS black_pop, - SUM(COALESCE(blocks.white_pop, 0))::BIGINT AS white_pop + SUM(COALESCE(blocks.white_pop, 0))::BIGINT AS white_pop, + SUM(COALESCE(blocks.two_or_more_races_pop, 0))::BIGINT AS two_or_more_races_pop FROM document.assignments LEFT JOIN gerrydb.%I blocks ON blocks.path = assignments.geo_id diff --git a/backend/app/sql/summary_stats_p1_totals.sql b/backend/app/sql/summary_stats_p1_totals.sql index 9b65c9ad9..591d37932 100644 --- a/backend/app/sql/summary_stats_p1_totals.sql +++ b/backend/app/sql/summary_stats_p1_totals.sql @@ -1,11 +1,14 @@ -CREATE OR REPLACE FUNCTION get_summary_p1_totals(gerrydb_table TEXT) +DROP FUNCTION IF EXISTS get_summary_p1_totals(TEXT); + +CREATE FUNCTION get_summary_p1_totals(gerrydb_table TEXT) RETURNS TABLE ( other_pop BIGINT, asian_pop BIGINT, amin_pop BIGINT, nhpi_pop BIGINT, black_pop BIGINT, - white_pop BIGINT + white_pop BIGINT, + two_or_more_races_pop BIGINT ) AS $$ DECLARE table_exists BOOLEAN; @@ -31,7 +34,8 @@ BEGIN SUM(COALESCE(amin_pop, 0))::BIGINT AS amin_pop, SUM(COALESCE(nhpi_pop, 0))::BIGINT AS nhpi_pop, SUM(COALESCE(black_pop, 0))::BIGINT AS black_pop, - SUM(COALESCE(white_pop, 0))::BIGINT AS white_pop + SUM(COALESCE(white_pop, 0))::BIGINT AS white_pop, + SUM(COALESCE(two_or_more_races_pop, 0))::BIGINT AS two_or_more_races_pop FROM gerrydb.%I ', $1); RETURN QUERY EXECUTE sql_query; diff --git a/backend/app/sql/summary_stats_p4.sql b/backend/app/sql/summary_stats_p4.sql index 1c75bfbcf..abfebc87e 100644 --- a/backend/app/sql/summary_stats_p4.sql +++ b/backend/app/sql/summary_stats_p4.sql @@ -1,4 +1,6 @@ -CREATE OR REPLACE FUNCTION get_summary_stats_p4(document_id UUID) +DROP FUNCTION IF EXISTS get_summary_stats_p4(uuid); + +CREATE FUNCTION get_summary_stats_p4(document_id UUID) RETURNS TABLE ( zone TEXT, hispanic_vap BIGINT, @@ -7,7 +9,8 @@ RETURNS TABLE ( non_hispanic_nhpi_vap BIGINT, non_hispanic_black_vap BIGINT, non_hispanic_white_vap BIGINT, - non_hispanic_other_vap BIGINT + non_hispanic_other_vap BIGINT, + non_hispanic_two_or_more_races_vap BIGINT ) AS $$ DECLARE doc_districtrmap RECORD; @@ -32,7 +35,8 @@ BEGIN SUM(COALESCE(blocks.non_hispanic_nhpi_vap, 0))::BIGINT AS non_hispanic_nhpi_vap, SUM(COALESCE(blocks.non_hispanic_black_vap, 0))::BIGINT AS non_hispanic_black_vap, SUM(COALESCE(blocks.non_hispanic_white_vap, 0))::BIGINT AS non_hispanic_white_vap, - SUM(COALESCE(blocks.non_hispanic_other_vap, 0))::BIGINT AS non_hispanic_other_vap + SUM(COALESCE(blocks.non_hispanic_other_vap, 0))::BIGINT AS non_hispanic_other_vap, + SUM(COALESCE(blocks.non_hispanic_two_or_more_races_vap, 0))::BIGINT AS non_hispanic_two_or_more_races_vap FROM document.assignments LEFT JOIN gerrydb.%I blocks ON blocks.path = assignments.geo_id diff --git a/backend/app/sql/summary_stats_p4_totals.sql b/backend/app/sql/summary_stats_p4_totals.sql index a9c57f12d..6ab9cfa56 100644 --- a/backend/app/sql/summary_stats_p4_totals.sql +++ b/backend/app/sql/summary_stats_p4_totals.sql @@ -1,4 +1,6 @@ -CREATE OR REPLACE FUNCTION get_summary_p4_totals(gerrydb_table TEXT) +DROP FUNCTION IF EXISTS get_summary_p4_totals(TEXT); + +CREATE FUNCTION get_summary_p4_totals(gerrydb_table TEXT) RETURNS TABLE ( hispanic_vap BIGINT, non_hispanic_asian_vap BIGINT, @@ -6,7 +8,8 @@ RETURNS TABLE ( non_hispanic_nhpi_vap BIGINT, non_hispanic_black_vap BIGINT, non_hispanic_white_vap BIGINT, - non_hispanic_other_vap BIGINT + non_hispanic_other_vap BIGINT, + non_hispanic_two_or_more_races_vap BIGINT ) AS $$ DECLARE table_exists BOOLEAN; @@ -33,7 +36,8 @@ BEGIN SUM(COALESCE(non_hispanic_nhpi_vap, 0))::BIGINT AS non_hispanic_nhpi_vap, SUM(COALESCE(non_hispanic_black_vap, 0))::BIGINT AS non_hispanic_black_vap, SUM(COALESCE(non_hispanic_white_vap, 0))::BIGINT AS non_hispanic_white_vap, - SUM(COALESCE(non_hispanic_other_vap, 0))::BIGINT AS non_hispanic_other_vap + SUM(COALESCE(non_hispanic_other_vap, 0))::BIGINT AS non_hispanic_other_vap, + SUM(COALESCE(non_hispanic_two_or_more_races_vap, 0))::BIGINT AS non_hispanic_two_or_more_races_vap FROM gerrydb.%I ', $1); RETURN QUERY EXECUTE sql_query; diff --git a/backend/app/utils.py b/backend/app/utils.py index 693a4a15f..f473c8153 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -1,11 +1,14 @@ -from sqlalchemy import text +from sqlalchemy import text, update from sqlalchemy import bindparam, Integer, String, Text from sqlalchemy.types import UUID from sqlmodel import Session, Float, Boolean import logging +from urllib.parse import ParseResult +import os +from app.core.config import settings -from app.models import SummaryStatisticType, UUIDType +from app.models import SummaryStatisticType, UUIDType, DistrictrMap, DistrictrMapUpdate logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -74,6 +77,41 @@ def create_districtr_map( return inserted_map_uuid # pyright: ignore +def update_districtrmap( + session: Session, + gerrydb_table_name: str, + **kwargs, +): + """ + Update a districtr map. + + Args: + session: The database session. + gerrydb_table_name: The name of the gerrydb table. + **kwargs: The fields to update. + + Returns: + The updated districtr map. + """ + data = DistrictrMapUpdate(gerrydb_table_name=gerrydb_table_name, **kwargs) + update_districtrmap = data.model_dump( + exclude_unset=True, exclude={"gerrydb_table_name"}, exclude_none=True + ) + + if not update_districtrmap.keys(): + raise KeyError("No fields to update") + + stmt = ( + update(DistrictrMap) + .where(DistrictrMap.gerrydb_table_name == data.gerrydb_table_name) # pyright: ignore + .values(update_districtrmap) + .returning(DistrictrMap) + ) + (updated_districtrmap,) = session.execute(stmt).one() + + return updated_districtrmap + + def create_shatterable_gerrydb_view( session: Session, parent_layer_name: str, @@ -273,3 +311,39 @@ def add_available_summary_stats_to_districtrmap( f"Updated available summary stats for districtr map {districtr_map_uuid} to {available_summary_stats}" ) return available_summary_stats + + +def download_file_from_s3(s3, url: ParseResult, replace=False) -> str: + """ + Download a file from S3 to the local volume path. + + Args: + s3: S3 client + url (ParseResult): URL of the file to download + replace (bool): If True, replace the file if it already exists + + Returns the path to the downloaded file. + """ + if not s3: + raise ValueError("S3 client is not available") + + file_name = url.path.lstrip("/") + logger.info("File name: %s", file_name) + object_information = s3.head_object(Bucket=url.netloc, Key=file_name) + + if object_information["ResponseMetadata"]["HTTPStatusCode"] != 200: + raise ValueError( + f"GeoPackage file {file_name} not found in S3 bucket {url.netloc}" + ) + + logger.info("Downloading GerryDB view. Got response:\n%s", object_information) + + path = os.path.join(settings.VOLUME_PATH, file_name) + + if os.path.exists(path) and not replace: + logger.info("File already exists. Skipping download.") + else: + logger.info("Downloading file...") + s3.download_file(url.netloc, file_name, path) + + return path diff --git a/backend/cli.py b/backend/cli.py index 5aab2a6fa..47f469eb8 100644 --- a/backend/cli.py +++ b/backend/cli.py @@ -2,10 +2,10 @@ import click import logging -from app.main import get_session +from app.core.db import engine from app.core.config import settings import subprocess -from urllib.parse import urlparse, ParseResult +from urllib.parse import urlparse from sqlalchemy import text from app.constants import GERRY_DB_SCHEMA from app.utils import ( @@ -14,51 +14,53 @@ create_parent_child_edges as _create_parent_child_edges, add_extent_to_districtrmap as _add_extent_to_districtrmap, add_available_summary_stats_to_districtrmap as _add_available_summary_stats_to_districtrmap, + update_districtrmap as _update_districtrmap, + download_file_from_s3, ) +from functools import wraps +from contextlib import contextmanager +from sqlmodel import Session +from typing import Callable, TypeVar, Any logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) -@click.group() -def cli(): - pass +T = TypeVar("T") -def download_file_from_s3(s3, url: ParseResult, replace=False) -> str: - """ - Download a file from S3 to the local volume path. +@contextmanager +def session_scope(): + """Provide a transactional scope around a series of operations.""" + session = Session(engine) + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() - Args: - s3: S3 client - url (ParseResult): URL of the file to download - replace (bool): If True, replace the file if it already exists - Returns the path to the downloaded file. +def with_session(f: Callable[..., T]) -> Callable[..., T]: + """ + Decorator that handles database session creation and cleanup. + Compatible with Click commands. """ - if not s3: - raise ValueError("S3 client is not available") - - file_name = url.path.lstrip("/") - logger.info("File name: %s", file_name) - object_information = s3.head_object(Bucket=url.netloc, Key=file_name) - - if object_information["ResponseMetadata"]["HTTPStatusCode"] != 200: - raise ValueError( - f"GeoPackage file {file_name} not found in S3 bucket {url.netloc}" - ) - logger.info("Downloading GerryDB view. Got response:\n%s", object_information) + @wraps(f) + def decorator(*args: Any, **kwargs: Any) -> T: + with session_scope() as session: + kwargs["session"] = session + return f(*args, **kwargs) - path = os.path.join(settings.VOLUME_PATH, file_name) + return decorator - if os.path.exists(path) and not replace: - logger.info("File already exists. Skipping download.") - else: - logger.info("Downloading file...") - s3.download_file(url.netloc, file_name, path) - return path +@click.group() +def cli(): + pass @cli.command("import-gerrydb-view") @@ -66,7 +68,10 @@ def download_file_from_s3(s3, url: ParseResult, replace=False) -> str: @click.option("--gpkg", "-g", help="Path or URL to GeoPackage file", required=True) @click.option("--replace", "-f", help="Replace the file if it exists", is_flag=True) @click.option("--rm", "-r", help="Delete file after loading to postgres", is_flag=True) -def import_gerrydb_view(layer: str, gpkg: str, replace: bool, rm: bool): +@with_session +def import_gerrydb_view( + session: Session, layer: str, gpkg: str, replace: bool, rm: bool +): logger.info("Importing GerryDB view...") url = urlparse(gpkg) @@ -110,9 +115,6 @@ def import_gerrydb_view(layer: str, gpkg: str, replace: bool, rm: bool): logger.info("GerryDB view imported successfully") - _session = get_session() - session = next(_session) - upsert_query = text( """ INSERT INTO gerrydbtable (uuid, name, updated_at) @@ -123,50 +125,39 @@ def import_gerrydb_view(layer: str, gpkg: str, replace: bool, rm: bool): """ ) - try: - session.execute( - upsert_query, - { - "name": layer, - }, - ) - session.commit() - logger.info("GerryDB view upserted successfully.") - except Exception as e: - session.rollback() - logger.error("Failed to upsert GerryDB view. Got %s", e) - raise ValueError(f"Failed to upsert GerryDB view. Got {e}") - - session.close() + session.execute( + upsert_query, + { + "name": layer, + }, + ) + logger.info("GerryDB view upserted successfully.") @cli.command("create-parent-child-edges") @click.option("--districtr-map", "-d", help="Districtr map name", required=True) -def create_parent_child_edges(districtr_map: str): +@with_session +def create_parent_child_edges(session: Session, districtr_map: str): logger.info("Creating parent-child edges...") - session = next(get_session()) stmt = text( "SELECT uuid FROM districtrmap WHERE gerrydb_table_name = :districtrmap_name" ) (districtr_map_uuid,) = session.execute( stmt, params={"districtrmap_name": districtr_map} ).one() - print(f"Found districtmap uuid: {districtr_map_uuid}") + logger.info(f"Found districtmap uuid: {districtr_map_uuid}") + _create_parent_child_edges(session=session, districtr_map_uuid=districtr_map_uuid) - session.commit() logger.info("Parent-child relationship upserted successfully.") - session.close() - @cli.command("delete-parent-child-edges") @click.option("--districtr-map", "-d", help="Districtr map name", required=True) -def delete_parent_child_edges(districtr_map: str): +@with_session +def delete_parent_child_edges(session: Session, districtr_map: str): logger.info("Deleting parent-child edges...") - session = next(get_session()) - delete_query = text( """ DELETE FROM parentchildedges @@ -179,11 +170,8 @@ def delete_parent_child_edges(districtr_map: str): "districtr_map": districtr_map, }, ) - session.commit() logger.info("Parent-child relationship upserted successfully.") - session.close() - @cli.command("create-districtr-map") @click.option("--name", help="Name of the districtr map", required=True) @@ -204,7 +192,9 @@ def delete_parent_child_edges(districtr_map: str): default=None, nargs=4, ) +@with_session def create_districtr_map( + session: Session, name: str, parent_layer_name: str, child_layer_name: str | None, @@ -215,7 +205,6 @@ def create_districtr_map( bounds: list[float] | None = None, ): logger.info("Creating districtr map...") - session = next(get_session()) (districtr_map_uuid,) = _create_districtr_map( session=session, name=name, @@ -238,28 +227,88 @@ def create_districtr_map( session=session, districtr_map_uuid=districtr_map_uuid ) - session.commit() logger.info(f"Districtr map created successfully {districtr_map_uuid}") +@cli.command("update-districtr-map") +@click.option( + "--gerrydb-table-name", + "-n", + help="Name of the GerryDB table", + type=str, + required=True, +) +@click.option("--name", help="Name of the districtr map", type=str, required=False) +@click.option( + "--parent-layer-name", help="Parent gerrydb layer name", type=str, required=False +) +@click.option( + "--child-layer-name", help="Child gerrydb layer name", type=str, required=False +) +@click.option("--num-districts", help="Number of districts", type=str, required=False) +@click.option( + "--tiles-s3-path", help="S3 path to the tileset", type=str, required=False +) +@click.option("--visibility", "-v", help="Visibility", type=bool, required=False) +@click.option( + "--bounds", + "-b", + help="Bounds of the extent as `--bounds x_min y_min x_max y_max`", + required=False, + type=float, + default=None, + nargs=4, +) +@with_session +def update_districtr_map( + session: Session, + gerrydb_table_name: str, + name: str | None, + parent_layer_name: str | None, + child_layer_name: str | None, + num_districts: int | None, + tiles_s3_path: str | None, + visibility: bool = False, + bounds: list[float] | None = None, +): + logger.info("Updating districtr map...") + + _bounds = None + if bounds and len(bounds) == 4: + _bounds = bounds + + result = _update_districtrmap( + session=session, + gerrydb_table_name=gerrydb_table_name, + name=name, + parent_layer=parent_layer_name, + child_layer=child_layer_name, + num_districts=num_districts, + tiles_s3_path=tiles_s3_path, + visible=visibility, + bounds=_bounds, + ) + logger.info(f"Districtr map updated successfully {result}") + + @cli.command("create-shatterable-districtr-view") @click.option("--parent-layer-name", help="Parent gerrydb layer name", required=True) @click.option("--child-layer-name", help="Child gerrydb layer name", required=False) @click.option("--gerrydb-table-name", help="Name of the GerryDB table", required=False) +@with_session def create_shatterable_gerrydb_view( + session: Session, parent_layer_name: str, child_layer_name: str, gerrydb_table_name: str, ): logger.info("Creating materialized shatterable gerrydb view...") - session = next(get_session()) inserted_uuid = _create_shatterable_gerrydb_view( session=session, parent_layer_name=parent_layer_name, child_layer_name=child_layer_name, gerrydb_table_name=gerrydb_table_name, ) - session.commit() logger.info( f"Materialized shatterable gerrydb view created successfully {inserted_uuid}" ) @@ -276,10 +325,12 @@ def create_shatterable_gerrydb_view( default=None, nargs=4, ) -def add_extent_to_districtr_map(districtr_map: str, bounds: list[float] | None = None): +@with_session +def add_extent_to_districtr_map( + session: Session, districtr_map: str, bounds: list[float] | None = None +): logger.info(f"User provided bounds: {bounds}") - session = next(get_session()) stmt = text( "SELECT uuid FROM districtrmap WHERE gerrydb_table_name = :districtrmap_name" ) @@ -291,16 +342,13 @@ def add_extent_to_districtr_map(districtr_map: str, bounds: list[float] | None = _add_extent_to_districtrmap( session=session, districtr_map_uuid=districtr_map_uuid, bounds=bounds ) - session.commit() logger.info("Updated extent successfully.") - session.close() - @cli.command("add-available-summary-stats-to-districtr-map") @click.option("--districtr-map", "-d", help="Districtr map name", required=True) -def add_available_summary_stats_to_districtr_map(districtr_map: str): - session = next(get_session()) +@with_session +def add_available_summary_stats_to_districtr_map(session: Session, districtr_map: str): stmt = text( "SELECT uuid FROM districtrmap WHERE gerrydb_table_name = :districtrmap_name" ) @@ -313,9 +361,7 @@ def add_available_summary_stats_to_districtr_map(districtr_map: str): session=session, districtr_map_uuid=districtr_map_uuid ) - session.commit() logger.info("Updated available summary stats successfully.") - session.close() if __name__ == "__main__": diff --git a/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats.geojson b/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats.geojson index 572cefc2e..e6ecf9d23 100644 --- a/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats.geojson +++ b/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats.geojson @@ -3,15 +3,15 @@ "name": "SELECT", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26914" } }, "features": [ -{ "type": "Feature", "properties": { "path": "202090416004010", "area_land": 33168, "area_water": 0, "other_pop": 12, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 55 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875211.081238693208434, 4337960.930915 ], [ 875265.224863927578554, 4337963.745180247351527 ], [ 875300.303792983642779, 4337971.768200713209808 ], [ 875327.274071992840618, 4337987.972723919898272 ], [ 875343.876800142694265, 4338007.019734212197363 ], [ 875356.197026390116662, 4338032.2054428094998 ], [ 875359.471527923597023, 4338063.413040107116103 ], [ 875355.397699509863742, 4338153.921939895488322 ], [ 875347.256564430193976, 4338195.378358344547451 ], [ 875334.623098340583965, 4338223.820951136760414 ], [ 875387.976528111146763, 4338241.289160016924143 ], [ 875410.516155685996637, 4338221.4454699838534 ], [ 875421.307235877029598, 4338045.005893977358937 ], [ 875432.630792022915557, 4338000.474406631663442 ], [ 875445.411641188431531, 4337979.829491405747831 ], [ 875454.801698453957215, 4337964.809443462640047 ], [ 875472.424312707735226, 4337940.835065085440874 ], [ 875484.035351007943973, 4337917.463120688684285 ], [ 875418.360436515184119, 4337916.432314324192703 ], [ 875375.462586269248277, 4337914.155511460267007 ], [ 875220.291984744369984, 4337906.170478757470846 ], [ 875212.765701709431596, 4337905.810247281566262 ], [ 875211.081238693208434, 4337960.930915 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090416003004", "area_land": 15823, "area_water": 0, "other_pop": 1, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 12 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 876029.603093245532364, 4338611.491121606901288 ], [ 876194.572833474143408, 4338619.185103545896709 ], [ 876196.43945312872529, 4338582.102246845141053 ], [ 876199.421181682031602, 4338523.593026914633811 ], [ 876114.734792487695813, 4338519.528731964528561 ], [ 876099.337269587209448, 4338518.789868013001978 ], [ 876033.778705667937174, 4338515.421732313930988 ], [ 876029.603093245532364, 4338611.491121606901288 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090443032011", "area_land": 19257, "area_water": 0, "other_pop": 5, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 31 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 869231.309174439054914, 4340396.545289561152458 ], [ 869319.693646978936158, 4340400.600859931670129 ], [ 869326.915629425784573, 4340398.159290821291506 ], [ 869328.812702536699362, 4340394.68766363710165 ], [ 869330.334983806591481, 4340389.974244618788362 ], [ 869330.934905984206125, 4340386.441455170512199 ], [ 869335.664594965986907, 4340326.571299341507256 ], [ 869338.182566312723793, 4340216.18538093008101 ], [ 869339.686451153829694, 4340210.024406461976469 ], [ 869335.697759646456689, 4340206.386548922397196 ], [ 869242.119611647445709, 4340200.305704364553094 ], [ 869237.76920640678145, 4340277.887773043476045 ], [ 869231.309174439054914, 4340396.545289561152458 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090434001003", "area_land": 24816, "area_water": 0, "other_pop": 24, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 130 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875930.37992833124008, 4332349.252949083223939 ], [ 875933.443328902358189, 4332350.512517770752311 ], [ 875936.723918574396521, 4332352.672826421447098 ], [ 875940.119178901193663, 4332356.062846168875694 ], [ 875943.424073546309955, 4332361.340518746525049 ], [ 875944.319405541173182, 4332364.388283201493323 ], [ 875945.113742337096483, 4332369.54577558953315 ], [ 875944.670285670785233, 4332382.434507312253118 ], [ 875945.567135536577553, 4332387.263029311783612 ], [ 875949.17501455033198, 4332398.89889903459698 ], [ 875951.559500945499167, 4332403.464760144241154 ], [ 875956.987613479956053, 4332411.515112683176994 ], [ 875970.784156027017161, 4332424.195230172947049 ], [ 876010.736134674632922, 4332445.139112876728177 ], [ 876044.262139945523813, 4332455.313976712524891 ], [ 876060.651922063203529, 4332457.323036558926105 ], [ 876103.475923568708822, 4332458.149635425768793 ], [ 876110.933183946879581, 4332291.567219044081867 ], [ 876066.784754700609483, 4332280.327024504542351 ], [ 876050.379456249298528, 4332273.19777974113822 ], [ 876027.842756676953286, 4332258.207085125148296 ], [ 875961.742888991255313, 4332328.495757032185793 ], [ 875952.4995389302494, 4332338.514782093465328 ], [ 875941.136727001168765, 4332343.869353833608329 ], [ 875938.680846909992397, 4332344.419547958299518 ], [ 875935.657703495351598, 4332345.944222977384925 ], [ 875924.61156704777386, 4332348.309071445837617 ], [ 875930.37992833124008, 4332349.252949083223939 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202099800001035", "area_land": 151703, "area_water": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877813.040280948858708, 4342314.444725112058222 ], [ 877809.014222223311663, 4342502.010962888598442 ], [ 877808.392830823198892, 4342556.183347844518721 ], [ 878187.281443994143046, 4342575.815153966657817 ], [ 878188.527672112570144, 4342539.257936611771584 ], [ 878203.946146892383695, 4342177.056350266560912 ], [ 877870.8709020371316, 4342163.087458959780633 ], [ 877820.343733718618751, 4342161.316630367189646 ], [ 877813.040280948858708, 4342314.444725112058222 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090429003012", "area_land": 27367, "area_water": 0, "other_pop": 32, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 139 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877171.124916166299954, 4338452.87533472944051 ], [ 877432.574727031751536, 4338466.573728412389755 ], [ 877438.946534773334861, 4338361.259861093014479 ], [ 877177.250717416638508, 4338348.996695580892265 ], [ 877171.124916166299954, 4338452.87533472944051 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "201730056003001", "area_land": 40244, "area_water": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 13 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 647618.257487860973924, 4162037.393264054320753 ], [ 647677.756609870004468, 4162038.897054230328649 ], [ 647719.070099569740705, 4162039.966174147557467 ], [ 647719.63226254703477, 4162018.332780665252358 ], [ 647786.68576369935181, 4161814.082042149733752 ], [ 647814.53967670770362, 4161742.877915579359978 ], [ 647835.253349754726514, 4161715.499313783831894 ], [ 647931.670532181044109, 4161647.849339275155216 ], [ 647941.242692932020873, 4161641.02766244718805 ], [ 647884.676134104607627, 4161638.35352012841031 ], [ 647883.100702673778869, 4161637.54847350390628 ], [ 647881.001446478301659, 4161636.401107432320714 ], [ 647879.341679102857597, 4161635.372573387343436 ], [ 647876.241502788383514, 4161635.872232513967901 ], [ 647868.445927470806055, 4161637.176076213829219 ], [ 647866.150291252415627, 4161637.135130017530173 ], [ 647770.255428230622783, 4161635.869233383797109 ], [ 647766.972603302798234, 4161651.571556095965207 ], [ 647760.328006867086515, 4161672.541566835716367 ], [ 647720.724104659864679, 4161768.731609313283116 ], [ 647664.439331455505453, 4161899.032108286395669 ], [ 647627.416294273571111, 4161989.274946445599198 ], [ 647621.465386576252058, 4162006.039716630242765 ], [ 647618.257487860973924, 4162037.393264054320753 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "200610008021023", "area_land": 5630, "area_water": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 683379.173801723285578, 4339718.274099923670292 ], [ 683388.636045723804273, 4339720.16153553687036 ], [ 683406.247825293801725, 4339226.619054754264653 ], [ 683392.94784223777242, 4339226.196451342664659 ], [ 683381.891119970707223, 4339620.723863031715155 ], [ 683379.173801723285578, 4339718.274099923670292 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "201474751002233", "area_land": 19988, "area_water": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 489352.375147499202285, 4391401.554798025637865 ], [ 489353.078518448804971, 4391476.024222874082625 ], [ 489437.041688203113154, 4391475.020668931305408 ], [ 489452.736617584130727, 4391474.888162637129426 ], [ 489455.068263905821368, 4391423.943221963010728 ], [ 489469.648946393863298, 4391424.256213800981641 ], [ 489503.608059620310087, 4391421.102274054661393 ], [ 489502.105179723410401, 4391325.324975534342229 ], [ 489455.362240921007469, 4391325.499848640523851 ], [ 489448.500717744231224, 4391325.398266786709428 ], [ 489438.294428156630602, 4391325.41226374451071 ], [ 489387.434208348044194, 4391325.260247093625367 ], [ 489352.443056440912187, 4391326.640378216281533 ], [ 489352.375147499202285, 4391401.554798025637865 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "200834611002251", "area_land": 2577172, "area_water": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 409731.251194308104459, 4200666.441016187891364 ], [ 409735.139420253282879, 4200955.250511697493494 ], [ 409736.066063234466128, 4201047.233642023056746 ], [ 409740.753175496880431, 4201504.041755408979952 ], [ 409810.145600938529242, 4201502.277895309962332 ], [ 409972.496923764934763, 4201500.4900694899261 ], [ 410110.95236269995803, 4201498.967932443134487 ], [ 410405.217876273440197, 4201492.078675809316337 ], [ 411342.044738269527443, 4201478.989577942527831 ], [ 411328.750386512838304, 4200568.193698559887707 ], [ 411324.006945162313059, 4200243.884643631987274 ], [ 411318.396692238282412, 4199863.879075475037098 ], [ 410818.479345699190162, 4199874.968814136460423 ], [ 410594.067309823876712, 4199878.082232806831598 ], [ 410399.997044122719672, 4199883.310992266982794 ], [ 410315.125831638462842, 4199885.238996434025466 ], [ 410086.93547103140736, 4199888.407714327797294 ], [ 409722.124784207146149, 4199894.641732443124056 ], [ 409731.251194308104459, 4200666.441016187891364 ] ] ] } } +{ "type": "Feature", "properties": { "path": "202090416004010", "area_land": 33168, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 12, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 55 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875211.081238693208434, 4337960.930915 ], [ 875265.224863927578554, 4337963.745180247351527 ], [ 875300.303792983642779, 4337971.768200713209808 ], [ 875327.274071992840618, 4337987.972723919898272 ], [ 875343.876800142694265, 4338007.019734212197363 ], [ 875356.197026390116662, 4338032.2054428094998 ], [ 875359.471527923597023, 4338063.413040107116103 ], [ 875355.397699509863742, 4338153.921939895488322 ], [ 875347.256564430193976, 4338195.378358344547451 ], [ 875334.623098340583965, 4338223.820951136760414 ], [ 875387.976528111146763, 4338241.289160016924143 ], [ 875410.516155685996637, 4338221.4454699838534 ], [ 875421.307235877029598, 4338045.005893977358937 ], [ 875432.630792022915557, 4338000.474406631663442 ], [ 875445.411641188431531, 4337979.829491405747831 ], [ 875454.801698453957215, 4337964.809443462640047 ], [ 875472.424312707735226, 4337940.835065085440874 ], [ 875484.035351007943973, 4337917.463120688684285 ], [ 875418.360436515184119, 4337916.432314324192703 ], [ 875375.462586269248277, 4337914.155511460267007 ], [ 875220.291984744369984, 4337906.170478757470846 ], [ 875212.765701709431596, 4337905.810247281566262 ], [ 875211.081238693208434, 4337960.930915 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090416003004", "area_land": 15823, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 1, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 12 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 876029.603093245532364, 4338611.491121606901288 ], [ 876194.572833474143408, 4338619.185103545896709 ], [ 876196.43945312872529, 4338582.102246845141053 ], [ 876199.421181682031602, 4338523.593026914633811 ], [ 876114.734792487695813, 4338519.528731964528561 ], [ 876099.337269587209448, 4338518.789868013001978 ], [ 876033.778705667937174, 4338515.421732313930988 ], [ 876029.603093245532364, 4338611.491121606901288 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090443032011", "area_land": 19257, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 5, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 31 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 869231.309174439054914, 4340396.545289561152458 ], [ 869319.693646978936158, 4340400.600859931670129 ], [ 869326.915629425784573, 4340398.159290821291506 ], [ 869328.812702536699362, 4340394.68766363710165 ], [ 869330.334983806591481, 4340389.974244618788362 ], [ 869330.934905984206125, 4340386.441455170512199 ], [ 869335.664594965986907, 4340326.571299341507256 ], [ 869338.182566312723793, 4340216.18538093008101 ], [ 869339.686451153829694, 4340210.024406461976469 ], [ 869335.697759646456689, 4340206.386548922397196 ], [ 869242.119611647445709, 4340200.305704364553094 ], [ 869237.76920640678145, 4340277.887773043476045 ], [ 869231.309174439054914, 4340396.545289561152458 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090434001003", "area_land": 24816, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 24, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 130 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875930.37992833124008, 4332349.252949083223939 ], [ 875933.443328902358189, 4332350.512517770752311 ], [ 875936.723918574396521, 4332352.672826421447098 ], [ 875940.119178901193663, 4332356.062846168875694 ], [ 875943.424073546309955, 4332361.340518746525049 ], [ 875944.319405541173182, 4332364.388283201493323 ], [ 875945.113742337096483, 4332369.54577558953315 ], [ 875944.670285670785233, 4332382.434507312253118 ], [ 875945.567135536577553, 4332387.263029311783612 ], [ 875949.17501455033198, 4332398.89889903459698 ], [ 875951.559500945499167, 4332403.464760144241154 ], [ 875956.987613479956053, 4332411.515112683176994 ], [ 875970.784156027017161, 4332424.195230172947049 ], [ 876010.736134674632922, 4332445.139112876728177 ], [ 876044.262139945523813, 4332455.313976712524891 ], [ 876060.651922063203529, 4332457.323036558926105 ], [ 876103.475923568708822, 4332458.149635425768793 ], [ 876110.933183946879581, 4332291.567219044081867 ], [ 876066.784754700609483, 4332280.327024504542351 ], [ 876050.379456249298528, 4332273.19777974113822 ], [ 876027.842756676953286, 4332258.207085125148296 ], [ 875961.742888991255313, 4332328.495757032185793 ], [ 875952.4995389302494, 4332338.514782093465328 ], [ 875941.136727001168765, 4332343.869353833608329 ], [ 875938.680846909992397, 4332344.419547958299518 ], [ 875935.657703495351598, 4332345.944222977384925 ], [ 875924.61156704777386, 4332348.309071445837617 ], [ 875930.37992833124008, 4332349.252949083223939 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202099800001035", "area_land": 151703, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877813.040280948858708, 4342314.444725112058222 ], [ 877809.014222223311663, 4342502.010962888598442 ], [ 877808.392830823198892, 4342556.183347844518721 ], [ 878187.281443994143046, 4342575.815153966657817 ], [ 878188.527672112570144, 4342539.257936611771584 ], [ 878203.946146892383695, 4342177.056350266560912 ], [ 877870.8709020371316, 4342163.087458959780633 ], [ 877820.343733718618751, 4342161.316630367189646 ], [ 877813.040280948858708, 4342314.444725112058222 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090429003012", "area_land": 27367, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 32, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 139 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877171.124916166299954, 4338452.87533472944051 ], [ 877432.574727031751536, 4338466.573728412389755 ], [ 877438.946534773334861, 4338361.259861093014479 ], [ 877177.250717416638508, 4338348.996695580892265 ], [ 877171.124916166299954, 4338452.87533472944051 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "201730056003001", "area_land": 40244, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 13 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 647618.257487860973924, 4162037.393264054320753 ], [ 647677.756609870004468, 4162038.897054230328649 ], [ 647719.070099569740705, 4162039.966174147557467 ], [ 647719.63226254703477, 4162018.332780665252358 ], [ 647786.68576369935181, 4161814.082042149733752 ], [ 647814.53967670770362, 4161742.877915579359978 ], [ 647835.253349754726514, 4161715.499313783831894 ], [ 647931.670532181044109, 4161647.849339275155216 ], [ 647941.242692932020873, 4161641.02766244718805 ], [ 647884.676134104607627, 4161638.35352012841031 ], [ 647883.100702673778869, 4161637.54847350390628 ], [ 647881.001446478301659, 4161636.401107432320714 ], [ 647879.341679102857597, 4161635.372573387343436 ], [ 647876.241502788383514, 4161635.872232513967901 ], [ 647868.445927470806055, 4161637.176076213829219 ], [ 647866.150291252415627, 4161637.135130017530173 ], [ 647770.255428230622783, 4161635.869233383797109 ], [ 647766.972603302798234, 4161651.571556095965207 ], [ 647760.328006867086515, 4161672.541566835716367 ], [ 647720.724104659864679, 4161768.731609313283116 ], [ 647664.439331455505453, 4161899.032108286395669 ], [ 647627.416294273571111, 4161989.274946445599198 ], [ 647621.465386576252058, 4162006.039716630242765 ], [ 647618.257487860973924, 4162037.393264054320753 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "200610008021023", "area_land": 5630, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 683379.173801723285578, 4339718.274099923670292 ], [ 683388.636045723804273, 4339720.16153553687036 ], [ 683406.247825293801725, 4339226.619054754264653 ], [ 683392.94784223777242, 4339226.196451342664659 ], [ 683381.891119970707223, 4339620.723863031715155 ], [ 683379.173801723285578, 4339718.274099923670292 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "201474751002233", "area_land": 19988, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 489352.375147499202285, 4391401.554798025637865 ], [ 489353.078518448804971, 4391476.024222874082625 ], [ 489437.041688203113154, 4391475.020668931305408 ], [ 489452.736617584130727, 4391474.888162637129426 ], [ 489455.068263905821368, 4391423.943221963010728 ], [ 489469.648946393863298, 4391424.256213800981641 ], [ 489503.608059620310087, 4391421.102274054661393 ], [ 489502.105179723410401, 4391325.324975534342229 ], [ 489455.362240921007469, 4391325.499848640523851 ], [ 489448.500717744231224, 4391325.398266786709428 ], [ 489438.294428156630602, 4391325.41226374451071 ], [ 489387.434208348044194, 4391325.260247093625367 ], [ 489352.443056440912187, 4391326.640378216281533 ], [ 489352.375147499202285, 4391401.554798025637865 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "200834611002251", "area_land": 2577172, "area_water": 0, "two_or_more_races_pop": 0, "other_pop": 0, "amin_pop": 0, "asian_pop":0, "black_pop":0,"nhpi_pop":0,"white_pop":0, "total_pop": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 409731.251194308104459, 4200666.441016187891364 ], [ 409735.139420253282879, 4200955.250511697493494 ], [ 409736.066063234466128, 4201047.233642023056746 ], [ 409740.753175496880431, 4201504.041755408979952 ], [ 409810.145600938529242, 4201502.277895309962332 ], [ 409972.496923764934763, 4201500.4900694899261 ], [ 410110.95236269995803, 4201498.967932443134487 ], [ 410405.217876273440197, 4201492.078675809316337 ], [ 411342.044738269527443, 4201478.989577942527831 ], [ 411328.750386512838304, 4200568.193698559887707 ], [ 411324.006945162313059, 4200243.884643631987274 ], [ 411318.396692238282412, 4199863.879075475037098 ], [ 410818.479345699190162, 4199874.968814136460423 ], [ 410594.067309823876712, 4199878.082232806831598 ], [ 410399.997044122719672, 4199883.310992266982794 ], [ 410315.125831638462842, 4199885.238996434025466 ], [ 410086.93547103140736, 4199888.407714327797294 ], [ 409722.124784207146149, 4199894.641732443124056 ], [ 409731.251194308104459, 4200666.441016187891364 ] ] ] } } ] } diff --git a/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats_p4.geojson b/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats_p4.geojson index d84d67435..f35f9fc30 100644 --- a/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats_p4.geojson +++ b/backend/tests/fixtures/ks_demo_view_census_blocks_summary_stats_p4.geojson @@ -3,15 +3,15 @@ "name": "SELECT", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26914" } }, "features": [ -{ "type": "Feature", "properties": { "path": "202090416004010", "area_land": 33168, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 12, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 55 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875211.081238693208434, 4337960.930915 ], [ 875265.224863927578554, 4337963.745180247351527 ], [ 875300.303792983642779, 4337971.768200713209808 ], [ 875327.274071992840618, 4337987.972723919898272 ], [ 875343.876800142694265, 4338007.019734212197363 ], [ 875356.197026390116662, 4338032.2054428094998 ], [ 875359.471527923597023, 4338063.413040107116103 ], [ 875355.397699509863742, 4338153.921939895488322 ], [ 875347.256564430193976, 4338195.378358344547451 ], [ 875334.623098340583965, 4338223.820951136760414 ], [ 875387.976528111146763, 4338241.289160016924143 ], [ 875410.516155685996637, 4338221.4454699838534 ], [ 875421.307235877029598, 4338045.005893977358937 ], [ 875432.630792022915557, 4338000.474406631663442 ], [ 875445.411641188431531, 4337979.829491405747831 ], [ 875454.801698453957215, 4337964.809443462640047 ], [ 875472.424312707735226, 4337940.835065085440874 ], [ 875484.035351007943973, 4337917.463120688684285 ], [ 875418.360436515184119, 4337916.432314324192703 ], [ 875375.462586269248277, 4337914.155511460267007 ], [ 875220.291984744369984, 4337906.170478757470846 ], [ 875212.765701709431596, 4337905.810247281566262 ], [ 875211.081238693208434, 4337960.930915 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090416003004", "area_land": 15823, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 1, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 12 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 876029.603093245532364, 4338611.491121606901288 ], [ 876194.572833474143408, 4338619.185103545896709 ], [ 876196.43945312872529, 4338582.102246845141053 ], [ 876199.421181682031602, 4338523.593026914633811 ], [ 876114.734792487695813, 4338519.528731964528561 ], [ 876099.337269587209448, 4338518.789868013001978 ], [ 876033.778705667937174, 4338515.421732313930988 ], [ 876029.603093245532364, 4338611.491121606901288 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090443032011", "area_land": 19257, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 5, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 31 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 869231.309174439054914, 4340396.545289561152458 ], [ 869319.693646978936158, 4340400.600859931670129 ], [ 869326.915629425784573, 4340398.159290821291506 ], [ 869328.812702536699362, 4340394.68766363710165 ], [ 869330.334983806591481, 4340389.974244618788362 ], [ 869330.934905984206125, 4340386.441455170512199 ], [ 869335.664594965986907, 4340326.571299341507256 ], [ 869338.182566312723793, 4340216.18538093008101 ], [ 869339.686451153829694, 4340210.024406461976469 ], [ 869335.697759646456689, 4340206.386548922397196 ], [ 869242.119611647445709, 4340200.305704364553094 ], [ 869237.76920640678145, 4340277.887773043476045 ], [ 869231.309174439054914, 4340396.545289561152458 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090434001003", "area_land": 24816, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 24, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 130 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875930.37992833124008, 4332349.252949083223939 ], [ 875933.443328902358189, 4332350.512517770752311 ], [ 875936.723918574396521, 4332352.672826421447098 ], [ 875940.119178901193663, 4332356.062846168875694 ], [ 875943.424073546309955, 4332361.340518746525049 ], [ 875944.319405541173182, 4332364.388283201493323 ], [ 875945.113742337096483, 4332369.54577558953315 ], [ 875944.670285670785233, 4332382.434507312253118 ], [ 875945.567135536577553, 4332387.263029311783612 ], [ 875949.17501455033198, 4332398.89889903459698 ], [ 875951.559500945499167, 4332403.464760144241154 ], [ 875956.987613479956053, 4332411.515112683176994 ], [ 875970.784156027017161, 4332424.195230172947049 ], [ 876010.736134674632922, 4332445.139112876728177 ], [ 876044.262139945523813, 4332455.313976712524891 ], [ 876060.651922063203529, 4332457.323036558926105 ], [ 876103.475923568708822, 4332458.149635425768793 ], [ 876110.933183946879581, 4332291.567219044081867 ], [ 876066.784754700609483, 4332280.327024504542351 ], [ 876050.379456249298528, 4332273.19777974113822 ], [ 876027.842756676953286, 4332258.207085125148296 ], [ 875961.742888991255313, 4332328.495757032185793 ], [ 875952.4995389302494, 4332338.514782093465328 ], [ 875941.136727001168765, 4332343.869353833608329 ], [ 875938.680846909992397, 4332344.419547958299518 ], [ 875935.657703495351598, 4332345.944222977384925 ], [ 875924.61156704777386, 4332348.309071445837617 ], [ 875930.37992833124008, 4332349.252949083223939 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202099800001035", "area_land": 151703, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877813.040280948858708, 4342314.444725112058222 ], [ 877809.014222223311663, 4342502.010962888598442 ], [ 877808.392830823198892, 4342556.183347844518721 ], [ 878187.281443994143046, 4342575.815153966657817 ], [ 878188.527672112570144, 4342539.257936611771584 ], [ 878203.946146892383695, 4342177.056350266560912 ], [ 877870.8709020371316, 4342163.087458959780633 ], [ 877820.343733718618751, 4342161.316630367189646 ], [ 877813.040280948858708, 4342314.444725112058222 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "202090429003012", "area_land": 27367, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 32, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 139 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877171.124916166299954, 4338452.87533472944051 ], [ 877432.574727031751536, 4338466.573728412389755 ], [ 877438.946534773334861, 4338361.259861093014479 ], [ 877177.250717416638508, 4338348.996695580892265 ], [ 877171.124916166299954, 4338452.87533472944051 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "201730056003001", "area_land": 40244, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 13 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 647618.257487860973924, 4162037.393264054320753 ], [ 647677.756609870004468, 4162038.897054230328649 ], [ 647719.070099569740705, 4162039.966174147557467 ], [ 647719.63226254703477, 4162018.332780665252358 ], [ 647786.68576369935181, 4161814.082042149733752 ], [ 647814.53967670770362, 4161742.877915579359978 ], [ 647835.253349754726514, 4161715.499313783831894 ], [ 647931.670532181044109, 4161647.849339275155216 ], [ 647941.242692932020873, 4161641.02766244718805 ], [ 647884.676134104607627, 4161638.35352012841031 ], [ 647883.100702673778869, 4161637.54847350390628 ], [ 647881.001446478301659, 4161636.401107432320714 ], [ 647879.341679102857597, 4161635.372573387343436 ], [ 647876.241502788383514, 4161635.872232513967901 ], [ 647868.445927470806055, 4161637.176076213829219 ], [ 647866.150291252415627, 4161637.135130017530173 ], [ 647770.255428230622783, 4161635.869233383797109 ], [ 647766.972603302798234, 4161651.571556095965207 ], [ 647760.328006867086515, 4161672.541566835716367 ], [ 647720.724104659864679, 4161768.731609313283116 ], [ 647664.439331455505453, 4161899.032108286395669 ], [ 647627.416294273571111, 4161989.274946445599198 ], [ 647621.465386576252058, 4162006.039716630242765 ], [ 647618.257487860973924, 4162037.393264054320753 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "200610008021023", "area_land": 5630, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 683379.173801723285578, 4339718.274099923670292 ], [ 683388.636045723804273, 4339720.16153553687036 ], [ 683406.247825293801725, 4339226.619054754264653 ], [ 683392.94784223777242, 4339226.196451342664659 ], [ 683381.891119970707223, 4339620.723863031715155 ], [ 683379.173801723285578, 4339718.274099923670292 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "201474751002233", "area_land": 19988, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 489352.375147499202285, 4391401.554798025637865 ], [ 489353.078518448804971, 4391476.024222874082625 ], [ 489437.041688203113154, 4391475.020668931305408 ], [ 489452.736617584130727, 4391474.888162637129426 ], [ 489455.068263905821368, 4391423.943221963010728 ], [ 489469.648946393863298, 4391424.256213800981641 ], [ 489503.608059620310087, 4391421.102274054661393 ], [ 489502.105179723410401, 4391325.324975534342229 ], [ 489455.362240921007469, 4391325.499848640523851 ], [ 489448.500717744231224, 4391325.398266786709428 ], [ 489438.294428156630602, 4391325.41226374451071 ], [ 489387.434208348044194, 4391325.260247093625367 ], [ 489352.443056440912187, 4391326.640378216281533 ], [ 489352.375147499202285, 4391401.554798025637865 ] ] ] } }, -{ "type": "Feature", "properties": { "path": "200834611002251", "area_land": 2577172, "area_water": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 409731.251194308104459, 4200666.441016187891364 ], [ 409735.139420253282879, 4200955.250511697493494 ], [ 409736.066063234466128, 4201047.233642023056746 ], [ 409740.753175496880431, 4201504.041755408979952 ], [ 409810.145600938529242, 4201502.277895309962332 ], [ 409972.496923764934763, 4201500.4900694899261 ], [ 410110.95236269995803, 4201498.967932443134487 ], [ 410405.217876273440197, 4201492.078675809316337 ], [ 411342.044738269527443, 4201478.989577942527831 ], [ 411328.750386512838304, 4200568.193698559887707 ], [ 411324.006945162313059, 4200243.884643631987274 ], [ 411318.396692238282412, 4199863.879075475037098 ], [ 410818.479345699190162, 4199874.968814136460423 ], [ 410594.067309823876712, 4199878.082232806831598 ], [ 410399.997044122719672, 4199883.310992266982794 ], [ 410315.125831638462842, 4199885.238996434025466 ], [ 410086.93547103140736, 4199888.407714327797294 ], [ 409722.124784207146149, 4199894.641732443124056 ], [ 409731.251194308104459, 4200666.441016187891364 ] ] ] } } +{ "type": "Feature", "properties": { "path": "202090416004010", "area_land": 33168, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 12, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 55 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875211.081238693208434, 4337960.930915 ], [ 875265.224863927578554, 4337963.745180247351527 ], [ 875300.303792983642779, 4337971.768200713209808 ], [ 875327.274071992840618, 4337987.972723919898272 ], [ 875343.876800142694265, 4338007.019734212197363 ], [ 875356.197026390116662, 4338032.2054428094998 ], [ 875359.471527923597023, 4338063.413040107116103 ], [ 875355.397699509863742, 4338153.921939895488322 ], [ 875347.256564430193976, 4338195.378358344547451 ], [ 875334.623098340583965, 4338223.820951136760414 ], [ 875387.976528111146763, 4338241.289160016924143 ], [ 875410.516155685996637, 4338221.4454699838534 ], [ 875421.307235877029598, 4338045.005893977358937 ], [ 875432.630792022915557, 4338000.474406631663442 ], [ 875445.411641188431531, 4337979.829491405747831 ], [ 875454.801698453957215, 4337964.809443462640047 ], [ 875472.424312707735226, 4337940.835065085440874 ], [ 875484.035351007943973, 4337917.463120688684285 ], [ 875418.360436515184119, 4337916.432314324192703 ], [ 875375.462586269248277, 4337914.155511460267007 ], [ 875220.291984744369984, 4337906.170478757470846 ], [ 875212.765701709431596, 4337905.810247281566262 ], [ 875211.081238693208434, 4337960.930915 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090416003004", "area_land": 15823, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 1, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 12 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 876029.603093245532364, 4338611.491121606901288 ], [ 876194.572833474143408, 4338619.185103545896709 ], [ 876196.43945312872529, 4338582.102246845141053 ], [ 876199.421181682031602, 4338523.593026914633811 ], [ 876114.734792487695813, 4338519.528731964528561 ], [ 876099.337269587209448, 4338518.789868013001978 ], [ 876033.778705667937174, 4338515.421732313930988 ], [ 876029.603093245532364, 4338611.491121606901288 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090443032011", "area_land": 19257, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 5, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 31 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 869231.309174439054914, 4340396.545289561152458 ], [ 869319.693646978936158, 4340400.600859931670129 ], [ 869326.915629425784573, 4340398.159290821291506 ], [ 869328.812702536699362, 4340394.68766363710165 ], [ 869330.334983806591481, 4340389.974244618788362 ], [ 869330.934905984206125, 4340386.441455170512199 ], [ 869335.664594965986907, 4340326.571299341507256 ], [ 869338.182566312723793, 4340216.18538093008101 ], [ 869339.686451153829694, 4340210.024406461976469 ], [ 869335.697759646456689, 4340206.386548922397196 ], [ 869242.119611647445709, 4340200.305704364553094 ], [ 869237.76920640678145, 4340277.887773043476045 ], [ 869231.309174439054914, 4340396.545289561152458 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090434001003", "area_land": 24816, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 24, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 130 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 875930.37992833124008, 4332349.252949083223939 ], [ 875933.443328902358189, 4332350.512517770752311 ], [ 875936.723918574396521, 4332352.672826421447098 ], [ 875940.119178901193663, 4332356.062846168875694 ], [ 875943.424073546309955, 4332361.340518746525049 ], [ 875944.319405541173182, 4332364.388283201493323 ], [ 875945.113742337096483, 4332369.54577558953315 ], [ 875944.670285670785233, 4332382.434507312253118 ], [ 875945.567135536577553, 4332387.263029311783612 ], [ 875949.17501455033198, 4332398.89889903459698 ], [ 875951.559500945499167, 4332403.464760144241154 ], [ 875956.987613479956053, 4332411.515112683176994 ], [ 875970.784156027017161, 4332424.195230172947049 ], [ 876010.736134674632922, 4332445.139112876728177 ], [ 876044.262139945523813, 4332455.313976712524891 ], [ 876060.651922063203529, 4332457.323036558926105 ], [ 876103.475923568708822, 4332458.149635425768793 ], [ 876110.933183946879581, 4332291.567219044081867 ], [ 876066.784754700609483, 4332280.327024504542351 ], [ 876050.379456249298528, 4332273.19777974113822 ], [ 876027.842756676953286, 4332258.207085125148296 ], [ 875961.742888991255313, 4332328.495757032185793 ], [ 875952.4995389302494, 4332338.514782093465328 ], [ 875941.136727001168765, 4332343.869353833608329 ], [ 875938.680846909992397, 4332344.419547958299518 ], [ 875935.657703495351598, 4332345.944222977384925 ], [ 875924.61156704777386, 4332348.309071445837617 ], [ 875930.37992833124008, 4332349.252949083223939 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202099800001035", "area_land": 151703, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877813.040280948858708, 4342314.444725112058222 ], [ 877809.014222223311663, 4342502.010962888598442 ], [ 877808.392830823198892, 4342556.183347844518721 ], [ 878187.281443994143046, 4342575.815153966657817 ], [ 878188.527672112570144, 4342539.257936611771584 ], [ 878203.946146892383695, 4342177.056350266560912 ], [ 877870.8709020371316, 4342163.087458959780633 ], [ 877820.343733718618751, 4342161.316630367189646 ], [ 877813.040280948858708, 4342314.444725112058222 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "202090429003012", "area_land": 27367, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 32, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 139 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 877171.124916166299954, 4338452.87533472944051 ], [ 877432.574727031751536, 4338466.573728412389755 ], [ 877438.946534773334861, 4338361.259861093014479 ], [ 877177.250717416638508, 4338348.996695580892265 ], [ 877171.124916166299954, 4338452.87533472944051 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "201730056003001", "area_land": 40244, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 13 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 647618.257487860973924, 4162037.393264054320753 ], [ 647677.756609870004468, 4162038.897054230328649 ], [ 647719.070099569740705, 4162039.966174147557467 ], [ 647719.63226254703477, 4162018.332780665252358 ], [ 647786.68576369935181, 4161814.082042149733752 ], [ 647814.53967670770362, 4161742.877915579359978 ], [ 647835.253349754726514, 4161715.499313783831894 ], [ 647931.670532181044109, 4161647.849339275155216 ], [ 647941.242692932020873, 4161641.02766244718805 ], [ 647884.676134104607627, 4161638.35352012841031 ], [ 647883.100702673778869, 4161637.54847350390628 ], [ 647881.001446478301659, 4161636.401107432320714 ], [ 647879.341679102857597, 4161635.372573387343436 ], [ 647876.241502788383514, 4161635.872232513967901 ], [ 647868.445927470806055, 4161637.176076213829219 ], [ 647866.150291252415627, 4161637.135130017530173 ], [ 647770.255428230622783, 4161635.869233383797109 ], [ 647766.972603302798234, 4161651.571556095965207 ], [ 647760.328006867086515, 4161672.541566835716367 ], [ 647720.724104659864679, 4161768.731609313283116 ], [ 647664.439331455505453, 4161899.032108286395669 ], [ 647627.416294273571111, 4161989.274946445599198 ], [ 647621.465386576252058, 4162006.039716630242765 ], [ 647618.257487860973924, 4162037.393264054320753 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "200610008021023", "area_land": 5630, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 0 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 683379.173801723285578, 4339718.274099923670292 ], [ 683388.636045723804273, 4339720.16153553687036 ], [ 683406.247825293801725, 4339226.619054754264653 ], [ 683392.94784223777242, 4339226.196451342664659 ], [ 683381.891119970707223, 4339620.723863031715155 ], [ 683379.173801723285578, 4339718.274099923670292 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "201474751002233", "area_land": 19988, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 489352.375147499202285, 4391401.554798025637865 ], [ 489353.078518448804971, 4391476.024222874082625 ], [ 489437.041688203113154, 4391475.020668931305408 ], [ 489452.736617584130727, 4391474.888162637129426 ], [ 489455.068263905821368, 4391423.943221963010728 ], [ 489469.648946393863298, 4391424.256213800981641 ], [ 489503.608059620310087, 4391421.102274054661393 ], [ 489502.105179723410401, 4391325.324975534342229 ], [ 489455.362240921007469, 4391325.499848640523851 ], [ 489448.500717744231224, 4391325.398266786709428 ], [ 489438.294428156630602, 4391325.41226374451071 ], [ 489387.434208348044194, 4391325.260247093625367 ], [ 489352.443056440912187, 4391326.640378216281533 ], [ 489352.375147499202285, 4391401.554798025637865 ] ] ] } }, +{ "type": "Feature", "properties": { "path": "200834611002251", "area_land": 2577172, "area_water": 0, "non_hispanic_two_or_more_races_vap": 0, "non_hispanic_other_vap": 0, "hispanic_vap": 0, "non_hispanic_amin_vap": 0, "non_hispanic_asian_vap":0, "non_hispanic_black_vap":0,"non_hispanic_nhpi_vap":0,"non_hispanic_white_vap":0, "total_vap": 2 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 409731.251194308104459, 4200666.441016187891364 ], [ 409735.139420253282879, 4200955.250511697493494 ], [ 409736.066063234466128, 4201047.233642023056746 ], [ 409740.753175496880431, 4201504.041755408979952 ], [ 409810.145600938529242, 4201502.277895309962332 ], [ 409972.496923764934763, 4201500.4900694899261 ], [ 410110.95236269995803, 4201498.967932443134487 ], [ 410405.217876273440197, 4201492.078675809316337 ], [ 411342.044738269527443, 4201478.989577942527831 ], [ 411328.750386512838304, 4200568.193698559887707 ], [ 411324.006945162313059, 4200243.884643631987274 ], [ 411318.396692238282412, 4199863.879075475037098 ], [ 410818.479345699190162, 4199874.968814136460423 ], [ 410594.067309823876712, 4199878.082232806831598 ], [ 410399.997044122719672, 4199883.310992266982794 ], [ 410315.125831638462842, 4199885.238996434025466 ], [ 410086.93547103140736, 4199888.407714327797294 ], [ 409722.124784207146149, 4199894.641732443124056 ], [ 409731.251194308104459, 4200666.441016187891364 ] ] ] } } ] } diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 724089d74..94b70ff8e 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -61,13 +61,15 @@ def ks_demo_view_census_blocks_fixture(session: Session): def ks_demo_view_census_blocks_districtrmap_fixture( session: Session, ks_demo_view_census_blocks_total_vap: None ): - upsert_query = text(""" + upsert_query = text( + """ INSERT INTO gerrydbtable (uuid, name, updated_at) VALUES (gen_random_uuid(), :name, now()) ON CONFLICT (name) DO UPDATE SET updated_at = now() - """) + """ + ) session.begin() session.execute(upsert_query, {"name": GERRY_DB_FIXTURE_NAME}) @@ -104,13 +106,15 @@ def ks_demo_view_census_blocks_total_vap_fixture(session: Session): def ks_demo_view_census_blocks_total_vap_districtrmap_fixture( session: Session, ks_demo_view_census_blocks_total_vap: None ): - upsert_query = text(""" + upsert_query = text( + """ INSERT INTO gerrydbtable (uuid, name, updated_at) VALUES (gen_random_uuid(), :name, now()) ON CONFLICT (name) DO UPDATE SET updated_at = now() - """) + """ + ) session.begin() session.execute(upsert_query, {"name": GERRY_DB_TOTAL_VAP_FIXTURE_NAME}) @@ -147,13 +151,15 @@ def ks_demo_view_census_blocks_no_pop_fixture(session: Session): def ks_demo_view_census_blocks_no_pop_districtrmap_fixture( session: Session, ks_demo_view_census_blocks_no_pop: None ): - upsert_query = text(""" + upsert_query = text( + """ INSERT INTO gerrydbtable (uuid, name, updated_at) VALUES (gen_random_uuid(), :name, now()) ON CONFLICT (name) DO UPDATE SET updated_at = now() - """) + """ + ) session.begin() session.execute(upsert_query, {"name": GERRY_DB_NO_POP_FIXTURE_NAME}) @@ -513,13 +519,15 @@ def ks_demo_view_census_blocks_summary_stats(session: Session): ], ) - upsert_query = text(""" + upsert_query = text( + """ INSERT INTO gerrydbtable (uuid, name, updated_at) VALUES (gen_random_uuid(), :name, now()) ON CONFLICT (name) DO UPDATE SET updated_at = now() - """) + """ + ) session.execute(upsert_query, {"name": layer}) @@ -529,9 +537,10 @@ def ks_demo_view_census_blocks_summary_stats(session: Session): parent_layer_name=layer, gerrydb_table_name=layer, ) - add_available_summary_stats_to_districtrmap( + summary_stats = add_available_summary_stats_to_districtrmap( session=session, districtr_map_uuid=districtr_map_uuid ) + assert summary_stats == ["P1"], f"Expected P1 to be available, got {summary_stats}" session.commit() @@ -598,13 +607,15 @@ def ks_demo_view_census_blocks_summary_stats_p4(session: Session): ], ) - upsert_query = text(""" + upsert_query = text( + """ INSERT INTO gerrydbtable (uuid, name, updated_at) VALUES (gen_random_uuid(), :name, now()) ON CONFLICT (name) DO UPDATE SET updated_at = now() - """) + """ + ) session.execute(upsert_query, {"name": layer}) diff --git a/backend/tests/test_utils.py b/backend/tests/test_utils.py index 7e03b154a..e730ff424 100644 --- a/backend/tests/test_utils.py +++ b/backend/tests/test_utils.py @@ -6,10 +6,12 @@ create_parent_child_edges, add_extent_to_districtrmap, get_available_summary_stats, + update_districtrmap, ) from sqlmodel import Session import subprocess from app.constants import GERRY_DB_SCHEMA +from app.models import DistrictrMap from tests.constants import OGR2OGR_PG_CONNECTION_STRING, FIXTURES_PATH from sqlalchemy import text @@ -193,6 +195,36 @@ def test_create_districtr_map_some_nulls(session: Session, simple_parent_geos_ge session.commit() +@pytest.fixture(name="simple_parent_geos_districtrmap") +def simple_parent_geos_districtrmap_fixture( + session: Session, simple_parent_geos_gerrydb, simple_child_geos_gerrydb +): + gerrydb_name = "simple_geos_test" + (inserted_districtr_map,) = create_districtr_map( + session, + name="Simple shatterable layer", + gerrydb_table_name=gerrydb_name, + num_districts=10, + tiles_s3_path="tilesets/simple_shatterable_layer.pmtiles", + parent_layer_name="simple_parent_geos", + child_layer_name="simple_child_geos", + visibility=True, + ) + session.commit() + return gerrydb_name + + +def test_update_districtr_map(session: Session, simple_parent_geos_districtrmap): + result = update_districtrmap( + session=session, + gerrydb_table_name=simple_parent_geos_districtrmap, + visible=False, + ) + session.commit() + districtr_map = DistrictrMap.model_validate(result) + assert not districtr_map.visible + + def test_add_extent_to_districtrmap(session: Session, simple_parent_geos_gerrydb): (inserted_districtr_map,) = create_districtr_map( session, diff --git a/pipelines/simple_elt/main.py b/pipelines/simple_elt/main.py index 268a24c27..8327edb28 100644 --- a/pipelines/simple_elt/main.py +++ b/pipelines/simple_elt/main.py @@ -5,10 +5,11 @@ import os import click import logging +from urllib.request import urlretrieve from urllib.parse import urlparse from subprocess import run from typing import Iterable - +import json from files import download_and_unzip_zipfile, exists_in_s3, download_file_from_s3 from settings import settings @@ -88,7 +89,8 @@ def create_county_tiles(replace: bool = False, upload: bool = False): LOGGER.info("Creating county label centroids") label_fgb = settings.OUT_SCRATCH / f"{file_name}_label.fgb" if replace or not label_fgb.exists(): - duckdb.execute(f""" + duckdb.execute( + f""" INSTALL SPATIAL; LOAD spatial; COPY ( SELECT @@ -98,7 +100,8 @@ def create_county_tiles(replace: bool = False, upload: bool = False): FROM st_read('{fgb}') ) TO '{label_fgb}' WITH (FORMAT GDAL, DRIVER 'FlatGeobuf', SRS 'EPSG:4326') - """) + """ + ) LOGGER.info("Creating county label tiles") label_tiles = settings.OUT_SCRATCH / f"{file_name}_label.pmtiles" @@ -311,5 +314,99 @@ def merge_gerrydb_tilesets( ) +@cli.command("load-districtr-v1-places") +@click.option("--replace", is_flag=True, help="Replace existing files", default=False) +def load_districtr_v1_places(replace: bool = False) -> None: + """ + Load data from districtr_v1 endpoint to the s3 bucket for later ingestion + """ + districtr_places = urlretrieve( + "https://districtr.org/assets/data/landing_pages.json?v=2", + settings.OUT_SCRATCH / "districtr_v1_places.json", + ) + + s3_client = settings.get_s3_client() + + key = f"{S3_PREFIX}/districtr_places/districtr_v1_places.json" + print(s3_client, settings.S3_BUCKET, key) + s3_client.upload_file(districtr_places, settings.S3_BUCKET, key) + + +def upsert_places_and_problems(): + """ + Upsert places and problems from districtr_v1. + WIP/not functional port of load_dv1_places_and_problems_problems in #167 + """ + + raise NotImplementedError + + s3_client = settings.get_s3_client() + + key = f"{S3_PREFIX}/districtr_places/districtr_v1_places.json" + districtr_places = download_file_from_s3(s3_client, urlparse(key)) + + if not districtr_places: + LOGGER.error("Failed to download districtr_v1_places.json") + return + + with open(districtr_places, "r") as file: + places = json.load(file) + + for place in places: + url = place["state"] + LOGGER.info(f"Downloading problems for {url}") + try: + problems = urlretrieve( + f"https://districtr.org/assets/data/modules/{place['state'].lower()}.json", + settings.OUT_SCRATCH / f"{place['state'].lower()}_problems.json", + ) + key = ( + f"{S3_PREFIX}/districtr_problems/{place['state'].lower()}_problems.json" + ) + s3_client.upload_file(problems, settings.S3_BUCKET, key) + except Exception as e: + LOGGER.error(f"Failed to download problems for {url}: {e}") + continue + + # load districtr_v1 places and problems + load_districtr_v1_places() + load_districtr_v1_problems() + + +@cli.command("load-districtr-v1-problems") +@click.option("--replace", is_flag=True, help="Replace existing files", default=False) +def load_districtr_v1_problems(replace: bool = False) -> None: + """ + load problems definition json file for states from districtr_v1 and store in s3 bucket + """ + s3_client = settings.get_s3_client() + + # check if the districtr_places object exists in s3; if not, download it using load_districtr_v1_places + key = f"{S3_PREFIX}/districtr_places/districtr_v1_places.json" + if not exists_in_s3(s3_client, settings.S3_BUCKET, key): + load_districtr_v1_places() + + districtr_places = download_file_from_s3(s3_client, urlparse(key), replace) + + with open(districtr_places, "r") as file: + places = json.load(file) + + for place in places: + url = place["state"] + LOGGER.info(f"Downloading problems for {url}") + try: + problems = urlretrieve( + f"https://districtr.org/assets/data/modules/{place['state'].lower()}.json", + settings.OUT_SCRATCH / f"{place['state'].lower()}_problems.json", + ) + key = ( + f"{S3_PREFIX}/districtr_problems/{place['state'].lower()}_problems.json" + ) + s3_client.upload_file(problems, settings.S3_BUCKET, key) + except Exception as e: + LOGGER.error(f"Failed to download problems for {url}: {e}") + continue + + if __name__ == "__main__": cli()