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 / format / vnext / validate.go
validate.go 7.6K
1package vnext23import (4 "fmt"5 "math"6 "strings"7)89type identities map[string]string1011func (ids identities) add(id, kind string) error {12 if id == "" {13 return fmt.Errorf("%s has no ID", kind)14 }15 key := kind + "\x00" + id16 if previous, held := ids[key]; held {17 return fmt.Errorf("ID %q is shared by %s and %s", id, previous, kind)18 }19 ids[key] = kind20 return nil21}2223func validateVolume(volume Volume) error {24 if volume.ID == "" || volume.Title == "" || len(volume.Worlds) == 0 {25 return fmt.Errorf("volume requires an ID, title, and at least one world")26 }27 ids := make(identities)28 if err := ids.add(volume.ID, "volume"); err != nil {29 return err30 }31 for _, world := range volume.Worlds {32 if err := validateWorld(world, ids); err != nil {33 return err34 }35 }36 for _, asset := range volume.Assets {37 if err := ids.add(asset.ID, "asset"); err != nil {38 return err39 }40 if asset.MediaType == "" {41 return fmt.Errorf("asset %s has no media type", asset.ID)42 }43 }44 return nil45}4647func validateWorld(world World, ids identities) error {48 if err := ids.add(world.ID, "world"); err != nil {49 return err50 }51 if world.Title == "" {52 return fmt.Errorf("world %s has no title", world.ID)53 }54 if err := ids.add(world.CoordinateSpace.ID, "coordinate space"); err != nil {55 return err56 }57 if world.CoordinateSpace.Kind == "" || world.CoordinateSpace.Unit == "" || world.CoordinateSpace.Definition == "" {58 return fmt.Errorf("world %s has an incomplete coordinate space", world.ID)59 }60 if world.CoordinateSpace.SourceZoom < 0 || world.CoordinateSpace.TileSize < 0 || world.CoordinateSpace.Size < 0 ||61 world.CoordinateSpace.OriginX < 0 || world.CoordinateSpace.OriginY < 0 {62 return fmt.Errorf("world %s has invalid coordinate grid metadata", world.ID)63 }64 if err := validateClaims(world.Claims, "world"); err != nil {65 return err66 }67 sets := make(map[string]bool, len(world.FeatureSets))68 for _, set := range world.FeatureSets {69 if err := validateFeatureSet(set, ids); err != nil {70 return err71 }72 sets[set.ID] = true73 }74 for _, raster := range world.RasterPyramids {75 if err := ids.add(raster.ID, "raster pyramid"); err != nil {76 return err77 }78 if raster.Name == "" || raster.Codec == "" || raster.Template == "" || raster.TileSize <= 0 || raster.MinZoom < 0 || raster.MaxZoom < raster.MinZoom || raster.FullZoom < raster.MinZoom || raster.SourceZoom < 0 {79 return fmt.Errorf("raster pyramid %s is incomplete", raster.ID)80 }81 if len(raster.Formats) != int(raster.MaxZoom-raster.MinZoom+1) {82 return fmt.Errorf("raster pyramid %s needs one format per zoom", raster.ID)83 }84 if err := offlineString(raster.Template, "raster template"); err != nil {85 return err86 }87 }88 return validatePresentation(world.Presentation, sets, ids)89}9091func validateFeatureSet(set FeatureSet, ids identities) error {92 if err := ids.add(set.ID, "feature set"); err != nil {93 return err94 }95 if set.SemanticType == "" {96 return fmt.Errorf("feature set %s has no semantic type", set.ID)97 }98 if err := validateClaims(set.Claims, "feature set"); err != nil {99 return err100 }101 fields := make(map[ID]bool, len(set.Properties))102 for _, field := range set.Properties {103 if field.ID.isZero() || field.Name == "" || !field.Kind.valid() || fields[field.ID] {104 return fmt.Errorf("feature set %s has an invalid property contract", set.ID)105 }106 fields[field.ID] = true107 }108 for _, feature := range set.Features {109 if err := ids.add(feature.ID, "feature"); err != nil {110 return err111 }112 if feature.Title == "" {113 return fmt.Errorf("feature %s has no title", feature.ID)114 }115 if _, err := encodeGeometry(feature.Geometry); err != nil {116 return fmt.Errorf("feature %s: %w", feature.ID, err)117 }118 seen := make(map[ID]bool, len(feature.Properties))119 for _, property := range feature.Properties {120 propertyID := property.fieldID()121 field, held := fieldByID(set.Properties, propertyID)122 if !held || seen[propertyID] || property.Value.Kind != field.Kind {123 return fmt.Errorf("feature %s violates its property contract", feature.ID)124 }125 seen[propertyID] = true126 if property.Value.Kind == KindString {127 if err := offlineString(property.Value.String, "feature property"); err != nil {128 return err129 }130 }131 }132 for _, field := range set.Properties {133 if !field.Optional && !seen[field.ID] {134 return fmt.Errorf("feature %s omits required property %s", feature.ID, field.ID)135 }136 }137 for _, relationship := range feature.Relationships {138 if err := offlineString(relationship.Target, "relationship target"); err != nil {139 return err140 }141 }142 }143 return nil144}145146func validateClaims(claims []Property, owner string) error {147 seen := make(map[ID]bool, len(claims))148 for _, claim := range claims {149 id := claim.fieldID()150 if id.isZero() || claim.Field.ID != id || claim.Field.Name == "" || !claim.Field.Kind.valid() || claim.Value.Kind != claim.Field.Kind || seen[id] {151 return fmt.Errorf("%s has an invalid typed claim", owner)152 }153 seen[id] = true154 if claim.Value.Kind == KindString {155 if err := offlineString(claim.Value.String, owner+" claim"); err != nil {156 return err157 }158 }159 }160 return nil161}162163func validateGeometry(geometry Geometry) error {164 if geometry.Kind < GeometryPoint || geometry.Kind > GeometryPolygon || len(geometry.Parts) == 0 {165 return fmt.Errorf("geometry has an invalid kind or no parts")166 }167 if geometry.Kind == GeometryPoint && len(geometry.Parts) != 1 {168 return fmt.Errorf("point geometry must contain exactly one part")169 }170 for _, part := range geometry.Parts {171 if len(part.Rings) == 0 || (geometry.Kind != GeometryPolygon && len(part.Rings) != 1) {172 return fmt.Errorf("geometry part has an invalid ring count")173 }174 for _, ring := range part.Rings {175 minimum := 1176 if geometry.Kind == GeometryLineString {177 minimum = 2178 } else if geometry.Kind == GeometryPolygon {179 minimum = 4180 }181 if len(ring) < minimum {182 return fmt.Errorf("geometry ring has too few positions")183 }184 for _, position := range ring {185 if math.IsNaN(position[0]) || math.IsNaN(position[1]) || math.IsInf(position[0], 0) || math.IsInf(position[1], 0) {186 return fmt.Errorf("geometry contains a non-finite position")187 }188 }189 }190 }191 if geometry.Kind == GeometryPoint && len(geometry.Parts[0].Rings[0]) != 1 {192 return fmt.Errorf("point geometry must contain exactly one position")193 }194 return nil195}196197func validatePresentation(presentation Presentation, sets map[string]bool, ids identities) error {198 if err := ids.add(presentation.ID, "presentation"); err != nil {199 return err200 }201 if presentation.Title == "" {202 return fmt.Errorf("presentation %s has no title", presentation.ID)203 }204 styles := make(map[string]bool, len(presentation.Styles))205 for _, style := range presentation.Styles {206 if err := ids.add(style.ID, "style"); err != nil {207 return err208 }209 styles[style.ID] = true210 }211 layers := make(map[string]bool, len(presentation.Layers))212 for _, layer := range presentation.Layers {213 if err := ids.add(layer.ID, "layer"); err != nil {214 return err215 }216 if !sets[layer.FeatureSet] || !styles[layer.Style] || layer.MaxZoom < layer.MinZoom {217 return fmt.Errorf("layer %s has an invalid feature set, style, or zoom range", layer.ID)218 }219 layers[layer.ID] = true220 }221 for _, entry := range presentation.Legend {222 if !layers[entry.Layer] || entry.Label == "" {223 return fmt.Errorf("legend has an invalid layer or label")224 }225 }226 return nil227}228229func fieldByID(fields []Field, id ID) (Field, bool) {230 for _, field := range fields {231 if field.ID == id {232 return field, true233 }234 }235 return Field{}, false236}237238func offlineString(value, context string) error {239 if strings.Contains(value, "http://") || strings.Contains(value, "https://") {240 return fmt.Errorf("%s carries a runtime URL", context)241 }242 return nil243}