1package app23import (4 "encoding/base64"5 "fmt"6 "math"7 "sort"8 "strconv"9 "strings"10 "sync"1112 "github.com/FelineStateMachine/atlas/format/semconv"13 "github.com/FelineStateMachine/atlas/format/vnext"14 "github.com/FelineStateMachine/atlas/internal/app/cells"15 "github.com/FelineStateMachine/atlas/internal/app/hostenv"16)1718type semanticCache struct {19 mu sync.Mutex20 held map[string]vnext.Volume21 order []string22}2324func newSemanticCache() *semanticCache {25 return &semanticCache{held: make(map[string]vnext.Volume)}26}2728func (cache *semanticCache) load(key string, importVolume func() (vnext.Volume, error)) (vnext.Volume, error) {29 cache.mu.Lock()30 defer cache.mu.Unlock()31 if held, ok := cache.held[key]; ok {32 return held, nil33 }34 volume, err := importVolume()35 if err != nil {36 return vnext.Volume{}, err37 }38 cache.order = append(cache.order, key)39 for len(cache.order) > worldsHeld {40 delete(cache.held, cache.order[0])41 cache.order = cache.order[1:]42 }43 cache.held[key] = volume44 return volume, nil45}4647func (a *App) semanticVolume(source hostenv.Volume) (vnext.Volume, error) {48 info := source.Info()49 key := info.Slug + "@" + vnext.ShortStamp(info.Stamp)50 return a.semantic.load(key, func() (vnext.Volume, error) {51 return materializeVolume(source)52 })53}5455func materializeVolume(source hostenv.Volume) (vnext.Volume, error) {56 volume := cloneOutline(source.Outline())57 for worldIndex := range volume.Worlds {58 for setIndex := range volume.Worlds[worldIndex].FeatureSets {59 set := &volume.Worlds[worldIndex].FeatureSets[setIndex]60 for after := ""; ; {61 page, err := source.FeaturePage(vnext.FeaturePageRequest{62 FeatureSet: set.ID, After: after, Limit: 1_000,63 })64 if err != nil {65 return vnext.Volume{}, fmt.Errorf("read feature set %s: %w", set.ID, err)66 }67 set.Features = append(set.Features, page.Features...)68 if page.Next == "" {69 break70 }71 after = page.Next72 }73 }74 }75 return volume, nil76}7778func cloneOutline(source vnext.Volume) vnext.Volume {79 out := source80 out.Worlds = append([]vnext.World(nil), source.Worlds...)81 for worldIndex := range out.Worlds {82 out.Worlds[worldIndex].FeatureSets = append([]vnext.FeatureSet(nil), source.Worlds[worldIndex].FeatureSets...)83 for setIndex := range out.Worlds[worldIndex].FeatureSets {84 out.Worlds[worldIndex].FeatureSets[setIndex].Features = nil85 }86 }87 return out88}8990func semanticWorld(volume vnext.Volume, slug string) (vnext.World, bool) {91 for _, world := range volume.Worlds {92 if world.ID == slug {93 return world, true94 }95 }96 return vnext.World{}, false97}9899// buildVNextWorld is the native presentation boundary. Stable schema IDs,100// typed properties, semantic relationships and coordinate-space geometry flow101// straight into the app model; no v3 JSON, ATLASLOC table or GeoJSON-shaped102// compatibility object is created along the way.103func buildVNextWorld(world vnext.World, assets []vnext.Asset) (*worldModel, error) {104 space := world.CoordinateSpace105 grid := nativeGrid(space)106 model := &worldModel{107 Slug: world.ID, Space: space, Lenses: nativeRasters(world.RasterPyramids), Attrs: propertiesToAttrs(world.Claims),108 Origin: worldOrigin(world), Grid: grid,109 ByID: map[string]*collectionModel{}, PointByID: map[string]*pointModel{}, ShapeByID: map[string]*shapeModel{},110 }111 sets := make(map[string]vnext.FeatureSet, len(world.FeatureSets))112 for _, set := range world.FeatureSets {113 sets[set.ID] = set114 }115 styles := make(map[string]vnext.Style, len(world.Presentation.Styles))116 for _, style := range world.Presentation.Styles {117 styles[style.ID] = style118 }119 assetPaths := make(map[string]string, len(assets))120 for _, asset := range assets {121 assetPaths[asset.ID] = asset.Path122 }123 layers := append([]vnext.Layer(nil), world.Presentation.Layers...)124 sort.SliceStable(layers, func(i, j int) bool { return layers[i].Order < layers[j].Order })125 for index, layer := range layers {126 set, ok := sets[layer.FeatureSet]127 if !ok {128 return nil, fmt.Errorf("layer %s refers to missing feature set %s", layer.ID, layer.FeatureSet)129 }130 style, ok := styles[layer.Style]131 if !ok {132 return nil, fmt.Errorf("layer %s refers to missing style %s", layer.ID, layer.Style)133 }134 kind := featureSetGeometryKind(set)135 attrs := propertiesToAttrs(set.Claims)136 collection := &collectionModel{137 ID: layer.ID, FeatureSet: set.ID, Title: legendLabel(world.Presentation.Legend, layer.ID, set.Title), Kind: kind,138 Group: layer.Group, Icon: style.Icon, IconAsset: assetPaths[style.IconAsset],139 Color: nativeColor(kind, style), Attrs: attrs, Index: index,140 Curated: layer.LabelPolicy, RenderAs: style.RenderAs, Hidden: !layer.Visible,141 }142 if collection.Curated == "" {143 collection.Curated = semconv.LabelPolicy(kind, attrs)144 }145 if collection.RenderAs == "" {146 collection.RenderAs = semconv.RenderAs(attrs, "")147 }148 model.Members = append(model.Members, collection)149 model.ByID[collection.ID] = collection150 for featureIndex := range set.Features {151 feature := &set.Features[featureIndex]152 if err := addNativeFeature(model, collection, feature, grid); err != nil {153 return nil, err154 }155 }156 }157 for _, shape := range model.Shapes {158 shape.Depth = shapeDepth(model, shape, 0)159 }160 return model, nil161}162163func addNativeFeature(164 model *worldModel,165 collection *collectionModel,166 feature *vnext.Feature,167 grid tileGrid,168) error {169 if feature.Geometry.Kind == vnext.GeometryPoint {170 if len(feature.Geometry.Parts) != 1 || len(feature.Geometry.Parts[0].Rings) != 1 || len(feature.Geometry.Parts[0].Rings[0]) != 1 {171 return fmt.Errorf("point %s has an invalid geometry", feature.ID)172 }173 position := feature.Geometry.Parts[0].Rings[0][0]174 pin := &pointModel{175 ID: feature.ID, Title: feature.Title, Coordinates: coordinateLabel(model.Space, grid, position),176 X: position[0], Y: -position[1],177 Shard: feature.Shard, Feature: feature, Collection: collection,178 }179 model.Points = append(model.Points, pin)180 model.PointByID[pin.ID] = pin181 collection.Count++182 return nil183 }184 shape, err := buildNativeShape(feature, collection)185 if err != nil {186 return err187 }188 collection.Shapes = append(collection.Shapes, shape)189 collection.Count++190 model.Shapes = append(model.Shapes, shape)191 model.ShapeByID[shape.ID] = shape192 return nil193}194195// coordinateLabel keeps the detail card in the coordinate space the volume196// declares. Raster-era tile planes retain their familiar latitude/longitude197// reading; a native projected or synthetic space shows its own x/y values and198// unit instead of pretending those values are Web Mercator.199func coordinateLabel(space vnext.CoordinateSpace, grid tileGrid, position vnext.Position) string {200 if tilePlane(space.Definition) && grid.SourceZoom > 0 && grid.TileSize > 0 && grid.Size > 0 {201 lat, lng := grid.unproject(position[0], position[1])202 return strconv.FormatFloat(lat, 'f', 6, 64) + ", " +203 strconv.FormatFloat(lng, 'f', 6, 64)204 }205 label := strconv.FormatFloat(position[0], 'f', 6, 64) + ", " +206 strconv.FormatFloat(position[1], 'f', 6, 64)207 if space.Unit != "" {208 label += " " + space.Unit209 }210 return label211}212213func nativeGrid(space vnext.CoordinateSpace) tileGrid {214 grid := tileGrid{215 SourceZoom: int(space.SourceZoom), OriginX: int(space.OriginX), OriginY: int(space.OriginY),216 FirstTile: int(space.FirstTile), TileSize: int(space.TileSize), Size: int(space.Size),217 }218 if grid.TileSize == 0 {219 grid.TileSize = 256220 }221 if grid.Size == 0 {222 width := space.Extent[2] - space.Extent[0]223 height := space.Extent[3] - space.Extent[1]224 grid.Size = int(math.Ceil(math.Max(width, height)))225 }226 return grid227}228229func tilePlane(definition string) bool {230 return definition == "atlas:tile-plane" || strings.HasPrefix(definition, "atlas:v3/tile-grid")231}232233func buildNativeShape(feature *vnext.Feature, collection *collectionModel) (*shapeModel, error) {234 shape := &shapeModel{235 ID: feature.ID, Title: feature.Title, Subtitle: feature.Subtitle, Shard: feature.Shard,236 HasText: hasNativeText(feature), Attrs: propertiesToAttrs(feature.Properties), Feature: feature, Collection: collection,237 MinX: math.Inf(1), MinY: math.Inf(1), MaxX: math.Inf(-1), MaxY: math.Inf(-1),238 }239 shape.Parent = relationshipTarget(feature.Relationships, "within")240 for _, part := range feature.Geometry.Parts {241 switch feature.Geometry.Kind {242 case vnext.GeometryPolygon:243 polygon := make([][]point, 0, len(part.Rings))244 for _, ring := range part.Rings {245 polygon = append(polygon, shape.nativePoints(ring))246 }247 if len(polygon) > 0 {248 shape.Polygons = append(shape.Polygons, polygon)249 }250 case vnext.GeometryLineString:251 if len(part.Rings) != 1 {252 return nil, fmt.Errorf("line %s has an invalid part", feature.ID)253 }254 if line := shape.nativePoints(part.Rings[0]); len(line) > 1 {255 shape.Lines = append(shape.Lines, line)256 }257 default:258 return nil, fmt.Errorf("feature %s has unsupported geometry kind %d", feature.ID, feature.Geometry.Kind)259 }260 }261 shape.Drawn = len(shape.Polygons) > 0 || len(shape.Lines) > 0262 return shape, nil263}264265func (shape *shapeModel) nativePoints(positions []vnext.Position) []point {266 out := make([]point, 0, len(positions))267 for _, position := range positions {268 x, y := position[0], -position[1]269 out = append(out, point{X: x, Y: y})270 shape.MinX, shape.MaxX = math.Min(shape.MinX, x), math.Max(shape.MaxX, x)271 shape.MinY, shape.MaxY = math.Min(shape.MinY, y), math.Max(shape.MaxY, y)272 }273 return out274}275276func nativeRasters(rasters []vnext.RasterPyramid) []payloadLens {277 out := make([]payloadLens, 0, len(rasters))278 for _, raster := range rasters {279 lens := payloadLens{280 Name: raster.Name, Tiles: raster.Template, MinZoom: int(raster.MinZoom), MaxZoom: int(raster.MaxZoom),281 FullZoom: int(raster.FullZoom), SourceZoom: int(raster.SourceZoom), Formats: append([]string(nil), raster.Formats...),282 Bounds: cellRect(raster.Bounds), Surface: cellRect(raster.Surface), Interpolate: raster.Interpolate,283 Background: raster.Background, Shard: int(raster.Shard),284 }285 if len(raster.Coverage) > 0 {286 lens.Coverage = make(map[string]payloadCoverage, len(raster.Coverage))287 for _, coverage := range raster.Coverage {288 lens.Coverage[strconv.FormatInt(coverage.Zoom, 10)] = payloadCoverage{289 X: int(coverage.X), Y: int(coverage.Y), W: int(coverage.W), H: int(coverage.H),290 Bits: base64.StdEncoding.EncodeToString(coverage.Bits),291 }292 }293 }294 out = append(out, lens)295 }296 return out297}298299func propertiesToAttrs(properties []vnext.Property) map[string]string {300 out := make(map[string]string, len(properties))301 for _, property := range properties {302 if property.Field.Name != "" {303 out[property.Field.Name] = nativeValue(property.Value)304 }305 }306 return out307}308309func nativeValue(value vnext.Value) string {310 switch value.Kind {311 case vnext.KindBool:312 return strconv.FormatBool(value.Bool)313 case vnext.KindInt64:314 return strconv.FormatInt(value.Int64, 10)315 case vnext.KindFloat64:316 return strconv.FormatFloat(value.Float64, 'g', -1, 64)317 case vnext.KindString:318 return value.String319 case vnext.KindBytes:320 return base64.StdEncoding.EncodeToString(value.Bytes)321 case vnext.KindID:322 return value.ID.String()323 default:324 return ""325 }326}327328func featureSetGeometryKind(set vnext.FeatureSet) string {329 for _, claim := range set.Claims {330 if claim.Field.Name == semconv.KeyGeometryKind {331 return claim.Value.String332 }333 }334 for _, feature := range set.Features {335 switch feature.Geometry.Kind {336 case vnext.GeometryPoint:337 return semconv.GeometryPoint338 case vnext.GeometryLineString:339 return semconv.GeometryPath340 case vnext.GeometryPolygon:341 return semconv.GeometryArea342 }343 }344 return semconv.GeometryPoint345}346347func relationshipTarget(relationships []vnext.Relationship, predicate string) string {348 for _, relationship := range relationships {349 if relationship.Predicate == predicate {350 return relationship.Target351 }352 }353 return ""354}355356func hasNativeText(feature *vnext.Feature) bool {357 return feature.Description != "" || len(feature.Properties) > 0 || len(feature.Relationships) > 0358}359360func worldOrigin(world vnext.World) string {361 for _, set := range world.FeatureSets {362 for _, feature := range set.Features {363 if len(feature.Provenance) > 0 {364 return feature.Provenance[0].Source365 }366 }367 }368 return ""369}370371func legendLabel(entries []vnext.LegendEntry, layer, fallback string) string {372 for _, entry := range entries {373 if entry.Layer == layer && entry.Label != "" {374 return entry.Label375 }376 }377 return fallback378}379380func nativeColor(kind string, style vnext.Style) string {381 if kind == semconv.GeometryPath && style.Stroke != "" {382 return style.Stroke383 }384 return style.Fill385}386387func cellRect(rect *vnext.RasterRect) *cells.Rect {388 if rect == nil {389 return nil390 }391 return &cells.Rect{X: rect.X, Y: rect.Y, Width: rect.Width, Height: rect.Height}392}