clone

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

feat/atlas-next-horizon
atlas / internal / generate / compose / native.go
native.go 11.4K
1package compose23import (4 "encoding/base64"5 "encoding/json"6 "fmt"7 "mime"8 "path/filepath"9 "sort"10 "strconv"11 "strings"1213 "github.com/FelineStateMachine/atlas/format/semconv"14 "github.com/FelineStateMachine/atlas/format/vnext"15 "github.com/FelineStateMachine/atlas/internal/generate/doc"16 "github.com/FelineStateMachine/atlas/internal/generate/tiles"17)1819func nativeVolume(options Options, worlds []composedWorld, icons map[string][]byte) (vnext.Volume, []vnext.Blob, int, string, error) {20 volume := vnext.Volume{ID: options.Document.Volume.Slug, Title: options.Document.Volume.Title}21 pyramids := make(map[string]tiles.Pyramid)22 capturedAt := ""23 for _, source := range worlds {24 world, err := nativeWorld(options, source, pyramids)25 if err != nil {26 return vnext.Volume{}, nil, 0, "", fmt.Errorf("world %s: %w", source.Slug, err)27 }28 volume.Worlds = append(volume.Worlds, world)29 if source.CapturedAt > capturedAt {30 capturedAt = source.CapturedAt31 }32 }33 if capturedAt == "" {34 return vnext.Volume{}, nil, 0, "", fmt.Errorf("no world carries a capture time to version the bundle by")35 }36 iconNames := make([]string, 0, len(icons))37 for name := range icons {38 iconNames = append(iconNames, name)39 }40 sort.Strings(iconNames)41 for _, name := range iconNames {42 mediaType := mime.TypeByExtension(filepath.Ext(name))43 if mediaType == "" {44 mediaType = "application/octet-stream"45 }46 volume.Assets = append(volume.Assets, vnext.Asset{ID: name, MediaType: mediaType, Data: icons[name], Provenance: options.Document.Volume.Slug})47 }48 blobs, tileCount, err := rasterBlobList(options.Tiles, pyramids)49 return volume, blobs, tileCount, capturedAt, err50}5152func nativeWorld(options Options, source composedWorld, pyramids map[string]tiles.Pyramid) (vnext.World, error) {53 grid := source.Grid54 if grid == nil {55 grid = &worldGrid{SourceZoom: options.Curation.Window.SourceZoom, FirstTile: options.Curation.Window.FirstTile}56 }57 world := vnext.World{58 ID: source.Slug, Title: source.Title,59 CoordinateSpace: vnext.CoordinateSpace{60 ID: source.Slug + "/space", Kind: "projected", Unit: "pixel", Definition: "atlas:tile-plane",61 Extent: [4]float64{0, 0, float64(options.Tiles.Size), float64(options.Tiles.Size)},62 SourceZoom: int64(grid.SourceZoom), FirstTile: int64(grid.FirstTile),63 TileSize: int64(options.Tiles.TileSize), Size: int64(options.Tiles.Size),64 },65 Claims: stringProperties("world", source.Attrs, true),66 Presentation: vnext.Presentation{ID: source.Slug + "/presentation", Title: source.Title},67 }68 for index, collection := range source.Collections {69 set, style, layer, err := nativeCollection(options.Document.Source.Name, source, collection, index, surfaceGrid{70 SourceZoom: grid.SourceZoom, FirstTile: grid.FirstTile,71 TileSize: options.Tiles.TileSize, Size: options.Tiles.Size,72 })73 if err != nil {74 return vnext.World{}, err75 }76 world.FeatureSets = append(world.FeatureSets, set)77 world.Presentation.Styles = append(world.Presentation.Styles, style)78 world.Presentation.Layers = append(world.Presentation.Layers, layer)79 world.Presentation.Legend = append(world.Presentation.Legend, vnext.LegendEntry{Layer: layer.ID, Label: collection.Title, Order: int64(index)})80 }81 for index, lens := range source.Lenses {82 pyramid := source.Pyramids[index]83 pyramids[lens.Tiles] = pyramid84 raster, err := nativeRaster(source.Slug, options.Tiles.TileSize, index, lens)85 if err != nil {86 return vnext.World{}, err87 }88 world.RasterPyramids = append(world.RasterPyramids, raster)89 }90 return world, nil91}9293func nativeCollection(provenanceSource string, world composedWorld, source composedCollection, order int, grid surfaceGrid) (vnext.FeatureSet, vnext.Style, vnext.Layer, error) {94 key := source.Key95 if key == "" {96 key = strconv.FormatInt(source.ID, 10)97 }98 setID := world.Slug + "/set/" + key99 set := vnext.FeatureSet{ID: setID, Title: source.Title, SemanticType: "geometry." + source.Kind, Claims: stringProperties("featureSet", source.Attrs, true)}100 contract := make(map[vnext.ID]vnext.Field)101 for _, item := range source.Features {102 feature, err := nativeFeature(provenanceSource, world, item, grid)103 if err != nil {104 return vnext.FeatureSet{}, vnext.Style{}, vnext.Layer{}, fmt.Errorf("feature %d: %w", item.ID, err)105 }106 for _, property := range feature.Properties {107 contract[property.Field.ID] = property.Field108 }109 set.Features = append(set.Features, feature)110 }111 for _, field := range contract {112 set.Properties = append(set.Properties, field)113 }114 sort.Slice(set.Properties, func(i, j int) bool { return set.Properties[i].ID.String() < set.Properties[j].ID.String() })115 styleID := world.Slug + "/style/" + key116 style := vnext.Style{117 ID: styleID, Icon: source.Icon, IconAsset: source.IconAsset, IconPicture: source.IconPicture,118 RenderAs: semconv.RenderAs(source.Attrs, ""), Stroke: source.IconColor, Fill: source.Color,119 }120 layer := vnext.Layer{121 ID: world.Slug + "/layer/" + key, FeatureSet: setID, Style: styleID,122 Group: source.Group, LabelPolicy: semconv.LabelPolicy(source.Kind, source.Attrs),123 Visible: source.Visible, Order: int64(order), MaxZoom: 32,124 }125 return set, style, layer, nil126}127128func nativeFeature(provenanceSource string, world composedWorld, source composedFeature, grid surfaceGrid) (vnext.Feature, error) {129 id := world.Slug + "/feature/" + strconv.FormatInt(source.ID, 10)130 feature := vnext.Feature{131 ID: id, Title: source.Title, Subtitle: source.Subtitle, Description: source.Description,132 Shard: source.Shard, Properties: stringProperties("feature", source.Attrs, false),133 Provenance: []vnext.Provenance{{Source: provenanceSource, NativeID: strconv.FormatInt(source.ID, 10), CapturedAt: world.CapturedAt}},134 }135 if source.At != nil {136 feature.Geometry = vnext.Geometry{Kind: vnext.GeometryPoint, Parts: []vnext.GeometryPart{{Rings: [][]vnext.Position{{projectPosition(source.At.Lng, source.At.Lat, grid)}}}}}137 } else {138 geometry, err := nativeGeometry(source.Geometry, grid)139 if err != nil {140 return vnext.Feature{}, err141 }142 feature.Geometry = geometry143 }144 if source.Center != nil {145 center := projectPosition(source.Center.Lng, source.Center.Lat, grid)146 feature.Center = &center147 }148 if source.Member != 0 {149 feature.Relationships = append(feature.Relationships, vnext.Relationship{Predicate: "within", Target: world.Slug + "/feature/" + strconv.FormatInt(source.Member, 10)})150 }151 if source.Parent != 0 {152 feature.Relationships = append(feature.Relationships, vnext.Relationship{Predicate: "within", Target: world.Slug + "/feature/" + strconv.FormatInt(source.Parent, 10)})153 }154 for _, link := range source.Links {155 feature.Relationships = append(feature.Relationships, vnext.Relationship{Predicate: "references", Target: world.Slug + "/feature/" + strconv.FormatInt(link.Feature, 10)})156 }157 return feature, nil158}159160func nativeGeometry(items []doc.Geometry, grid surfaceGrid) (vnext.Geometry, error) {161 var geometry vnext.Geometry162 for _, item := range items {163 switch item.Type {164 case "Point":165 var point vnext.Position166 if err := json.Unmarshal(item.Coordinates, &point); err != nil {167 return vnext.Geometry{}, err168 }169 geometry.Kind, geometry.Parts = vnext.GeometryPoint, append(geometry.Parts, vnext.GeometryPart{Rings: [][]vnext.Position{{projectPosition(point[0], point[1], grid)}}})170 case "LineString":171 var line []vnext.Position172 if err := json.Unmarshal(item.Coordinates, &line); err != nil {173 return vnext.Geometry{}, err174 }175 geometry.Kind, geometry.Parts = vnext.GeometryLineString, append(geometry.Parts, vnext.GeometryPart{Rings: [][]vnext.Position{projectPositions(line, grid)}})176 case "MultiLineString":177 var lines [][]vnext.Position178 if err := json.Unmarshal(item.Coordinates, &lines); err != nil {179 return vnext.Geometry{}, err180 }181 geometry.Kind = vnext.GeometryLineString182 for _, line := range lines {183 geometry.Parts = append(geometry.Parts, vnext.GeometryPart{Rings: [][]vnext.Position{projectPositions(line, grid)}})184 }185 case "Polygon":186 var rings [][]vnext.Position187 if err := json.Unmarshal(item.Coordinates, &rings); err != nil {188 return vnext.Geometry{}, err189 }190 geometry.Kind, geometry.Parts = vnext.GeometryPolygon, append(geometry.Parts, vnext.GeometryPart{Rings: projectRings(rings, grid)})191 case "MultiPolygon":192 var polygons [][][]vnext.Position193 if err := json.Unmarshal(item.Coordinates, &polygons); err != nil {194 return vnext.Geometry{}, err195 }196 geometry.Kind = vnext.GeometryPolygon197 for _, rings := range polygons {198 geometry.Parts = append(geometry.Parts, vnext.GeometryPart{Rings: projectRings(rings, grid)})199 }200 default:201 return vnext.Geometry{}, fmt.Errorf("unsupported geometry %q", item.Type)202 }203 }204 return geometry, nil205}206207func projectPosition(lng, lat float64, grid surfaceGrid) vnext.Position {208 return vnext.Position{projectX(lng, grid), projectY(lat, grid)}209}210211func projectPositions(positions []vnext.Position, grid surfaceGrid) []vnext.Position {212 out := make([]vnext.Position, len(positions))213 for index, position := range positions {214 out[index] = projectPosition(position[0], position[1], grid)215 }216 return out217}218219func projectRings(rings [][]vnext.Position, grid surfaceGrid) [][]vnext.Position {220 out := make([][]vnext.Position, len(rings))221 for index, ring := range rings {222 out[index] = projectPositions(ring, grid)223 }224 return out225}226227func nativeRaster(world string, tileSize, index int, source lens) (vnext.RasterPyramid, error) {228 raster := vnext.RasterPyramid{229 ID: world + "/raster/" + strconv.Itoa(index), Name: source.Name, Codec: rasterCodec(source.Formats),230 TileSize: int64(tileSize), MinZoom: int64(source.MinZoom), MaxZoom: int64(source.MaxZoom), FullZoom: int64(source.FullZoom), SourceZoom: int64(source.SourceZoom),231 Template: "tiles/" + source.Tiles + "/{z}/{x}/{y}.{format}", Formats: source.Formats,232 Bounds: nativeBox(source.Bounds), Surface: nativeBox(source.Surface), Interpolate: source.Interpolate, Background: source.Background, Shard: source.Shard,233 }234 levels := make([]string, 0, len(source.Coverage))235 for level := range source.Coverage {236 levels = append(levels, level)237 }238 sort.Strings(levels)239 for _, level := range levels {240 zoom, err := strconv.ParseInt(level, 10, 64)241 if err != nil {242 return vnext.RasterPyramid{}, err243 }244 coverage := source.Coverage[level]245 bits, err := base64.StdEncoding.DecodeString(coverage.Bits)246 if err != nil {247 return vnext.RasterPyramid{}, err248 }249 raster.Coverage = append(raster.Coverage, vnext.RasterCoverage{Zoom: zoom, X: int64(coverage.X), Y: int64(coverage.Y), W: int64(coverage.W), H: int64(coverage.H), Bits: bits})250 }251 return raster, nil252}253254func nativeBox(box *tiles.Box) *vnext.RasterRect {255 if box == nil {256 return nil257 }258 return &vnext.RasterRect{X: float64(box.X), Y: float64(box.Y), Width: float64(box.Width), Height: float64(box.Height)}259}260261func rasterCodec(formats []string) string {262 if len(formats) == 0 {263 return "application/octet-stream"264 }265 extension := strings.TrimPrefix(formats[len(formats)-1], ".")266 if extension == "jpg" {267 extension = "jpeg"268 }269 return "image/" + extension270}271272func stringProperties(owner string, attrs map[string]string, optional bool) []vnext.Property {273 keys := make([]string, 0, len(attrs))274 for key := range attrs {275 keys = append(keys, key)276 }277 sort.Strings(keys)278 properties := make([]vnext.Property, 0, len(keys))279 for _, key := range keys {280 field := vnext.Field{ID: vnext.IDFromName("dev.atlas.attribute."+owner, key), Name: key, Kind: vnext.KindString, Optional: optional}281 properties = append(properties, vnext.Property{FieldID: field.ID, Field: field, Value: vnext.StringValue(attrs[key])})282 }283 return properties284}