Add a draggable local area picker, official nationwide source profile, typed FeatureSet mappings, configurable compiled presentation, live budget preview, and neutral Sample Region acceptance.
- commit
- a7ab0254919119ce3cbdde68bccc92c9c929bf0f
- parent
- 72e814780ae01e58d74381580d255c0fe91489a6
- author
- Dami <dami.etoile@gmail.com>
| A | docs/ | 188 | ++++++++++++++++ |
| A | internal/ | 353 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 224 | +++++++++++++++++++ |
| M | internal/ | 76 | ++++++ |
| M | internal/ | 151 | +++++++++++++ |
| M | internal/ | 83 | +++++++- |
| M | internal/ | 97 | ++++++++ |
| A | internal/ | 103 | +++++++++ |
| M | internal/ | 1 | + |
@@ -0,0 +1,188 @@
1+# Arbitrary U.S. area review gate2+3+This gate proves that `Sample Region` is a parameter, not a fixture. A creator4+draws any bounded area in the United States, accepts the nationwide defaults,5+and receives one native `.atlas` that renders its raster and semantic features6+with the network disabled.7+8+## Default source profile9+10+| Role | Nationwide source | Selection | Semantic output |11+|---|---|---|---|12+| Basemap | [USGS Topo MapServer](https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer) | one aligned Web Mercator `/export` image, bounded to 4096×4096 | locally derived raster pyramid |13+| Roads | [Census TIGERweb Transportation](https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Transportation/MapServer) | layers 2, 6, and 8; area envelope; all intersecting features | one `roads` path FeatureSet |14+| Water | [Census TIGERweb Hydro](https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Hydro/MapServer) | layers 0 and 1; area envelope; all intersecting features | `waterways` path and `water-bodies` area FeatureSets |15+| Context | [Census TIGERweb State/County](https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/State_County/MapServer/1) | current Counties layer 1; area envelope | `jurisdictions` area FeatureSet |16+17+USGS describes Topo as a 256-pixel, EPSG:3857 cache made from current National18+Map and other public-domain data. Its service metadata carries the complete19+contributor credit. The National Map states that its services and downloads20+are public domain and requests the acknowledgment: “Map services and data21+available from U.S. Geological Survey, National Geospatial Program.”22+23+TIGERweb describes the selected road, hydrography, and county layers as24+January 1, 2025 vintage, EPSG:3857 feature layers. Their service metadata names25+the U.S. Census Bureau as source, advertises GeoJSON, and reports pagination26+support. The generated project records the service URL, layer, public-domain27+status, and Census source credit in its legal/evidence receipt. It must not28+imply Census endorsement.29+30+Authoritative protocol references:31+32+- [ArcGIS export map](https://developers.arcgis.com/rest/services-reference/enterprise/export-map/)33+ defines the projected bounding box and exact output image dimensions.34+- [ArcGIS layer query](https://developers.arcgis.com/rest/services-reference/enterprise/query-map-service-layer/)35+ defines geometry envelopes, spatial references, ordering, object-ID36+ selection, and pagination.37+- [The National Map terms](https://www.usgs.gov/faqs/what-are-terms-uselicensing-map-services-and-data-national-map)38+ state the public-domain status and requested acknowledgment.39+- [Census citation guidance](https://www.census.gov/about/policies/citation.html)40+ is the source-credit policy; the Atlas receipt preserves the exact service41+ copyright text in addition to the authored attribution.42+- [TIGER/Line technical documentation](https://www2.census.gov/geo/pdfs/maps-data/data/tiger/tgrshp2024/TGRSHP2024_TechDoc.pdf)43+ states the federal-work copyright rule, requests Census source credit, and44+ supplies the positional-accuracy and non-jurisdictional-boundary disclaimers45+ that the compiled source record must retain.46+47+## Spatial contract48+49+The picker retains the creator's WGS84 rectangle as intent. Compilation maps50+it to one aligned Web Mercator tile block:51+52+```text53+creator rectangle (EPSG:4326)54+ │ project55+ ▼56+covered global tiles at source zoom Z57+ │ grow to smallest aligned power-of-two square58+ ▼59+origin tile [column, row] + N × N tiles60+ │ request one exact N·256 square and derive local levels61+ ▼62+local pixel plane [0,0,N·256,N·256]63+```64+65+This keeps local pyramid derivation deterministic, makes the captured source66+image independently verifiable, and makes the EPSG:3857-to-local feature67+transform exactly affine.68+For radius `R`, source zoom `Z`, world pixel width `W = 256 * 2^Z`, and tile69+origin `(ox, oy)`, the transform from EPSG:3857 metres is:70+71+```text72+s = W / (2πR)73+x' = s*x + πR*s - 256*ox74+y' = -s*y + πR*s - 256*oy75+```76+77+The native coordinate contract therefore needs distinct tile-column and78+tile-row origins. The existing singular `firstTile` cannot locate an arbitrary79+Earth window and must not be reused for both axes. The generated profile asks80+the export service for PNG; the native raster index still carries a codec per81+tile so other profiles may use mixed PNG/JPEG caches.82+83+Feature queries use the same projected envelope and request EPSG:3857 output.84+The adapter verifies the returned CRS before applying the affine transform.85+Intersecting roads and polygons can extend outside the requested envelope, so86+the transform lane must deterministically clip paths and areas to the aligned87+window before extent validation and partitioning.88+89+## Capture-set contract90+91+Each ArcGIS feature source is one atomic acquisition, not one response:92+93+1. Query the projected envelope with explicit `inSR=3857`, `outSR=3857`,94+ `orderByFields=OBJECTID ASC`, page size, and offset.95+2. Follow `exceededTransferLimit` with the next exact offset. Require object96+ IDs to remain strictly increasing and refuse an empty promised page,97+ duplicate ID, out-of-order ID, or false termination.98+3. Roads use `OID` as stable feature identity and the declared99+ `coalesce(NAME, OID)` title mapping; `OBJECTID` is capture pagination state,100+ not semantic identity. The fallback preserves unnamed features instead of101+ silently changing “all roads” into “named roads.”102+4. Commit the capture-set receipt only after every page and its termination103+ proof validates. A partial acquisition never becomes selectable.104+5. Replay the recorded pages in receipt order. Offline and exact replay make no105+ network request and compile byte-identically from the selected bodies.106+107+The request shapes are generated rather than copied into the manifest:108+109+```text110+GET {layer}/query?f=geojson&returnGeometry=true&outFields=*111+ &geometry={minX,minY,maxX,maxY}&geometryType=esriGeometryEnvelope112+ &inSR=3857&outSR=3857&spatialRel=esriSpatialRelIntersects113+ &where=1%3D1&orderByFields=OBJECTID%20ASC114+ &resultOffset={offset}&resultRecordCount={page size}115+```116+117+The source-neutral contracts use only data meaning:118+119+| FeatureSet | Geometry | Stable identity | Typed properties |120+|---|---|---|---|121+| `roads` | path | `OID` | `name?: string`, `class: string` (`MTFCC`), `route-type?: string` (`RTTYP`) |122+| `waterways` | path | `OID` | `name?: string`, `class: string` (`MTFCC`) |123+| `water-bodies` | area | `OID` | `name?: string`, `class: string`, `land-area?: int64`, `water-area?: int64` |124+| `jurisdictions` | area | `GEOID` | `name: string`, `land-area: int64`, `water-area: int64` |125+126+Title fallback is a small declarative mapping expression, not adapter-owned127+semantics or an arbitrary script.128+129+Raster acquisition records the exact export request, media type, byte length,130+hash, capture time, licence, and attribution. Local pyramid tiles are derived131+only after that source image is captured as one complete selectable set.132+133+## Workbench path134+135+```text136+Choose area ──► Sources ──► Contracts ──► Build ──► Compiled preview137+ drag box defaults mappings live styles + legend138+ or bbox + budget + clipping stages on .atlas truth139+ │140+ Inspect / Open / Drag141+```142+143+The first map is only a source-area selector. After the first compile, every144+transform, feature, raster, layer, style, and legend preview is read from the145+validated `.atlas`; the browser keeps no parallel presentation model.146+147+The generated single manifest uses neutral labels (`sample-region`, `Sample148+Region`) and contains the chosen WGS84 intent, resolved local tile-window149+contract, explicit sources, mappings, legal text, presentation, and budgets.150+The default presentation is:151+152+1. USGS Topo raster;153+2. pale blue water areas and blue waterway paths;154+3. quiet county outlines;155+4. local roads, secondary roads, then primary roads with increasing weight;156+5. labels at source-appropriate minimum zooms and a generated legend.157+158+Area size is governed by evidence, tile, feature, and output budgets rather159+than a hard-coded named place. The UI estimates the aligned tile block before160+capture and offers detail profiles: local roads for bounded local areas,161+primary/secondary roads for regional areas. It refuses an over-budget plan; it162+never silently drops pages or tiles.163+164+## Acceptance evidence165+166+The release gate uses a creator-selected area and fresh temporary library:167+168+- CUA draws the area, keeps the nationwide defaults, inspects the generated169+ neutral manifest, builds, and verifies compiled raster/road/water/context170+ semantics plus legal receipts.171+- The live feature request uses a deliberately small page size so the receipt172+ proves at least two pages and explicit termination.173+- The artifact opens through native intake and by drag/drop, then renders with174+ the network disabled.175+- An offline build into another clean library and an exact-replay build produce176+ the same bytes as the online selection while an HTTP counter remains zero.177+- Reader telemetry proves bootstrap/page zero, visible feature partitions, and178+ visible raster shards were range-read; total bytes stay below the full Atlas179+ size and no whole `Volume` is materialized.180+- Adversarial fixtures cover repeated/out-of-order IDs, missing continuations,181+ false termination, crossing geometry, mixed raster codecs, corrupt shard182+ ranges, and a cancelled partial capture.183+- Packaged macOS, Windows, and Linux acceptance repeats open, association,184+ drag/drop where the desktop supports it, and offline first render.185+186+Live government services are an opt-in review smoke, not a hermetic CI187+dependency. CI uses byte-pinned protocol fixtures that reproduce the same188+multi-page, clipping, mixed-codec, and termination contracts.@@ -0,0 +1,353 @@
1+package authoring2+3+import (4+ "fmt"5+ "math"6+ "net/url"7+ "strconv"8+ "strings"9+10+ "gopkg.in/yaml.v3"11+)12+13+const (14+ usTopoService = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/export"15+ tigerRoads = "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Transportation/MapServer"16+ tigerHydro = "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/Hydro/MapServer"17+ tigerCounties = "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/State_County/MapServer/1"18+ webMercatorR = 20037508.34278924419+ maxExportSize = int64(4096)20+)21+22+// USAreaProfile is the compact authoring choice behind the Workbench's23+// nationwide default. Bounds are WGS84 west, south, east, north coordinates.24+type USAreaProfile struct {25+ Bounds [4]float6426+ DetailZoom int27+ IncludeTopo bool28+ IncludeRoads bool29+ IncludeHydro bool30+ IncludeCounties bool31+ Presentation USAreaPresentation32+}33+34+// USAreaPresentation is the creator-owned layer, label, ordering, and color35+// choice that is compiled into the Atlas presentation and legend.36+type USAreaPresentation struct {37+ RoadLabel, HydroLabel, CountyLabel string38+ RoadColor, HydroColor, HydroFill, CountyColor string39+ RoadVisible, HydroVisible, CountyVisible bool40+ RoadOrder, HydroOrder, CountyOrder int6441+}42+43+// USAreaSummary is the cost preview shown before a manifest is installed.44+type USAreaSummary struct {45+ OriginX int6446+ OriginY int6447+ SourceZoom int48+ PixelSize int6449+ RasterTiles int6450+ CaptureRequests int51+ EstimatedBytes int6452+}53+54+// DefaultUSAreaProfile selects all official nationwide source families.55+func DefaultUSAreaProfile() USAreaProfile {56+ return USAreaProfile{57+ Bounds: [4]float64{-105.1, 39.6, -104.9, 39.8},58+ DetailZoom: 12, IncludeTopo: true, IncludeRoads: true,59+ IncludeHydro: true, IncludeCounties: true,60+ Presentation: USAreaPresentation{61+ RoadLabel: "Roads", HydroLabel: "Water", CountyLabel: "Counties",62+ RoadColor: "#b45f3c", HydroColor: "#4b8db8", HydroFill: "#b9d9ea", CountyColor: "#766b5b",63+ RoadVisible: true, HydroVisible: true, CountyVisible: true,64+ RoadOrder: 40, HydroOrder: 20, CountyOrder: 10,65+ },66+ }67+}68+69+// NewUSAreaProject turns one bounded U.S. region selection into a complete portable70+// project. Captures and artifacts remain outside the manifest.71+func NewUSAreaProject(profile USAreaProfile) (Project, USAreaSummary, error) {72+ window, err := usTileWindow(profile.Bounds, profile.DetailZoom)73+ if err != nil {74+ return Project{}, USAreaSummary{}, err75+ }76+ if !profile.IncludeTopo && !profile.IncludeRoads && !profile.IncludeHydro && !profile.IncludeCounties {77+ return Project{}, USAreaSummary{}, fmt.Errorf("select at least one nationwide source")78+ }79+ if err := validateUSPresentation(profile); err != nil {80+ return Project{}, USAreaSummary{}, err81+ }82+ project := usProjectBase(window, profile.DetailZoom)83+ if profile.IncludeTopo {84+ project.Rasters = append(project.Rasters, usTopoRaster(window, profile.DetailZoom))85+ }86+ if profile.IncludeRoads {87+ addRoads(&project, window.transform, profile.Presentation)88+ }89+ if profile.IncludeHydro {90+ addHydro(&project, window.transform, profile.Presentation)91+ }92+ if profile.IncludeCounties {93+ addCounties(&project, window.transform, profile.Presentation)94+ }95+ if err := project.Validate(); err != nil {96+ return Project{}, USAreaSummary{}, fmt.Errorf("validate U.S. area project: %w", err)97+ }98+ return project, summarizeUSProject(project, window), nil99+}100+101+func validateUSPresentation(profile USAreaProfile) error {102+ choices := []struct {103+ enabled bool104+ label string105+ colors []string106+ order int64107+ }{108+ {profile.IncludeRoads, profile.Presentation.RoadLabel, []string{profile.Presentation.RoadColor}, profile.Presentation.RoadOrder},109+ {profile.IncludeHydro, profile.Presentation.HydroLabel, []string{profile.Presentation.HydroColor, profile.Presentation.HydroFill}, profile.Presentation.HydroOrder},110+ {profile.IncludeCounties, profile.Presentation.CountyLabel, []string{profile.Presentation.CountyColor}, profile.Presentation.CountyOrder},111+ }112+ for _, choice := range choices {113+ if !choice.enabled {114+ continue115+ }116+ if strings.TrimSpace(choice.label) == "" || choice.order < 0 {117+ return fmt.Errorf("enabled presentation layers require labels and non-negative order")118+ }119+ for _, color := range choice.colors {120+ if !validHexColor(color) {121+ return fmt.Errorf("presentation color %q must be #RRGGBB", color)122+ }123+ }124+ }125+ return nil126+}127+128+func validHexColor(value string) bool {129+ if len(value) != 7 || value[0] != '#' {130+ return false131+ }132+ for _, character := range value[1:] {133+ if !((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') || (character >= 'A' && character <= 'F')) {134+ return false135+ }136+ }137+ return true138+}139+140+// MarshalProjectYAML emits the single portable manifest after validating it.141+func MarshalProjectYAML(project Project) ([]byte, error) {142+ if err := project.Validate(); err != nil {143+ return nil, err144+ }145+ data, err := yaml.Marshal(project)146+ if err != nil {147+ return nil, fmt.Errorf("encode project manifest: %w", err)148+ }149+ return data, nil150+}151+152+type usWindow struct {153+ originX, originY int64154+ side, pixels int64155+ maxZoom int64156+ bbox [4]float64157+ transform CoordinateTransform158+}159+160+func usTileWindow(bounds [4]float64, zoom int) (usWindow, error) {161+ if bounds[0] >= bounds[2] || bounds[1] >= bounds[3] {162+ return usWindow{}, fmt.Errorf("area requires west < east and south < north")163+ }164+ if !insideUSRegion(bounds) {165+ return usWindow{}, fmt.Errorf("area must stay within one supported United States region")166+ }167+ if zoom < 6 || zoom > 16 {168+ return usWindow{}, fmt.Errorf("detail zoom must be between 6 and 16")169+ }170+ n := int64(1) << zoom171+ minX, maxY := lonTile(bounds[0], n), latTile(bounds[1], n)172+ maxX, minY := lonTileUpper(bounds[2], n), latTileUpper(bounds[3], n)173+ originX, originY, side := alignedSquare(minX, minY, maxX, maxY)174+ pixels := side * 256175+ if pixels > maxExportSize {176+ return usWindow{}, fmt.Errorf("selection needs a %d-pixel source image; maximum is %d (lower detail or narrow the area)", pixels, maxExportSize)177+ }178+ bbox := mercatorTileBounds(originX, originY, side, n)179+ scale := float64(256*n) / (2 * webMercatorR)180+ transform := CoordinateTransform{Kind: "affine", Matrix: [6]float64{181+ scale, 0, webMercatorR*scale - float64(256*originX),182+ 0, -scale, webMercatorR*scale - float64(256*originY),183+ }}184+ return usWindow{originX: originX, originY: originY, side: side, pixels: pixels, maxZoom: int64(math.Log2(float64(side))), bbox: bbox, transform: transform}, nil185+}186+187+func insideUSRegion(bounds [4]float64) bool {188+ regions := [][4]float64{189+ {-125, 24, -66, 50}, // contiguous states190+ {-180, 50, -129, 72}, // Alaska191+ {-161, 18, -154, 23}, // Hawaii192+ {-68, 17, -65, 19}, // Puerto Rico193+ {-65.2, 17.5, -64.4, 18.6}, // U.S. Virgin Islands194+ {144, 13, 146.2, 21}, // Guam and Northern Mariana Islands195+ {-171, -15, -168, -10}, // American Samoa196+ }197+ for _, region := range regions {198+ if bounds[0] >= region[0] && bounds[1] >= region[1] && bounds[2] <= region[2] && bounds[3] <= region[3] {199+ return true200+ }201+ }202+ return false203+}204+205+func lonTile(longitude float64, count int64) int64 {206+ return int64(math.Floor((longitude + 180) / 360 * float64(count)))207+}208+209+func lonTileUpper(longitude float64, count int64) int64 {210+ return int64(math.Ceil((longitude+180)/360*float64(count))) - 1211+}212+213+func latTile(latitude float64, count int64) int64 {214+ value := (1 - math.Asinh(math.Tan(latitude*math.Pi/180))/math.Pi) / 2 * float64(count)215+ return int64(math.Floor(value))216+}217+218+func latTileUpper(latitude float64, count int64) int64 {219+ value := (1 - math.Asinh(math.Tan(latitude*math.Pi/180))/math.Pi) / 2 * float64(count)220+ return int64(math.Floor(value))221+}222+223+func alignedSquare(minX, minY, maxX, maxY int64) (int64, int64, int64) {224+ for side := int64(1); ; side *= 2 {225+ originX, originY := minX-(minX%side), minY-(minY%side)226+ if originX+side-1 >= maxX && originY+side-1 >= maxY {227+ return originX, originY, side228+ }229+ }230+}231+232+func mercatorTileBounds(originX, originY, side, count int64) [4]float64 {233+ unit := 2 * webMercatorR / float64(count)234+ return [4]float64{235+ -webMercatorR + float64(originX)*unit,236+ webMercatorR - float64(originY+side)*unit,237+ -webMercatorR + float64(originX+side)*unit,238+ webMercatorR - float64(originY)*unit,239+ }240+}241+242+func usProjectBase(window usWindow, sourceZoom int) Project {243+ return Project{244+ Schema: ProjectSchema, SchemaNamespace: "example.invalid/atlas/sample-region/v1",245+ ID: "sample-region", Title: "Sample Region",246+ Target: Target{World: "sample-region", Title: "Sample Region", CoordinateSpace: CoordinateSpace{247+ ID: "sample-region-grid", Kind: "projected", Unit: "pixel", Definition: "atlas:tile-plane",248+ Extent: [4]float64{0, 0, float64(window.pixels), float64(window.pixels)},249+ SourceZoom: int64(sourceZoom), OriginX: window.originX, OriginY: window.originY,250+ TileSize: 256, Size: window.pixels,251+ }},252+ Presentation: Presentation{ID: "default", Title: "Sample Region"},253+ Release: Release{Revision: 1},254+ Budgets: BuildBudgets{RequestBytes: 512 << 20, TotalBytes: 4 << 30, Requests: 10_000, RasterTiles: 10_000, RasterPixels: 1_000_000_000},255+ }256+}257+258+func usTopoRaster(window usWindow, sourceZoom int) Raster {259+ query := url.Values{260+ "bbox": {floatList(window.bbox)}, "bboxSR": {"3857"}, "imageSR": {"3857"},261+ "size": {strconv.FormatInt(window.pixels, 10) + "," + strconv.FormatInt(window.pixels, 10)},262+ "format": {"png32"}, "transparent": {"false"}, "f": {"image"},263+ }264+ return Raster{265+ ID: "usgs-topo", Name: "USGS Topo", Adapter: "raster-file", Locator: usTopoService + "?" + query.Encode(),266+ MediaType: "image/png", License: "U.S. government work; public domain", Attribution: "Map services and data available from U.S. Geological Survey, National Geospatial Program.",267+ EstimateBytes: window.pixels * window.pixels, TileSize: 256, SourceZoom: int64(sourceZoom), MaxZoom: window.maxZoom,268+ FullZoom: window.maxZoom, Interpolate: true, Background: "#e9e4d7",269+ Bounds: &RasterRect{Width: float64(window.pixels), Height: float64(window.pixels)},270+ }271+}272+273+func floatList(values [4]float64) string {274+ return strconv.FormatFloat(values[0], 'f', 6, 64) + "," + strconv.FormatFloat(values[1], 'f', 6, 64) + "," +275+ strconv.FormatFloat(values[2], 'f', 6, 64) + "," + strconv.FormatFloat(values[3], 'f', 6, 64)276+}277+278+func addRoads(project *Project, transform CoordinateTransform, presentation USAreaPresentation) {279+ project.FeatureSets = append(project.FeatureSets, FeatureSetContract{280+ ID: "roads", Title: "Roads", SemanticType: "transportation", Geometry: "path",281+ Properties: []PropertyContract{{ID: "name", Name: "Name", Type: "string", Optional: true}, {ID: "class", Name: "Class", Type: "string", Optional: true}, {ID: "route-type", Name: "Route type", Type: "string", Optional: true}},282+ })283+ for _, layer := range []struct{ id, number string }{{"primary-roads", "2"}, {"secondary-roads", "6"}, {"local-roads", "8"}} {284+ source := tigerSource(layer.id, tigerRoads+"/"+layer.number, "roads", "path", transform)285+ source.Mapping.Identity = "properties.OID"286+ source.Mapping.FeatureTitle = "coalesce(properties.NAME, properties.OID)"287+ source.Mapping.Properties = append(source.Mapping.Properties, PropertyMap{Field: "route-type", Source: "properties.RTTYP"})288+ project.Sources = append(project.Sources, source)289+ }290+ project.Presentation.Styles = append(project.Presentation.Styles, Style{ID: "roads", Stroke: presentation.RoadColor})291+ project.Presentation.Layers = append(project.Presentation.Layers, Layer{ID: "roads", FeatureSet: "roads", Style: "roads", Label: presentation.RoadLabel, Group: "Transportation", LabelPolicy: "quiet", Visible: presentation.RoadVisible, Order: presentation.RoadOrder, MaxZoom: 32})292+}293+294+func addHydro(project *Project, transform CoordinateTransform, presentation USAreaPresentation) {295+ project.FeatureSets = append(project.FeatureSets,296+ FeatureSetContract{ID: "waterways", Title: "Waterways", SemanticType: "hydrography", Geometry: "path", Properties: commonTigerProperties()},297+ FeatureSetContract{ID: "water-bodies", Title: "Water bodies", SemanticType: "hydrography", Geometry: "area", Properties: append(commonTigerProperties(), PropertyContract{ID: "water-area", Name: "Water area", Type: "int64", Optional: true})},298+ )299+ waterways := tigerSource("waterways", tigerHydro+"/0", "waterways", "path", transform)300+ waterBodies := tigerSource("water-bodies", tigerHydro+"/1", "water-bodies", "area", transform)301+ waterBodies.Mapping.Properties = append(waterBodies.Mapping.Properties, PropertyMap{Field: "water-area", Source: "properties.AREAWATER"})302+ project.Sources = append(project.Sources, waterways, waterBodies)303+ project.Presentation.Styles = append(project.Presentation.Styles, Style{ID: "waterways", Stroke: presentation.HydroColor}, Style{ID: "water-bodies", Stroke: presentation.HydroColor, Fill: presentation.HydroFill})304+ project.Presentation.Layers = append(project.Presentation.Layers,305+ Layer{ID: "water-bodies", FeatureSet: "water-bodies", Style: "water-bodies", Label: presentation.HydroLabel + " bodies", Group: presentation.HydroLabel, LabelPolicy: "quiet", Visible: presentation.HydroVisible, Order: presentation.HydroOrder, MaxZoom: 32},306+ Layer{ID: "waterways", FeatureSet: "waterways", Style: "waterways", Label: presentation.HydroLabel + "ways", Group: presentation.HydroLabel, LabelPolicy: "quiet", Visible: presentation.HydroVisible, Order: presentation.HydroOrder + 1, MaxZoom: 32},307+ )308+}309+310+func addCounties(project *Project, transform CoordinateTransform, presentation USAreaPresentation) {311+ project.FeatureSets = append(project.FeatureSets, FeatureSetContract{312+ ID: "jurisdictions", Title: "Counties", SemanticType: "jurisdiction", Geometry: "area",313+ Properties: []PropertyContract{{ID: "name", Name: "Name", Type: "string"}, {ID: "land-area", Name: "Land area", Type: "int64", Optional: true}, {ID: "water-area", Name: "Water area", Type: "int64", Optional: true}},314+ })315+ source := tigerSource("counties", tigerCounties, "jurisdictions", "area", transform)316+ source.Mapping.Identity, source.Mapping.FeatureTitle = "properties.GEOID", "properties.NAME"317+ source.Mapping.Properties = []PropertyMap{{Field: "name", Source: "properties.NAME"}, {Field: "land-area", Source: "properties.AREALAND"}, {Field: "water-area", Source: "properties.AREAWATER"}}318+ project.Sources = append(project.Sources, source)319+ project.Presentation.Styles = append(project.Presentation.Styles, Style{ID: "counties", Stroke: presentation.CountyColor})320+ project.Presentation.Layers = append(project.Presentation.Layers, Layer{ID: "counties", FeatureSet: "jurisdictions", Style: "counties", Label: presentation.CountyLabel, Group: "Context", LabelPolicy: "quiet", Visible: presentation.CountyVisible, Order: presentation.CountyOrder, MaxZoom: 32})321+}322+323+func commonTigerProperties() []PropertyContract {324+ return []PropertyContract{{ID: "name", Name: "Name", Type: "string", Optional: true}, {ID: "class", Name: "Class", Type: "string", Optional: true}}325+}326+327+func tigerSource(id, locator, set, family string, transform CoordinateTransform) Source {328+ return Source{329+ ID: id, Adapter: "arcgis-feature-service", Locator: locator, MediaType: "application/geo+json",330+ License: "U.S. government work; public domain", Attribution: "Source: U.S. Census Bureau TIGERweb. This product does not imply Census Bureau endorsement.",331+ EstimateBytes: 16 << 20, Query: Query{Where: "1=1", Limit: 1000, ObjectID: "OBJECTID", SpatialReference: 3857},332+ Mapping: Mapping{333+ FeatureSet: set, Identity: "properties.OBJECTID", FeatureTitle: "coalesce(properties.NAME, properties.OBJECTID)",334+ Geometry: GeometryMap{Family: family, SourceSpace: "EPSG:3857", Transform: transform},335+ Properties: []PropertyMap{{Field: "name", Source: "properties.NAME"}, {Field: "class", Source: "properties.MTFCC"}},336+ },337+ }338+}339+340+func summarizeUSProject(project Project, window usWindow) USAreaSummary {341+ tiles := (int64(1)<<(2*(window.maxZoom+1)) - 1) / 3342+ estimated := int64(0)343+ for _, source := range project.Sources {344+ estimated += source.EstimateBytes345+ }346+ for _, raster := range project.Rasters {347+ estimated += raster.EstimateBytes348+ }349+ return USAreaSummary{350+ OriginX: window.originX, OriginY: window.originY, SourceZoom: int(project.Target.CoordinateSpace.SourceZoom),351+ PixelSize: window.pixels, RasterTiles: tiles, CaptureRequests: len(project.Sources) + len(project.Rasters), EstimatedBytes: estimated,352+ }353+}@@ -0,0 +1,224 @@
1+package authoring2+3+import (4+ "bytes"5+ "fmt"6+ "strings"7+ "testing"8+9+ "gopkg.in/yaml.v3"10+)11+12+func TestUSAreaProjectBuildsTheNationwideDefaultContract(t *testing.T) {13+ profile := DefaultUSAreaProfile()14+ profile.Bounds = [4]float64{-105.1, 39.6, -104.9, 39.8}15+ profile.DetailZoom = 1216+17+ project, summary, err := NewUSAreaProject(profile)18+ if err != nil {19+ t.Fatal(err)20+ }21+ if err := project.Validate(); err != nil {22+ t.Fatalf("generated project is invalid: %v", err)23+ }24+ if project.ID != "sample-region" || project.Title != "Sample Region" {25+ t.Fatalf("project identity = %q / %q", project.ID, project.Title)26+ }27+ space := project.Target.CoordinateSpace28+ if space.Definition != "atlas:tile-plane" || space.SourceZoom != 12 || space.Size != summary.PixelSize {29+ t.Fatalf("coordinate space = %+v, summary = %+v", space, summary)30+ }31+ if space.Extent != [4]float64{0, 0, float64(space.Size), float64(space.Size)} {32+ t.Fatalf("coordinate extent = %v", space.Extent)33+ }34+ if len(project.Rasters) != 1 || project.Rasters[0].Adapter != "raster-file" || !strings.Contains(project.Rasters[0].Locator, "USGSTopo/MapServer/export") {35+ t.Fatalf("topographic raster = %+v", project.Rasters)36+ }37+ if len(project.Sources) != 6 {38+ t.Fatalf("feature sources = %d, want roads, hydro, and counties", len(project.Sources))39+ }40+ for _, source := range project.Sources {41+ mapped := make(map[string]string, len(source.Mapping.Properties))42+ for _, property := range source.Mapping.Properties {43+ mapped[property.Field] = property.Source44+ }45+ switch source.ID {46+ case "primary-roads", "secondary-roads", "local-roads":47+ if source.Mapping.Identity != "properties.OID" || source.Query.ObjectID != "OBJECTID" || mapped["route-type"] != "properties.RTTYP" {48+ t.Errorf("road identity or mapping = %+v", source)49+ }50+ case "water-bodies":51+ if mapped["water-area"] != "properties.AREAWATER" {52+ t.Errorf("water body mapping = %+v", mapped)53+ }54+ case "counties":55+ if mapped["land-area"] != "properties.AREALAND" || mapped["water-area"] != "properties.AREAWATER" {56+ t.Errorf("county mapping = %+v", mapped)57+ }58+ }59+ }60+ wantSets := map[string]string{"roads": "path", "waterways": "path", "water-bodies": "area", "jurisdictions": "area"}61+ for _, set := range project.FeatureSets {62+ if wantSets[set.ID] != set.Geometry {63+ t.Errorf("feature set %s geometry = %q", set.ID, set.Geometry)64+ }65+ delete(wantSets, set.ID)66+ }67+ if len(wantSets) != 0 {68+ t.Fatalf("missing feature sets: %v", wantSets)69+ }70+ if summary.CaptureRequests != len(project.Sources)+1 || summary.RasterTiles < 1 || summary.EstimatedBytes < 1 {71+ t.Fatalf("summary = %+v", summary)72+ }73+ volume, err := assemble(project, nil)74+ if err != nil {75+ t.Fatalf("assemble presentation: %v", err)76+ }77+ presentation := volume.Worlds[0].Presentation78+ if len(presentation.Legend) != len(project.Presentation.Layers) || len(presentation.Legend) == 0 {79+ t.Fatalf("compiled legend = %+v for layers %+v", presentation.Legend, project.Presentation.Layers)80+ }81+ plan, err := PlanProject(project, nil, t.TempDir())82+ if err != nil {83+ t.Fatalf("generated project does not plan: %v", err)84+ }85+ if plan.RasterTiles != summary.RasterTiles || len(plan.Requests) != summary.CaptureRequests {86+ t.Fatalf("plan = %d tiles / %d requests, summary = %+v", plan.RasterTiles, len(plan.Requests), summary)87+ }88+}89+90+func TestUSAreaProjectCanSelectSourceFamiliesWithoutLosingTypedPresentation(t *testing.T) {91+ profile := DefaultUSAreaProfile()92+ profile.Bounds = [4]float64{-105.1, 39.6, -104.9, 39.8}93+ profile.DetailZoom = 1094+ profile.IncludeHydro = false95+ profile.IncludeCounties = false96+ profile.Presentation.RoadLabel = "Routes"97+ profile.Presentation.RoadColor = "#c05030"98+ profile.Presentation.RoadVisible = false99+ profile.Presentation.RoadOrder = 17100+101+ project, _, err := NewUSAreaProject(profile)102+ if err != nil {103+ t.Fatal(err)104+ }105+ if len(project.FeatureSets) != 1 || project.FeatureSets[0].ID != "roads" {106+ t.Fatalf("selected feature sets = %+v", project.FeatureSets)107+ }108+ if len(project.Presentation.Layers) != 1 || project.Presentation.Layers[0].FeatureSet != "roads" {109+ t.Fatalf("selected presentation = %+v", project.Presentation.Layers)110+ }111+ layer, style := project.Presentation.Layers[0], project.Presentation.Styles[0]112+ if layer.Label != "Routes" || layer.Visible || layer.Order != 17 || style.Stroke != "#c05030" {113+ t.Fatalf("creator presentation choices were lost: layer=%+v style=%+v", layer, style)114+ }115+ for _, source := range project.Sources {116+ if source.Mapping.FeatureSet != "roads" || source.Query.SpatialReference != 3857 || source.Query.ObjectID == "" {117+ t.Fatalf("road source = %+v", source)118+ }119+ }120+}121+122+func TestUSAreaProjectSupportsAlaskaAndHawaii(t *testing.T) {123+ tests := []struct {124+ name string125+ bounds [4]float64126+ }{127+ {name: "Alaska", bounds: [4]float64{-151.0, 60.9, -150.7, 61.2}},128+ {name: "Hawaii", bounds: [4]float64{-157.95, 21.25, -157.7, 21.45}},129+ }130+ for _, tt := range tests {131+ t.Run(tt.name, func(t *testing.T) {132+ profile := DefaultUSAreaProfile()133+ profile.Bounds, profile.DetailZoom = tt.bounds, 11134+ project, _, err := NewUSAreaProject(profile)135+ if err != nil {136+ t.Fatal(err)137+ }138+ if err := project.Validate(); err != nil {139+ t.Fatalf("generated project is invalid: %v", err)140+ }141+ })142+ }143+}144+145+func TestUSRoadLayersDoNotFuseSharedPagingObjectIDs(t *testing.T) {146+ profile := DefaultUSAreaProfile()147+ profile.Bounds = [4]float64{-105.1, 39.6, -104.9, 39.8}148+ profile.DetailZoom = 10149+ profile.IncludeTopo, profile.IncludeHydro, profile.IncludeCounties = false, false, false150+ project, _, err := NewUSAreaProject(profile)151+ if err != nil {152+ t.Fatal(err)153+ }154+ var observations []observation155+ for index, source := range project.Sources[:2] {156+ extent, err := sourceExtent(source.Mapping.Geometry, project.Target.CoordinateSpace)157+ if err != nil {158+ t.Fatal(err)159+ }160+ x, y := (extent[0]+extent[2])/2, (extent[1]+extent[3])/2161+ body := []byte(fmt.Sprintf(`{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"OBJECTID":7,"OID":"road-%d","NAME":"Road %d","MTFCC":"S1100","RTTYP":"I"},"geometry":{"type":"LineString","coordinates":[[%f,%f],[%f,%f]]}}]}`,162+ index, index, x, y, x+1, y+1))163+ seen, err := observe(project, index, source, Capture{}, body)164+ if err != nil {165+ t.Fatal(err)166+ }167+ observations = append(observations, seen...)168+ }169+ volume, err := assemble(project, observations)170+ if err != nil {171+ t.Fatal(err)172+ }173+ if got := len(volume.Worlds[0].FeatureSets[0].Features); got != 2 {174+ t.Fatalf("two road layers sharing OBJECTID fused into %d feature(s)", got)175+ }176+}177+178+func TestUSAreaProjectRefusesOutOfBoundsOrExplosiveSelections(t *testing.T) {179+ tests := []struct {180+ name string181+ bounds [4]float64182+ zoom int183+ message string184+ }{185+ {name: "not a U.S. region", bounds: [4]float64{2, 48, 3, 49}, zoom: 10, message: "United States region"},186+ {name: "empty", bounds: [4]float64{-105, 40, -105, 40}, zoom: 10, message: "west < east"},187+ {name: "too detailed", bounds: [4]float64{-124, 25, -67, 49}, zoom: 16, message: "4096"},188+ }189+ for _, tt := range tests {190+ t.Run(tt.name, func(t *testing.T) {191+ profile := DefaultUSAreaProfile()192+ profile.Bounds, profile.DetailZoom = tt.bounds, tt.zoom193+ if _, _, err := NewUSAreaProject(profile); err == nil || !strings.Contains(err.Error(), tt.message) {194+ t.Fatalf("error = %v, want %q", err, tt.message)195+ }196+ })197+ }198+}199+200+func TestMarshalProjectYAMLRoundTripsStrictly(t *testing.T) {201+ profile := DefaultUSAreaProfile()202+ profile.Bounds = [4]float64{-105.1, 39.6, -104.9, 39.8}203+ profile.DetailZoom = 11204+ project, _, err := NewUSAreaProject(profile)205+ if err != nil {206+ t.Fatal(err)207+ }208+ data, err := MarshalProjectYAML(project)209+ if err != nil {210+ t.Fatal(err)211+ }212+ decoder := yaml.NewDecoder(bytes.NewReader(data))213+ decoder.KnownFields(true)214+ var restored Project215+ if err := decoder.Decode(&restored); err != nil {216+ t.Fatalf("strict decode: %v\n%s", err, data)217+ }218+ if err := restored.Validate(); err != nil {219+ t.Fatalf("round trip: %v\n%s", err, data)220+ }221+ if !strings.Contains(string(data), "title: Sample Region") || !strings.Contains(string(data), "origin-x:") || !strings.Contains(string(data), "origin-y:") {222+ t.Fatalf("portable manifest misses neutral identity or tile origins:\n%s", data)223+ }224+}@@ -247,6 +247,73 @@ textarea {
247247 .artifact-handoff form { margin: 0; }248248 .file-icon { display: inline-grid; place-items: center; width: 30px; height: 36px; margin-right: 10px; color: var(--accent-ink); background: var(--accent); font-weight: 700; float: left; }249249 250+/* spatial authoring ----------------------------------------------------- */251+252+.area-authoring { padding: 0; overflow: hidden; border-left: 3px solid var(--accent); }253+.authoring-head {254+ display: flex;255+ align-items: end;256+ justify-content: space-between;257+ gap: 24px;258+ padding: 16px 20px;259+ border-bottom: 1px solid var(--line);260+}261+.authoring-head p { margin-bottom: 0; }262+.authoring-flow { display: flex; align-items: center; margin: 0; padding: 0; list-style: none; counter-reset: flow; }263+.authoring-flow li { display: flex; align-items: center; color: var(--muted); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }264+.authoring-flow li::before { counter-increment: flow; content: counter(flow); display: grid; place-items: center; width: 20px; height: 20px; margin-right: 5px; border: 1px solid var(--line-strong); border-radius: 50%; color: var(--muted); }265+.authoring-flow li::after { content: "→"; margin: 0 8px; color: var(--line-strong); font-size: 14px; }266+.authoring-flow li:last-child::after { display: none; }267+.authoring-flow li.active, .authoring-flow li.active::before { color: var(--data); border-color: var(--accent); }268+.area-form { display: grid; grid-template-columns: minmax(360px, 1.15fr) minmax(330px, 1fr) minmax(180px, .45fr); align-items: stretch; gap: 12px; margin: 0; padding: 16px 20px; }269+.area-form fieldset { min-width: 0; margin: 0; padding: 10px 12px 12px; border: 1px solid var(--line); }270+.area-form legend { padding: 0 6px; color: var(--text-strong); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }271+.area-bounds { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }272+.area-bounds label { display: grid; gap: 3px; }273+.area-bounds input { width: 100%; min-width: 0; font-variant-numeric: tabular-nums; }274+.area-map-toolbar { grid-column: 1 / -1; display: flex; align-items: end; justify-content: space-between; gap: 12px; color: var(--muted); font-size: 10px; }275+.area-map-toolbar label { display: flex; align-items: center; gap: 6px; }276+.area-map-toolbar select { min-width: 110px; padding: 3px 6px; font-size: 11px; }277+.area-map { grid-column: 1 / -1; width: 100%; height: 150px; border: 1px solid var(--line-strong); border-radius: 4px; background: var(--map-surround); cursor: crosshair; touch-action: none; }278+.area-map:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }279+.detail-control { grid-column: 1 / -1; grid-template-columns: 1fr auto ; align-items: center; }280+.detail-control input { grid-column: 1 / -1; padding: 4px 0; accent-color: var(--accent); }281+.detail-control output { color: var(--data); font-size: 14px; font-weight: 600; }282+.source-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }283+.source-choice { display: flex; align-items: center; gap: 8px; padding: 8px; border: 1px solid var(--line); border-radius: 4px; background: var(--panel-raised); cursor: pointer; }284+.source-choice:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }285+.source-choice input { width: auto; min-width: 0; margin: 0; accent-color: var(--accent); }286+.source-choice span, .source-choice small { display: block; }287+.source-choice b { color: var(--text-strong); font-size: 12px; }288+.source-choice small { color: var(--muted); font-size: 10px; }289+.budget-preview { display: flex; flex-direction: column; justify-content: center; gap: 4px; padding: 12px; border: 1px solid var(--line-strong); background: var(--bg); }290+.budget-preview b { color: var(--data); font-size: 16px; }291+.budget-preview span:not(.node-kind) { color: var(--muted); font-size: 11px; }292+.budget-preview.over { border-color: #d98a72; }293+.budget-preview.over b, .budget-preview.over [data-budget-state] { color: #d98a72; }294+.presentation-editor { grid-column: 1 / -1; display: grid; gap: 6px; }295+.presentation-head, .presentation-row { display: grid; grid-template-columns: minmax(160px, .8fr) minmax(180px, 1fr) minmax(110px, .5fr) 80px; align-items: center; gap: 8px; }296+.presentation-head { padding: 0 8px; color: var(--muted); font-size: 9px; letter-spacing: .08em; text-transform: uppercase; }297+.presentation-row { padding: 7px 8px; border: 1px solid var(--line); border-radius: 4px; background: var(--panel-raised); }298+.presentation-row > span:first-child { display: flex; align-items: center; gap: 6px; color: var(--text); }299+.presentation-row input { width: 100%; min-width: 0; }300+.presentation-row input[type="checkbox"] { width: auto; }301+.presentation-row input[type="color"] { width: 42px; height: 30px; padding: 2px; }302+.color-pair { display: flex; gap: 4px; }303+.generate-manifest { grid-column: 1 / -1; justify-self: end; padding: 8px 14px; }304+.contract-preview { display: grid; grid-template-columns: minmax(0, 1fr) 30px minmax(0, .8fr) 30px minmax(180px, .45fr); align-items: center; gap: 8px; padding: 14px 20px; border-top: 1px solid var(--line); background: var(--bg); }305+.contract-preview p { margin: 5px 0 0; }306+.contract-pill { display: inline-block; margin: 0 4px 4px 0; padding: 2px 7px; border: 1px solid var(--line-strong); border-radius: 999px; color: var(--text); font-size: 10px; }307+.contract-pill.path { border-color: var(--accent); }308+.contract-pill.area { border-color: var(--warm); }309+.style-stack { display: flex; align-items: center; gap: 5px; color: var(--text); font-size: 11px; }310+.swatch { display: inline-block; width: 18px; height: 18px; margin-right: -9px; border: 2px solid var(--panel); border-radius: 50%; }311+.swatch.county { background: #766b5b; }312+.swatch.water { background: #4b8db8; }313+.swatch.road { margin-right: 4px; background: #b45f3c; }314+.truth-preview { display: flex; flex-direction: column; gap: 3px; padding: 10px 12px; border: 1px solid var(--accent); background: var(--accent-wash); }315+.truth-preview b { color: var(--data); }316+250317 /* project contract graph ------------------------------------------------ */251318 252319 .contract-graph { padding: 0; overflow: hidden; }@@ -387,6 +454,15 @@ textarea {
387454 388455 @media (max-width: 850px) {389456 .project-columns { grid-template-columns: 1fr; }457+ .authoring-head { align-items: start; flex-direction: column; }458+ .authoring-flow { flex-wrap: wrap; }459+ .area-form { grid-template-columns: 1fr; }460+ .area-bounds { grid-template-columns: 1fr 1fr; }461+ .presentation-head { display: none; }462+ .presentation-row { grid-template-columns: 1fr 1fr; }463+ .generate-manifest { grid-column: 1; }464+ .contract-preview { grid-template-columns: 1fr; }465+ .contract-preview > .graph-arrow { transform: rotate(90deg); }390466 .graph-head { align-items: start; flex-direction: column; gap: 6px; }391467 .graph-totals { text-align: left; }392468 .contract-group { grid-template-columns: 1fr; }@@ -9,3 +9,154 @@ document.addEventListener("dragstart", (event) => {
99 event.dataTransfer.setData("text/uri-list", endpoint);1010 event.dataTransfer.setData("text/plain", handoff.dataset.artifactPath || name);1111 });12+13+const tileX = (longitude, count, upper = false) => {14+ const value = ((longitude + 180) / 360) * count;15+ return upper ? Math.ceil(value) - 1 : Math.floor(value);16+};17+18+const tileY = (latitude, count) => {19+ const radians = latitude * Math.PI / 180;20+ return Math.floor((1 - Math.asinh(Math.tan(radians)) / Math.PI) / 2 * count);21+};22+23+const latitudeAtTile = (tile, count) => Math.atan(Math.sinh(Math.PI * (1 - 2 * tile / count))) * 180 / Math.PI;24+25+const mapRegions = {26+ conus: {27+ extent: [-125, 24, -66, 50], initial: [-105.1, 39.6, -104.9, 39.8], zoom: 12,28+ outline: [[-124, 48], [-124, 42], [-122, 38], [-117, 33], [-111, 32], [-106, 31], [-103, 29], [-97, 26], [-82, 25], [-80, 27], [-81, 31], [-75, 35], [-67, 45], [-75, 45], [-83, 46], [-95, 49], [-112, 49]],29+ },30+ alaska: {31+ extent: [-180, 50, -129, 72], initial: [-151.0, 60.9, -150.7, 61.2], zoom: 11,32+ outline: [[-168, 54], [-160, 55], [-153, 57], [-147, 59], [-141, 60], [-141, 69], [-151, 71], [-162, 68], [-168, 64], [-179, 52]],33+ },34+ hawaii: {35+ extent: [-161, 18, -154, 23], initial: [-157.95, 21.25, -157.7, 21.45], zoom: 12,36+ outline: [[-160.3, 22.1], [-159.2, 21.8], [-158.2, 21.7], [-157.7, 21.3], [-156.3, 20.8], [-155.5, 19.4]],37+ },38+ "puerto-rico": {39+ extent: [-68, 17, -65, 19], initial: [-66.7, 18.1, -66.3, 18.4], zoom: 13,40+ outline: [[-67.3, 18.5], [-65.6, 18.5], [-65.2, 18.2], [-66.0, 17.9], [-67.2, 18.0]],41+ },42+};43+44+const alignedSelection = (form) => {45+ const value = (name) => Number(form.elements.namedItem(name).value);46+ const zoom = value("detail");47+ const count = 2 ** zoom;48+ const minX = tileX(value("west"), count);49+ const maxX = tileX(value("east"), count, true);50+ const minY = tileY(value("north"), count);51+ const maxY = tileY(value("south"), count);52+ let side = 1;53+ while (Math.floor(minX / side) * side + side - 1 < maxX || Math.floor(minY / side) * side + side - 1 < maxY) side *= 2;54+ const originX = Math.floor(minX / side) * side;55+ const originY = Math.floor(minY / side) * side;56+ return {57+ zoom, side, pixels: side * 256, localZoom: Math.log2(side),58+ bounds: [originX / count * 360 - 180, latitudeAtTile(originY + side, count),59+ (originX + side) / count * 360 - 180, latitudeAtTile(originY, count)],60+ };61+};62+63+const mapPoint = (bounds, canvas, longitude, latitude) => [64+ (longitude - bounds[0]) / (bounds[2] - bounds[0]) * canvas.width,65+ (bounds[3] - latitude) / (bounds[3] - bounds[1]) * canvas.height,66+];67+68+const drawBounds = (context, canvas, region, bounds, color, fill) => {69+ const start = mapPoint(region.extent, canvas, bounds[0], bounds[3]);70+ const end = mapPoint(region.extent, canvas, bounds[2], bounds[1]);71+ context.strokeStyle = color;72+ context.fillStyle = fill;73+ context.lineWidth = 3;74+ context.setLineDash(color === "#7fc5de" ? [] : [10, 6]);75+ context.fillRect(start[0], start[1], end[0] - start[0], end[1] - start[1]);76+ context.strokeRect(start[0], start[1], end[0] - start[0], end[1] - start[1]);77+};78+79+const renderAreaMap = (form, aligned) => {80+ const canvas = form.querySelector("[data-area-map]");81+ const region = mapRegions[form.querySelector("[data-map-region]").value];82+ const context = canvas.getContext("2d");83+ context.clearRect(0, 0, canvas.width, canvas.height);84+ context.fillStyle = "#0d1626";85+ context.fillRect(0, 0, canvas.width, canvas.height);86+ context.beginPath();87+ region.outline.forEach(([longitude, latitude], index) => {88+ const point = mapPoint(region.extent, canvas, longitude, latitude);89+ if (index === 0) context.moveTo(point[0], point[1]); else context.lineTo(point[0], point[1]);90+ });91+ if (region.outline.length > 4) context.closePath();92+ context.fillStyle = "rgba(138, 106, 68, .35)";93+ context.strokeStyle = "#8a6a44";94+ context.lineWidth = 2;95+ context.fill();96+ context.stroke();97+ const value = (name) => Number(form.elements.namedItem(name).value);98+ drawBounds(context, canvas, region, aligned.bounds, "#8e897c", "rgba(0,0,0,0)");99+ drawBounds(context, canvas, region, [value("west"), value("south"), value("east"), value("north")], "#7fc5de", "rgba(32, 138, 174, .22)");100+};101+102+const updateAreaBudget = (form) => {103+ const budget = form.querySelector("[data-budget-preview]");104+ const detail = form.querySelector("[data-detail-output]");105+ const selected = alignedSelection(form);106+ const rasterTiles = (4 ** (selected.localZoom + 1) - 1) / 3;107+ const captures = (form.elements.namedItem("topo").checked ? 1 : 0) +108+ (form.elements.namedItem("roads").checked ? 3 : 0) +109+ (form.elements.namedItem("hydro").checked ? 2 : 0) +110+ (form.elements.namedItem("counties").checked ? 1 : 0);111+ detail.value = String(selected.zoom);112+ budget.querySelector("[data-budget-tiles]").value = rasterTiles.toLocaleString();113+ budget.querySelector("[data-budget-size]").value = `${selected.pixels.toLocaleString()} × ${selected.pixels.toLocaleString()}`;114+ const over = selected.pixels > 4096 || captures === 0;115+ budget.classList.toggle("over", over);116+ budget.querySelector("[data-budget-state]").value = captures === 0 ? "Select at least one source." :117+ over ? `${captures} captures · lower detail or narrow the area.` : `${captures} capture requests · within default gate.`;118+ renderAreaMap(form, selected);119+};120+121+const installAreaMap = (form) => {122+ const canvas = form.querySelector("[data-area-map]");123+ const regionPicker = form.querySelector("[data-map-region]");124+ let start = null;125+ const coordinates = (event) => {126+ const rect = canvas.getBoundingClientRect();127+ const x = Math.max(0, Math.min(canvas.width, (event.clientX - rect.left) / rect.width * canvas.width));128+ const y = Math.max(0, Math.min(canvas.height, (event.clientY - rect.top) / rect.height * canvas.height));129+ const region = mapRegions[regionPicker.value];130+ return [region.extent[0] + x / canvas.width * (region.extent[2] - region.extent[0]),131+ region.extent[3] - y / canvas.height * (region.extent[3] - region.extent[1])];132+ };133+ canvas.addEventListener("pointerdown", (event) => {134+ start = coordinates(event);135+ canvas.setPointerCapture(event.pointerId);136+ });137+ canvas.addEventListener("pointermove", (event) => {138+ if (!start) return;139+ const end = coordinates(event);140+ form.elements.namedItem("west").value = Math.min(start[0], end[0]).toFixed(6);141+ form.elements.namedItem("east").value = Math.max(start[0], end[0]).toFixed(6);142+ form.elements.namedItem("south").value = Math.min(start[1], end[1]).toFixed(6);143+ form.elements.namedItem("north").value = Math.max(start[1], end[1]).toFixed(6);144+ updateAreaBudget(form);145+ });146+ canvas.addEventListener("pointerup", (event) => {147+ if (start) canvas.releasePointerCapture(event.pointerId);148+ start = null;149+ });150+ regionPicker.addEventListener("change", () => {151+ const region = mapRegions[regionPicker.value];152+ ["west", "south", "east", "north"].forEach((name, index) => { form.elements.namedItem(name).value = region.initial[index]; });153+ form.elements.namedItem("detail").value = region.zoom;154+ updateAreaBudget(form);155+ });156+};157+158+document.querySelectorAll("[data-us-area-form]").forEach((form) => {159+ installAreaMap(form);160+ updateAreaBudget(form);161+ form.addEventListener("input", () => updateAreaBudget(form));162+});@@ -124,7 +124,9 @@ func TestProjectPageMakesTheNativeBuildGraphVisible(t *testing.T) {
124124 wants(t, "project", body,125125 "Sample Region", "One manifest → one native volume", "sample-features", "geojson",126126 "0 / 1 requests", "feature sources", "semantic sets", "presentation layers",127- "Build native Atlas", "Portable manifest", "Sample Region contributors")127+ "Build native Atlas", "Portable manifest", "Sample Region contributors",128+ "Choose a U.S. area", "USGS Topo", "TIGER roads", "TIGER hydro", "TIGER counties",129+ "Typed FeatureSets", "Compiled Atlas truth")128130 if response, _ := get(t, server, "/operations"); response.StatusCode != http.StatusNotFound {129131 t.Errorf("retired operations route answered %d", response.StatusCode)130132 }@@ -133,6 +135,85 @@ func TestProjectPageMakesTheNativeBuildGraphVisible(t *testing.T) {
133135 }134136 }135137 138+func TestUSAreaAuthoringReplacesTheSingleManifestWithTheSelectedDefaults(t *testing.T) {139+ targets := projectTargets(t)140+ held, err := New(Options{Targets: targets, Runtime: []byte("/* runtime */")})141+ if err != nil {142+ t.Fatal(err)143+ }144+ server := site(t, held)145+ form := usAreaForm()146+ form.Set("road-label", "Routes")147+ form.Set("road-color", "#c05030")148+ form.Set("road-order", "17")149+ form.Del("road-visible")150+ response, body := postForm(t, noRedirectClient(server.Client()), server.URL+"/project/us-area", form)151+ if response.StatusCode != http.StatusSeeOther {152+ t.Fatalf("U.S. area authoring answered %d: %s", response.StatusCode, body)153+ }154+ project, err := authoring.LoadProject(targets.Project)155+ if err != nil {156+ t.Fatalf("generated manifest: %v", err)157+ }158+ if project.Title != "Sample Region" || len(project.Rasters) != 1 || len(project.Sources) != 6 {159+ t.Fatalf("generated project = %+v", project)160+ }161+ var roads authoring.Layer162+ for _, layer := range project.Presentation.Layers {163+ if layer.ID == "roads" {164+ roads = layer165+ }166+ }167+ if roads.Label != "Routes" || roads.Visible || roads.Order != 17 {168+ t.Fatalf("presentation choices were not authored: %+v", roads)169+ }170+ if project.Target.CoordinateSpace.OriginX == 0 || project.Target.CoordinateSpace.OriginY == 0 {171+ t.Fatalf("generated tile origins = %+v", project.Target.CoordinateSpace)172+ }173+ _, page := get(t, server, "/project?notice=Official+U.S.+profile+installed")174+ wants(t, "generated authoring", page, "Official U.S. profile installed", "Capture requests", "Raster tiles", "Compiled Atlas truth")175+}176+177+func TestUSAreaAuthoringRefusesAnInvalidAreaBeforeReplacingTheManifest(t *testing.T) {178+ targets := projectTargets(t)179+ original, err := os.ReadFile(targets.Project)180+ if err != nil {181+ t.Fatal(err)182+ }183+ held, err := New(Options{Targets: targets})184+ if err != nil {185+ t.Fatal(err)186+ }187+ server := site(t, held)188+ form := usAreaForm()189+ form.Set("west", "2")190+ form.Set("south", "48")191+ form.Set("east", "3")192+ form.Set("north", "49")193+ response, body := postForm(t, server.Client(), server.URL+"/project/us-area", form)194+ if response.StatusCode != http.StatusBadRequest || !strings.Contains(body, "United States region") {195+ t.Fatalf("invalid area answered %d: %s", response.StatusCode, body)196+ }197+ after, err := os.ReadFile(targets.Project)198+ if err != nil {199+ t.Fatal(err)200+ }201+ if string(after) != string(original) {202+ t.Fatal("invalid area replaced the manifest")203+ }204+}205+206+func usAreaForm() url.Values {207+ return url.Values{208+ "west": {"-105.1"}, "south": {"39.6"}, "east": {"-104.9"}, "north": {"39.8"},209+ "detail": {"12"}, "topo": {"true"}, "roads": {"true"}, "hydro": {"true"}, "counties": {"true"},210+ "road-label": {"Roads"}, "hydro-label": {"Water"}, "county-label": {"Counties"},211+ "road-color": {"#b45f3c"}, "hydro-color": {"#4b8db8"}, "hydro-fill": {"#b9d9ea"}, "county-color": {"#766b5b"},212+ "road-visible": {"true"}, "hydro-visible": {"true"}, "county-visible": {"true"},213+ "road-order": {"40"}, "hydro-order": {"20"}, "county-order": {"10"},214+ }215+}216+136217 func TestProjectPageShowsTheSourceToContractLaneWithoutOpeningEditors(t *testing.T) {137218 targets := projectTargets(t)138219 held, err := New(Options{Targets: targets, Runtime: []byte("/* runtime */")})@@ -8,6 +8,101 @@
88 {{if .Error}}<p class="warn">{{.Error}}</p>{{end}}99 </section>1010 11+<section class="card area-authoring" aria-labelledby="area-authoring-title">12+ <header class="authoring-head">13+ <div>14+ <p class="eyebrow">Nationwide source profile</p>15+ <h2 id="area-authoring-title">Choose a U.S. area</h2>16+ <p class="dim">One bounded region becomes the single manifest below. The label stays neutral: Sample Region.</p>17+ </div>18+ <ol class="authoring-flow" aria-label="Authoring flow">19+ <li class="active">Area</li><li>Sources</li><li>Contracts</li><li>Build</li><li>Compiled preview</li>20+ </ol>21+ </header>22+23+ <form class="area-form" method="post" action="/project/us-area" data-us-area-form>24+ <fieldset class="area-bounds">25+ <legend>Area · WGS84 bbox</legend>26+ <div class="area-map-toolbar">27+ <label>Region28+ <select data-map-region>29+ <option value="conus">Lower 48</option><option value="alaska">Alaska</option><option value="hawaii">Hawaii</option><option value="puerto-rico">Puerto Rico</option>30+ </select>31+ </label>32+ <span>Drag to draw · aligned source window is outlined</span>33+ </div>34+ <canvas class="area-map" width="720" height="300" tabindex="0" aria-label="Drag to select a rectangular map area" data-area-map></canvas>35+ <label>West<input type="number" name="west" value="-105.1" min="-180" max="180" step="0.000001" required></label>36+ <label>South<input type="number" name="south" value="39.6" min="-90" max="90" step="0.000001" required></label>37+ <label>East<input type="number" name="east" value="-104.9" min="-180" max="180" step="0.000001" required></label>38+ <label>North<input type="number" name="north" value="39.8" min="-90" max="90" step="0.000001" required></label>39+ <label class="detail-control">Detail zoom <output data-detail-output>12</output>40+ <input type="range" name="detail" value="12" min="6" max="16" step="1">41+ </label>42+ </fieldset>43+44+ <fieldset class="source-picker">45+ <legend>Official nationwide sources</legend>46+ <label class="source-choice topo"><input type="checkbox" name="topo" value="true" checked><span><b>USGS Topo</b><small>raster · public domain</small></span></label>47+ <label class="source-choice roads"><input type="checkbox" name="roads" value="true" checked><span><b>TIGER roads</b><small>paths · typed class</small></span></label>48+ <label class="source-choice hydro"><input type="checkbox" name="hydro" value="true" checked><span><b>TIGER hydro</b><small>paths + areas</small></span></label>49+ <label class="source-choice counties"><input type="checkbox" name="counties" value="true" checked><span><b>TIGER counties</b><small>areas · context</small></span></label>50+ </fieldset>51+52+ <aside class="budget-preview" aria-live="polite" data-budget-preview>53+ <span class="node-kind">Selection budget</span>54+ <b><output data-budget-tiles>estimating</output> raster tiles</b>55+ <span><output data-budget-size>—</output> local pixel plane</span>56+ <span><output data-budget-state>Adjust the box or detail to preview.</output></span>57+ </aside>58+59+ <fieldset class="presentation-editor">60+ <legend>Presentation · compiled into layers, styles + legend</legend>61+ <div class="presentation-head"><span>Layer</span><span>Legend label</span><span>Style</span><span>Order</span></div>62+ <label class="presentation-row">63+ <span><input type="checkbox" name="road-visible" value="true" checked> Roads visible</span>64+ <input type="text" name="road-label" value="Roads" maxlength="48" required aria-label="Road legend label">65+ <input type="color" name="road-color" value="#b45f3c" aria-label="Road color">66+ <input type="number" name="road-order" value="40" min="0" max="999" required aria-label="Road layer order">67+ </label>68+ <label class="presentation-row">69+ <span><input type="checkbox" name="hydro-visible" value="true" checked> Water visible</span>70+ <input type="text" name="hydro-label" value="Water" maxlength="48" required aria-label="Water legend label">71+ <span class="color-pair"><input type="color" name="hydro-color" value="#4b8db8" aria-label="Water line color"><input type="color" name="hydro-fill" value="#b9d9ea" aria-label="Water fill color"></span>72+ <input type="number" name="hydro-order" value="20" min="0" max="999" required aria-label="Water layer order">73+ </label>74+ <label class="presentation-row">75+ <span><input type="checkbox" name="county-visible" value="true" checked> Counties visible</span>76+ <input type="text" name="county-label" value="Counties" maxlength="48" required aria-label="County legend label">77+ <input type="color" name="county-color" value="#766b5b" aria-label="County color">78+ <input type="number" name="county-order" value="10" min="0" max="999" required aria-label="County layer order">79+ </label>80+ </fieldset>81+ <button class="generate-manifest">Generate single manifest</button>82+ </form>83+84+ <div class="contract-preview">85+ <div>86+ <span class="node-kind">Typed FeatureSets</span>87+ <p><span class="contract-pill path">roads · path</span><span class="contract-pill path">waterways · path</span><span class="contract-pill area">water bodies · area</span><span class="contract-pill area">counties · area</span></p>88+ <p class="dim">Stable identities, typed properties, EPSG:3857 → local affine geometry, provenance on every source.</p>89+ </div>90+ <i class="graph-arrow" aria-hidden="true">→</i>91+ <div>92+ <span class="node-kind">Presentation</span>93+ <p class="style-stack"><i class="swatch county"></i><i class="swatch water"></i><i class="swatch road"></i> counties · water · roads over topo</p>94+ <p class="dim">Layers, styles, labels and legend compile into the artifact.</p>95+ </div>96+ <i class="graph-arrow" aria-hidden="true">→</i>97+ <div class="truth-preview">98+ <span class="node-kind">Compiled Atlas truth</span>99+ <b>{{if and .Run.Artifact (not .Run.Failed)}}Ready to inspect{{else}}Available after build{{end}}</b>100+ <span class="dim">Preview never substitutes browser-side source data.</span>101+ {{if and .Run.Artifact (not .Run.Failed)}}<a href="/project/inspect">Inspect compiled artifact</a>{{end}}102+ </div>103+ </div>104+</section>105+11106 {{if .Project.ID}}12107 <section class="card contract-graph" aria-labelledby="contract-graph-title">13108 <header class="graph-head">@@ -93,6 +188,8 @@
93188 <div><dt>project digest</dt><dd><code>{{short .Plan.ProjectDigest}}</code></dd></div>94189 <div><dt>world</dt><dd>{{.Plan.World}}</dd></div>95190 <div><dt>estimated input</dt><dd>{{.Plan.Estimated}} bytes</dd></div>191+ <div><dt>Capture requests</dt><dd>{{.RequestCount}}</dd></div>192+ <div><dt>Raster tiles</dt><dd>{{.Plan.RasterTiles}}</dd></div>96193 <div><dt>output library</dt><dd>{{.Plan.Output}}</dd></div>97194 </dl>98195 <details class="plan-detail">@@ -0,0 +1,103 @@
1+package workbench2+3+import (4+ "fmt"5+ "net/http"6+ "strconv"7+8+ "github.com/FelineStateMachine/atlas/internal/authoring"9+ "github.com/FelineStateMachine/atlas/internal/workbench/oprunner"10+)11+12+const maxUSAreaFormBytes = 32 << 1013+14+func (w *Workbench) handleProjectUSArea(rw http.ResponseWriter, request *http.Request) {15+ if err := oprunner.CheckOrigin(request); err != nil {16+ http.Error(rw, err.Error(), http.StatusForbidden)17+ return18+ }19+ request.Body = http.MaxBytesReader(rw, request.Body, maxUSAreaFormBytes)20+ if err := request.ParseForm(); err != nil {21+ http.Error(rw, err.Error(), http.StatusBadRequest)22+ return23+ }24+ profile, err := usAreaProfileFromForm(request)25+ if err != nil {26+ http.Error(rw, err.Error(), http.StatusBadRequest)27+ return28+ }29+ project, _, err := authoring.NewUSAreaProject(profile)30+ if err != nil {31+ http.Error(rw, err.Error(), http.StatusBadRequest)32+ return33+ }34+ manifest, err := authoring.MarshalProjectYAML(project)35+ if err != nil {36+ http.Error(rw, err.Error(), http.StatusBadRequest)37+ return38+ }39+ if err := validateAndInstallManifest(w.targets.Project, manifest); err != nil {40+ http.Error(rw, err.Error(), http.StatusBadRequest)41+ return42+ }43+ http.Redirect(rw, request, "/project?notice=Official+U.S.+profile+installed", http.StatusSeeOther)44+}45+46+func usAreaProfileFromForm(request *http.Request) (authoring.USAreaProfile, error) {47+ profile := authoring.DefaultUSAreaProfile()48+ fields := []struct {49+ name string50+ at int51+ }{52+ {name: "west", at: 0}, {name: "south", at: 1}, {name: "east", at: 2}, {name: "north", at: 3},53+ }54+ for _, field := range fields {55+ value, err := strconv.ParseFloat(request.FormValue(field.name), 64)56+ if err != nil {57+ return authoring.USAreaProfile{}, fmt.Errorf("%s coordinate is invalid", field.name)58+ }59+ profile.Bounds[field.at] = value60+ }61+ detail, err := strconv.Atoi(request.FormValue("detail"))62+ if err != nil {63+ return authoring.USAreaProfile{}, fmt.Errorf("detail zoom is invalid")64+ }65+ profile.DetailZoom = detail66+ profile.IncludeTopo = request.FormValue("topo") == "true"67+ profile.IncludeRoads = request.FormValue("roads") == "true"68+ profile.IncludeHydro = request.FormValue("hydro") == "true"69+ profile.IncludeCounties = request.FormValue("counties") == "true"70+ if err := presentationFromForm(request, &profile.Presentation); err != nil {71+ return authoring.USAreaProfile{}, err72+ }73+ return profile, nil74+}75+76+func presentationFromForm(request *http.Request, presentation *authoring.USAreaPresentation) error {77+ presentation.RoadLabel = request.FormValue("road-label")78+ presentation.HydroLabel = request.FormValue("hydro-label")79+ presentation.CountyLabel = request.FormValue("county-label")80+ presentation.RoadColor = request.FormValue("road-color")81+ presentation.HydroColor = request.FormValue("hydro-color")82+ presentation.HydroFill = request.FormValue("hydro-fill")83+ presentation.CountyColor = request.FormValue("county-color")84+ presentation.RoadVisible = request.FormValue("road-visible") == "true"85+ presentation.HydroVisible = request.FormValue("hydro-visible") == "true"86+ presentation.CountyVisible = request.FormValue("county-visible") == "true"87+ orders := []struct {88+ name string89+ to *int6490+ }{91+ {name: "road-order", to: &presentation.RoadOrder},92+ {name: "hydro-order", to: &presentation.HydroOrder},93+ {name: "county-order", to: &presentation.CountyOrder},94+ }95+ for _, order := range orders {96+ value, err := strconv.ParseInt(request.FormValue(order.name), 10, 64)97+ if err != nil {98+ return fmt.Errorf("%s is invalid", order.name)99+ }100+ *order.to = value101+ }102+ return nil103+}@@ -137,6 +137,7 @@ func (w *Workbench) routes() {
137137 w.mux.HandleFunc("GET /volume/{slug}", w.handleVolume)138138 w.mux.HandleFunc("GET /volume/{slug}/diff", w.handleDiff)139139 w.mux.HandleFunc("GET /project", w.handleProject)140+ w.mux.HandleFunc("POST /project/us-area", w.handleProjectUSArea)140141 w.mux.HandleFunc("POST /project/save", w.handleProjectSave)141142 w.mux.HandleFunc("POST /project/build", w.handleProjectBuild)142143 w.mux.HandleFunc("GET /project/run", w.handleProjectRun)