1// The filtering side of the collections model. Every feature belongs to a2// named collection, and highlighting reads AND across collections, OR within3// one: two districts highlighted widens the question, a district and a4// subwatershed narrows it to the ground they share.56import { state } from "./state.js";78// collectionOf names the collection a shape feature belongs to: the id it9// was stamped with when its collection was rendered.10export function collectionOf(zone) {11 return zone?.collectionId;12}1314// collectionFor answers with the collection itself, for anything that needs15// its attributes -- the label ladder, the stroke width -- rather than its id.16export function collectionFor(zone) {17 return state.world?.collectionsById?.get(zone?.collectionId);18}1920// anyShapeCollectionVisible says whether any ground is drawn at all: the21// zone layers stay up while one shape collection is shown, and each feature's22// own style answers for its collection from there.23export function anyShapeCollectionVisible() {24 return (state.world?.collections || []).some(25 (collection) => collection.kind !== "point" && !isCollectionHidden(collection.id),26 );27}2829// isCollectionHidden is the one question every renderer asks about a30// collection: pins ask by their collection's id, shape features ask through31// collectionOf. Nothing outside the legend touches the set directly.32export function isCollectionHidden(collectionID) {33 return state.hiddenCollections.has(collectionID);34}3536// groupByCollection buckets highlighted zone records under their collections,37// which is the shape the filter thinks in: each bucket a question, its38// members the acceptable answers.39export function groupByCollection(records) {40 const groups = new Map();41 for (const record of records) {42 const key = collectionOf(record.zone);43 if (!groups.has(key)) groups.set(key, []);44 groups.get(key).push(record);45 }46 return groups;47}4849// passesZoneFilters answers whether a coordinate survives the highlights: it50// must lie inside at least one highlighted zone of every collection holding51// any. Within a collection the zones are alternatives; across collections52// they are conditions.53export function passesZoneFilters(groups, coordinate) {54 for (const records of groups.values()) {55 const inside = records.some((record) =>56 record.geometries.some((geometry) => geometryContainsCoordinate(geometry, coordinate)));57 if (!inside) return false;58 }59 return true;60}6162// Containment with a pixel of grace: a pin dropped on a zone's border was put63// there to mean the zone, and exact point-in-polygon arithmetic would flip it64// out over the width of the line it stands on.65export function geometryContainsCoordinate(geometry, coordinate) {66 if (geometry.intersectsCoordinate(coordinate)) return true;67 const closest = geometry.getClosestPoint(coordinate);68 const x = closest[0] - coordinate[0];69 const y = closest[1] - coordinate[1];70 return x * x + y * y <= 1;71}