import { Feature , MultiPolygon as GeoJSONMultiPolygon } from "geojson" ; import * as topojson from "topojson-client" ; import { GeometryCollection , GeometryObject , MultiPolygon , Polygon , Topology } from "topojson-specification" ; import * as _ from "lodash" ; import area from "@turf/area" ; import length from "@turf/length" ; import polygonToLine from "@turf/polygon-to-line" ; import { Contiguity , DistrictProperties , GeoUnitCollection , GeoUnitDefinition , HierarchyDefinition , IStaticMetadata , TypedArrays , DistrictsDefinition , GeoUnitHierarchy , IRegionConfig , IUser , IChamber } from "../../../../shared/entities" ; import { getAllBaseIndices , getDemographics , getVoting } from "../../../../shared/functions" ; import { DistrictsGeoJSON } from "../../projects/entities/project.entity" ; import { mapValues } from "lodash" ; interface GeoUnitPolygonHierarchy { geom : Polygon | MultiPolygon ; children : ReadonlyArray < GeoUnitPolygonHierarchy > ; } type GroupedPolygons = { [ groupName ] : { [ geounitId ] : ReadonlyArray < Polygon | MultiPolygon > ; } ; } ; function calcPolsbyPopper ( feature ) { if ( feature . geometry && feature . geometry . type === "MultiPolygon" && feature . geometry . coordinates . length === 0 ) { return [ 0 , "" ] ; } if ( feature . geometry && feature . geometry . type === "MultiPolygon" && feature . geometry . coordinates . length > 1 ) { return [ 0 , "non-contiguous" ] ; } const districtArea = area ( feature ) ; const outline = polygonToLine ( feature ) ; const districtPerimeter = length ( outline , { units : "meters" } ) ; return [ ( 4 * Math . PI * districtArea ) / districtPerimeter ** 2 , "contiguous" ] ; } function group ( topology , definition ) { const geounitsByParentId = definition . groups . map ( ( groupName , index ) => { const parentCollection = topology . objects [ groupName ] as GeometryCollection ; const mutableMappings = Object . fromEntries ( parentCollection . geometries . map ( ( geom ) => [ geom . properties [ groupName ] , [ ] ] ) ) ; const childGroupName = definition . groups [ index + 1 ] ; if ( childGroupName ) { const childCollection = topology . objects [ childGroupName ] as GeometryCollection ; childCollection . geometries . forEach ( ( geometry ) => { if ( geometry . type === "Polygon" || geometry . type === "MultiPolygon" ) { mutableMappings [ geometry . properties [ groupName ] ] . push ( geometry ) ; } } ) ; } return [ groupName , mutableMappings ] ; } ) ; const firstGroup = definition . groups [ 0 ] ; const toplevelCollection = topology . objects [ firstGroup ] as GeometryCollection < any > ; const geounits = Object . fromEntries ( geounitsByParentId ) ; return toplevelCollection . geometries . map ( geom => getNode ( geom , definition , geounits ) ) ; } function getNode ( geometry , definition , geounitsByParentId ) { const firstGroup = definition . groups [ 0 ] ; const remainingGroups = definition . groups . slice ( 1 ) ; const geomId = geometry . properties [ firstGroup ] ; const childGeoms = geounitsByParentId [ firstGroup ] [ geomId ] ; return { geom : geometry , children : childGeoms . map ( childGeom => getNode ( childGeom , { ... definition , groups : remainingGroups } , geounitsByParentId ) ) } as GeoUnitPolygonHierarchy ; } function groupForHierarchy ( topology , definition ) { const geounitsByParentId = definition . groups . map ( ( groupName , index ) => { const parentCollection = topology . objects [ groupName ] as GeometryCollection ; const mutableMappings = Object . fromEntries ( parentCollection . geometries . map ( ( geom ) => [ geom . properties [ groupName ] , [ ] ] ) ) ; const childGroupName = definition . groups [ index + 1 ] ; if ( childGroupName ) { const childCollection = topology . objects [ childGroupName ] as GeometryCollection < any > ; childCollection . geometries . forEach ( ( geometry ) => { if ( geometry . type === "Polygon" || geometry . type === "MultiPolygon" ) { mutableMappings [ geometry . properties [ groupName ] ] . push ( geometry ) ; } } ) ; } return [ groupName , mutableMappings ] ; } ) ; const firstGroup = definition . groups [ 0 ] ; const toplevelCollection = topology . objects [ firstGroup ] as GeometryCollection < any > ; const geounits = Object . fromEntries ( geounitsByParentId ) ; return toplevelCollection . geometries . map ( geom => getNodeForHierarchy ( geom , definition , geounits ) ) ; } function getNodeForHierarchy ( geometry , definition , geounitsByParentId ) { const firstGroup = definition . groups [ 0 ] ; const remainingGroups = definition . groups . slice ( 1 ) ; const geomId = geometry . properties [ firstGroup ] ; const childGeoms = geounitsByParentId [ firstGroup ] [ geomId ] ; return remainingGroups . length > 1 ? childGeoms . map ( childGeom => getNodeForHierarchy ( childGeom as GeometryObject < any > , { ... definition , groups : remainingGroups } , geounitsByParentId ) ) : childGeoms . map ( ( childGeom ) => childGeom . id ) ; } type FeatureProperties = Pick < DistrictProperties , "demographics" | "voting" > ; export class GeoUnitTopology { public readonly hierarchy : ReadonlyArray < GeoUnitPolygonHierarchy > ; constructor ( public readonly topology , public readonly definition , public readonly staticMetadata , public readonly demographics , public readonly voting , public readonly geoLevels ) { this . hierarchy = group ( topology , definition ) ; } merge ( { districtsDefinition , numberOfDistricts , user , chamber , regionConfig } ) { const mutableDistrictGeoms = Array . from ( Array ( numberOfDistricts + 1 ) ) . map ( _ => this . staticMetadata . geoLevelHierarchy . map ( _ => [ ] ) ) ; const addToDistrict = ( elem , hierarchy , level = 0 ) : boolean => { if ( Array . isArray ( elem ) ) { if ( elem . length !== hierarchy . children . length ) { return false ; } return elem . every ( ( subelem , idx ) => addToDistrict ( subelem , hierarchy . children [ idx ] , level + 1 ) ) ; } else if ( typeof elem === "number" && elem >= 0 ) { const districtIndex = elem ; mutableDistrictGeoms [ districtIndex ] [ level ] . push ( hierarchy . geom ) ; return true ; } return false ; } ; const valid = districtsDefinition . length === this . hierarchy . length && districtsDefinition . every ( ( elem , idx ) => addToDistrict ( elem , this . hierarchy [ idx ] ) ) ; if ( ! valid ) { return null ; } const merged = mutableDistrictGeoms . map ( ( geometries , idx ) => { const mutableGeom = topojson . mergeArcs ( this . topology , geometries . flat ( ) ) ; const baseIndices = geometries . reduce ( ( indices , levelGeometries , levelIndex ) => { const levelIds = levelGeometries . map ( geom => geom . id ) . filter ( id => id !== undefined && typeof id === "number" ) as number [ ] ; const levelIndices = getAllBaseIndices ( this . geoLevels . slice ( ) . reverse ( ) , levelIndex , levelIds ) ; return indices . concat ( levelIndices ) ; } , [ ] ) ; mutableGeom . id = idx ; const geom = { ... mutableGeom , properties : { demographics : getDemographics ( baseIndices , this . staticMetadata , this . demographics ) , voting : getVoting ( baseIndices , this . staticMetadata , this . voting ) } } ; return geom ; } ) ; const featureCollection = topojson . feature ( this . topology , { type : "GeometryCollection" , geometries : merged } ) ; return { ... featureCollection , metadata : { completed : featureCollection . features [ 0 ] . geometry . type === "MultiPolygon" && featureCollection . features [ 0 ] . geometry . coordinates . length === 0 , chamber , creator : _ . pick ( user , [ "id" , "name" ] ) , regionConfig : _ . pick ( regionConfig , [ "id" , "name" , "regionCode" , "countryCode" , "s3URI" ] ) } , features : featureCollection . features . map ( feature => { const [ compactness , contiguity ] = calcPolsbyPopper ( feature ) ; const geometry = feature . geometry as GeoJSONMultiPolygon ; return { ... feature , geometry , properties : { ... feature . properties , compactness , contiguity } } ; } ) } ; } importFromCSV ( blockToDistricts ) { const baseGeoLevel = this . definition . groups . slice ( ) . reverse ( ) [ 0 ] ; const baseGeoUnitProperties = this . topologyProperties [ baseGeoLevel ] ; const mapToDefinition = ( hierarchySubset ) : DistrictsDefinition => hierarchySubset . map ( hierarchyNumOrArray => { if ( typeof hierarchyNumOrArray === "number" ) { const props = baseGeoUnitProperties [ hierarchyNumOrArray ] ; const id = props [ baseGeoLevel ] ; return blockToDistricts [ id ] || 0 ; } else { const results = mapToDefinition ( hierarchyNumOrArray ) ; return results . length !== 1 && results . every ( item => item === results [ 0 ] ) ? results [ 0 ] : results ; } } ) ; return mapToDefinition ( this . hierarchyDefinition ) ; } get topologyProperties ( ) { return mapValues ( this . topology . objects , collection => collection . type === "GeometryCollection" ? collection . geometries . map ( feature => feature . properties || { } ) : [ ] ) ; } get hierarchyDefinition ( ) { const geoLevelIds = this . staticMetadata . geoLevelHierarchy . map ( level => level . id ) ; const definition = { groups : geoLevelIds . slice ( ) . reverse ( ) } ; return groupForHierarchy ( this . topology , definition ) ; } }