clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / internal / generate / sources / arcgishub / drawing.go
drawing.go 4.0K
1package arcgishub23import "github.com/FelineStateMachine/atlas/internal/generate/doc"45// The city's picture, stated as shapes.6//7// A municipal open-data hub publishes geometry and no tiles, so the ground a8// city's bundle stands on is drawn from that geometry rather than fetched. What9// this file produces is the drawing the tile deriver rasterizes: every curated10// layer that carries a role, projected onto the world square through the very11// window the pins are projected through, so the raster and the features cannot12// disagree about where the ground is.13//14// It is not the same pass as shapeCollections, and deliberately so. A15// collection is what a reader clicks; a drawing is what the city looks like.16// Every road centreline draws and none of them is a legend row; only named17// trails are legend rows and all trail segments draw; a shape's emphasis varies18// row by row where a collection's features are buckets of many rows.1920// drawShapes builds the drawing from a normalized capture, reading each layer's21// role and emphasis out of the same curation entry the rest of the layer's22// judgement lives in.23//24// The order is the capture's own canonical order -- datasets by slug, rows by25// id -- and not the curation table's, which is the order everything else in26// this reader is emitted in. That is a deliberate exception, for a reason worth27// writing down rather than a mistake worth tidying.28//29// Shapes of one role are unioned rather than painted over one another, so no30// ordering should be visible in the picture. "Should" is the load-bearing word:31// a rasterizer accumulates coverage in floating point and floating-point32// addition is not associative, so an order-blind drawing is a claim about33// arithmetic rather than about geometry. The claim was measured -- reversing34// the whole shape list moves no tile of this city -- and the order is pinned to35// the capture anyway, because pinning it costs nothing and means the pyramid's36// bytes never rest on that measurement holding for the next city.37func drawShapes(raw *capture, curated city) *doc.Drawing {38 roles := make(map[string]*dataset)39 table := curated.datasets()40 for at := range table {41 if table[at].Role != "" {42 roles[table[at].Slug] = &table[at]43 }44 }4546 out := &doc.Drawing{Zoom: raw.Basemap.MaxZoom}47 for _, captured := range raw.Datasets {48 layer, drawn := roles[captured.Slug]49 if !drawn {50 continue51 }52 for _, row := range captured.Features {53 emphasis := 0.054 if layer.Emphasis != nil {55 emphasis = layer.Emphasis(row.Fields)56 }57 switch row.Geometry.Type {58 case geometryRings:59 // Each polygon is its own shape, so the winding convention --60 // first ring ground, the rest holes -- reads one polygon at a61 // time rather than across a whole multipart row.62 for _, polygon := range row.Geometry.Rings {63 shape := doc.Shape{Role: layer.Role, Emphasis: emphasis}64 for _, ring := range polygon {65 shape.Rings = append(shape.Rings, raw.Window.project(ring))66 }67 if len(shape.Rings) > 0 {68 out.Shapes = append(out.Shapes, shape)69 }70 }71 case geometryLines:72 shape := doc.Shape{Role: layer.Role, Emphasis: emphasis}73 for _, line := range row.Geometry.Lines {74 shape.Lines = append(shape.Lines, raw.Window.project(line))75 }76 if len(shape.Lines) > 0 {77 out.Shapes = append(out.Shapes, shape)78 }79 }80 // A point draws nothing: pins are features, not ground.81 }82 }83 if len(out.Shapes) == 0 {84 return nil85 }86 return out87}8889// project lands a path of true coordinates on the world square. A position90// spelled with fewer than two numbers is dropped rather than guessed at; the91// path it was in survives without it, because a hub's occasional malformed92// vertex is not a reason to lose a street.93func (w window) project(path [][]float64) [][2]float64 {94 out := make([][2]float64, 0, len(path))95 for _, position := range path {96 if len(position) < 2 {97 continue98 }99 x, y := w.worldPixel(position[0], position[1])100 out = append(out, [2]float64{x, y})101 }102 return out103}