1package arcgishub23import (4 "fmt"5 "strconv"6 "strings"7 "time"89 "github.com/FelineStateMachine/atlas/internal/generate/doc"10)1112// The curation table, which is this source's whole authority.13//14// A city's hub publishes dozens of layers under machine names, and what a15// bundle should make of each one -- a pin or a zone, titled from which field,16// described from which others, bucketed how -- is an editorial judgement about17// that particular city. It lives here, in reviewed code, and it is applied when18// a capture is read rather than when it is taken: the archive keeps what the19// city published, and re-curating replays over every day already on disk.20//21// An uncurated city is refused at the door. An unverified bounding box would22// hang every pin on the wrong pixel and unverified field names would title every23// zone with nothing, so the table is a gate rather than a default.24//25// What is *not* here is how to fetch any of it -- item ids, layer numbers, the26// field allowlist, the query filters. Those are the crawler's, and the crawler27// is the only thing in this rewrite permitted to reach the network. A source is28// a pure function of bytes already on disk (issue #5 §5.1), so its table says29// only what a capture means.3031// dataset is one curated layer as the reader understands it.32type dataset struct {33 Slug string34 Title string35 // Geometry is the shape the capture normalized this layer to:36 // "point", "line" or "polygon".37 Geometry string3839 // Pins. A dataset with Group set becomes a point collection filed under40 // that heading; Icon names a standard glyph for it to be resolved to.41 Group string42 Icon string43 TitleOf func(fields) string44 Describe func(fields) string4546 // Zones. ZoneOf folds a layer's rows into buckets, each bucket one shape47 // feature of the dataset's collection. StrokeWidth is the ground width a48 // line dataset's features are drawn and clicked at, in world pixels; Label49 // is the label policy the collection declares, and empty means the50 // format's own default for the kind.51 ZoneOf func(fields) zoneKey52 StrokeWidth float6453 Label string5455 // National says this layer came from a federal service by bounding box56 // rather than from the city's own hub. It is not a fetch detail here: the57 // membership join claims a subwatershed for the city's own zones and never58 // for the nation's, so the reader has to be able to tell them apart.59 National bool6061 // Basemap. Role is what the renderer draws this layer as, and Emphasis62 // scales a stroke per row -- an arterial wider than a lane. Neither is read63 // by translation; they are the curation the offline renderer answers to,64 // and they live beside the rest of a layer's judgement rather than in a65 // second table that could drift from it.66 Role string67 Emphasis func(fields) float6468}6970// zoneKey says which bucket a row folds into: the bucket's stable key and the71// title and subtitle the resulting shape wears. An empty key is a row that makes72// no zone.73type zoneKey struct {74 Key string75 Title string76 Subtitle string77}7879// city is one curated hub.80type city struct {81 Slug string82 Title string83 BBox [4]float64 // west, south, east, north, degrees84 Datasets []dataset85}8687// datasets is the city's own curation followed by the national enrichment every88// US city receives by bounding box, in that order, so a city's zones keep their89// places and the nation's sort after them. The order is the legend order.90func (c city) datasets() []dataset {91 out := make([]dataset, 0, len(c.Datasets)+len(national))92 out = append(out, c.Datasets...)93 return append(out, national...)94}9596// cities is every city this source can read. A city that should stay out of97// the repository is not registered here at all, and there is deliberately no98// gitignored companion file that would extend this table: a table a reviewer99// cannot see the whole of is not a gate.100var cities = map[string]city{101 "bend-or": bend,102 "redondo-beach-ca": redondoBeach,103}104105// bend is the City of Bend, Oregon -- the proof city, chosen for a hub surface106// with every shape the format wants to prove: polygon ground, named line work,107// and titled points. The water utility layers stay out deliberately: their108// attribute tables carry staff account names, and an open capture keeps people109// out of it.110var bend = city{111 Slug: "bend-or",112 Title: "Bend, Oregon",113 BBox: [4]float64{-121.4180, 43.9500, -121.2170, 44.1650},114 Datasets: []dataset{115 {116 Slug: "mpo-boundary", Title: "MPO Boundary",117 Geometry: "polygon",118 ZoneOf: func(fields) zoneKey {119 return zoneKey{Key: "mpo-boundary", Title: "MPO Boundary",120 Subtitle: "Metropolitan planning area"}121 },122 Role: "boundary",123 },124 {125 Slug: "zoning", Title: "Zoning",126 Geometry: "polygon",127 ZoneOf: func(f fields) zoneKey {128 code := f["ZONE"]129 if code == "" {130 return zoneKey{}131 }132 return zoneKey{Key: doc.Slugify(code), Title: code}133 },134 },135 {136 Slug: "annexations", Title: "Annexations",137 Geometry: "polygon",138 // Annexations fold into the decades they took effect in: the story139 // a version history wants to tell is how the city grew, not which140 // ordinance grew it.141 ZoneOf: func(f fields) zoneKey {142 ms, err := strconv.ParseInt(f["EFF_DATE"], 10, 64)143 if err != nil || ms == 0 {144 return zoneKey{Key: "undated", Title: "Annexed, date unknown"}145 }146 decade := time.UnixMilli(ms).UTC().Year() / 10 * 10147 return zoneKey{148 Key: strconv.Itoa(decade) + "s",149 Title: fmt.Sprintf("Annexed %d–%d", decade, decade+9),150 }151 },152 },153 {154 Slug: "wetlands", Title: "Wetlands",155 Geometry: "polygon",156 ZoneOf: func(f fields) zoneKey {157 kind := f["TYPE"]158 if kind == "" {159 return zoneKey{}160 }161 return zoneKey{Key: doc.Slugify(kind), Title: kind,162 Subtitle: "Local Wetlands Inventory"}163 },164 Role: "water",165 },166 {167 Slug: "trails", Title: "Paths & Trails",168 Geometry: "line",169 // Every existing segment draws into the basemap; the named trails170 // earn zones, each one clickable end to end.171 ZoneOf: func(f fields) zoneKey {172 if f["Status"] != "Existing" || f["Trail_Name"] == "" {173 return zoneKey{}174 }175 return zoneKey{Key: doc.Slugify(f["Trail_Name"]), Title: f["Trail_Name"],176 Subtitle: named(f["Park"], "Trail")}177 },178 StrokeWidth: 12,179 Role: "trail",180 },181 {182 Slug: "road-centerlines", Title: "Road Centerlines",183 Geometry: "line",184 Role: "street",185 Emphasis: func(f fields) float64 {186 switch f["roadclass"] {187 case "Local":188 return 1189 case "Collector":190 return 1.4191 default: // arterials and everything grander192 return 1.8193 }194 },195 },196 {197 Slug: "historic-sites", Title: "Historic Resources",198 Geometry: "point",199 Group: "Heritage",200 Icon: "maki/monument",201 TitleOf: func(f fields) string { return f["NAME"] },202 Describe: func(f fields) string {203 return join(" · ", f["TAB_NAME"], f["SHORT_DESC"],204 f["DESC1"], f["DESC2"], f["DESC3"], f["DESC4"], f["DESC5"])205 },206 },207 },208}209210// redondoBeach is the City of Redondo Beach, California -- the second proof211// city. Its land-use layer is parcel-grained and carries the zoning code on212// every polygon, so the zoning zones bucket by code and double as the basemap's213// parcel texture.214var redondoBeach = city{215 Slug: "redondo-beach-ca",216 Title: "Redondo Beach, California",217 BBox: [4]float64{-118.4060, 33.8110, -118.3490, 33.8985},218 Datasets: []dataset{219 {220 Slug: "land-use", Title: "Zoning",221 Geometry: "polygon",222 ZoneOf: func(f fields) zoneKey {223 code := strings.TrimSpace(f["ZONINGCODE"])224 if code == "" {225 return zoneKey{}226 }227 return zoneKey{Key: doc.Slugify(code), Title: code}228 },229 Role: "parcel",230 },231 {232 Slug: "general-plan", Title: "General Plan",233 Geometry: "polygon",234 ZoneOf: func(f fields) zoneKey {235 code := strings.TrimSpace(f["GENERALPLAN"])236 if code == "" {237 return zoneKey{}238 }239 return zoneKey{Key: doc.Slugify(code), Title: code, Subtitle: "General Plan"}240 },241 },242 {243 Slug: "council-districts", Title: "Council Districts",244 Geometry: "polygon",245 ZoneOf: func(f fields) zoneKey {246 number := strings.TrimSpace(f["District"])247 if number == "" {248 return zoneKey{}249 }250 return zoneKey{Key: "district-" + doc.Slugify(number),251 Title: "Council District " + number,252 Subtitle: strings.TrimSpace(f["CouncilMember"])}253 },254 },255 {256 Slug: "district-lines", Title: "Council District Boundaries",257 Geometry: "line",258 Role: "boundary",259 },260 {261 Slug: "parks", Title: "Parks & Recreation",262 Geometry: "point",263 Group: "Recreation",264 Icon: "maki/park",265 TitleOf: func(f fields) string { return f["name"] },266 Describe: func(f fields) string {267 return join(" · ", f["address"], f["facility_size"], f["web_desc"])268 },269 },270 },271}272273// join glues the non-empty pieces with the separator, which is most of what274// composing a card from optional fields is.275func join(sep string, pieces ...string) string {276 kept := make([]string, 0, len(pieces))277 for _, piece := range pieces {278 if trimmed := strings.TrimSpace(piece); trimmed != "" {279 kept = append(kept, trimmed)280 }281 }282 return strings.Join(kept, sep)283}284285// named prefers what was given and falls back when it is empty.286func named(given, fallback string) string {287 if given != "" {288 return given289 }290 return fallback291}