1package authoring23import (4 "crypto/sha256"5 "encoding/hex"6 "fmt"7 "sort"89 "github.com/FelineStateMachine/atlas/format/semconv"10 "github.com/FelineStateMachine/atlas/format/vnext"11)1213type assembledFeature struct {14 feature vnext.Feature15 values map[string]vnext.Property16 relationships map[string]bool17}1819func assemble(project Project, observations []observation) (vnext.Volume, error) {20 space := project.Target.CoordinateSpace21 world := vnext.World{22 ID: project.Target.World, Title: project.Target.Title,23 CoordinateSpace: vnext.CoordinateSpace{24 ID: space.ID, Kind: space.Kind, Unit: space.Unit, Definition: space.Definition,25 Extent: space.Extent, SourceZoom: space.SourceZoom,26 OriginX: space.OriginX, OriginY: space.OriginY, FirstTile: space.FirstTile,27 TileSize: space.TileSize, Size: space.Size,28 },29 Presentation: vnext.Presentation{ID: project.Presentation.ID, Title: project.Presentation.Title},30 }31 sort.SliceStable(observations, func(i, j int) bool {32 if observations[i].FeatureSet != observations[j].FeatureSet {33 return observations[i].FeatureSet < observations[j].FeatureSet34 }35 if observations[i].NativeID != observations[j].NativeID {36 return observations[i].NativeID < observations[j].NativeID37 }38 return observations[i].SourceOrder < observations[j].SourceOrder39 })40 sets := make(map[string]int)41 contracts := make(map[string]FeatureSetContract)42 fields := make(map[string]map[string]vnext.Field)43 features := make(map[string]map[string]*assembledFeature)44 for _, contract := range project.FeatureSets {45 set := vnext.FeatureSet{46 ID: setID(world.ID, contract.ID), Title: contract.Title, SemanticType: contract.SemanticType,47 Claims: []vnext.Property{geometryClaim(contract.Geometry)},48 }49 fields[contract.ID] = make(map[string]vnext.Field, len(contract.Properties))50 for _, property := range contract.Properties {51 kind, err := kindOf(property.Type)52 if err != nil {53 return vnext.Volume{}, fmt.Errorf("feature set %s property %s: %w", contract.ID, property.ID, err)54 }55 field := vnext.Field{56 ID: vnext.IDFromName(project.SchemaNamespace, "feature."+contract.ID+"."+property.ID),57 Name: property.Name, Kind: kind, Optional: property.Optional,58 }59 set.Properties = append(set.Properties, field)60 fields[contract.ID][property.ID] = field61 }62 sort.Slice(set.Properties, func(i, j int) bool { return set.Properties[i].ID.String() < set.Properties[j].ID.String() })63 world.FeatureSets = append(world.FeatureSets, set)64 sets[contract.ID] = len(world.FeatureSets) - 165 contracts[contract.ID] = contract66 features[contract.ID] = make(map[string]*assembledFeature)67 }68 for _, item := range observations {69 _, exists := sets[item.FeatureSet]70 if !exists {71 return vnext.Volume{}, fmt.Errorf("observation targets undeclared feature set %s", item.FeatureSet)72 }73 contract := contracts[item.FeatureSet]74 if familyOf(item.Geometry.Kind) != contract.Geometry {75 return vnext.Volume{}, fmt.Errorf("feature set %s has incompatible observations", item.FeatureSet)76 }77 held := features[item.FeatureSet][item.NativeID]78 if held == nil {79 held = &assembledFeature{80 feature: vnext.Feature{81 ID: featureID(world.ID, item.FeatureSet, item.NativeID), Title: item.Title,82 Geometry: item.Geometry,83 },84 values: make(map[string]vnext.Property), relationships: make(map[string]bool),85 }86 features[item.FeatureSet][item.NativeID] = held87 }88 held.feature.Provenance = appendProvenance(held.feature.Provenance, vnext.Provenance{89 Source: item.Source, NativeID: item.NativeID, CapturedAt: item.Evidence.CapturedAt,90 })91 for key, value := range item.Values {92 if _, exists := held.values[key]; !exists {93 field, exists := fields[item.FeatureSet][key]94 if !exists {95 return vnext.Volume{}, fmt.Errorf("feature set %s has no property %s", item.FeatureSet, key)96 }97 held.values[key] = vnext.Property{FieldID: field.ID, Field: field, Value: value}98 }99 }100 for _, relation := range item.Relations {101 held.feature.Relationships = appendRelationship(held.feature.Relationships, vnext.Relationship{102 Predicate: relation.Predicate,103 Target: featureID(world.ID, relation.FeatureSet, relation.NativeID),104 })105 held.relationships[relation.Contract] = true106 }107 }108 for _, setName := range sortedSetNames(sets) {109 set := &world.FeatureSets[sets[setName]]110 contract := contracts[setName]111 for _, nativeID := range sortedFeatureNames(features[setName]) {112 held := features[setName][nativeID]113 for _, property := range contract.Properties {114 if _, exists := held.values[property.ID]; !property.Optional && !exists {115 return vnext.Volume{}, fmt.Errorf("feature %s omits required property %s", held.feature.ID, property.ID)116 }117 }118 for _, relation := range contract.Relationships {119 if !relation.Optional && !held.relationships[relation.ID] {120 return vnext.Volume{}, fmt.Errorf("feature %s omits required relationship %s", held.feature.ID, relation.ID)121 }122 }123 for _, key := range sortedPropertyNames(held.values) {124 held.feature.Properties = append(held.feature.Properties, held.values[key])125 }126 sort.Slice(held.feature.Provenance, func(i, j int) bool {127 if held.feature.Provenance[i].Source != held.feature.Provenance[j].Source {128 return held.feature.Provenance[i].Source < held.feature.Provenance[j].Source129 }130 return held.feature.Provenance[i].CapturedAt < held.feature.Provenance[j].CapturedAt131 })132 sort.Slice(held.feature.Relationships, func(i, j int) bool {133 if held.feature.Relationships[i].Predicate != held.feature.Relationships[j].Predicate {134 return held.feature.Relationships[i].Predicate < held.feature.Relationships[j].Predicate135 }136 return held.feature.Relationships[i].Target < held.feature.Relationships[j].Target137 })138 set.Features = append(set.Features, held.feature)139 }140 }141 sort.Slice(world.FeatureSets, func(i, j int) bool { return world.FeatureSets[i].ID < world.FeatureSets[j].ID })142 for _, style := range project.Presentation.Styles {143 world.Presentation.Styles = append(world.Presentation.Styles, vnext.Style{144 ID: style.ID, Symbol: style.Symbol, Icon: style.Icon, RenderAs: style.RenderAs,145 IconAsset: style.IconAsset, IconPicture: style.IconPicture, Stroke: style.Stroke, Fill: style.Fill,146 })147 }148 for _, layer := range project.Presentation.Layers {149 maxZoom := layer.MaxZoom150 if maxZoom == 0 {151 maxZoom = 32152 }153 world.Presentation.Layers = append(world.Presentation.Layers, vnext.Layer{154 ID: layer.ID, FeatureSet: setID(world.ID, layer.FeatureSet), Style: layer.Style,155 Group: layer.Group, LabelPolicy: layer.LabelPolicy, Visible: layer.Visible,156 Order: layer.Order, MinZoom: layer.MinZoom, MaxZoom: maxZoom,157 })158 world.Presentation.Legend = append(world.Presentation.Legend, vnext.LegendEntry{Layer: layer.ID, Label: layer.Label, Order: layer.Order})159 }160 sort.Slice(world.Presentation.Styles, func(i, j int) bool { return world.Presentation.Styles[i].ID < world.Presentation.Styles[j].ID })161 sort.Slice(world.Presentation.Layers, func(i, j int) bool {162 if world.Presentation.Layers[i].Order != world.Presentation.Layers[j].Order {163 return world.Presentation.Layers[i].Order < world.Presentation.Layers[j].Order164 }165 return world.Presentation.Layers[i].ID < world.Presentation.Layers[j].ID166 })167 sort.Slice(world.Presentation.Legend, func(i, j int) bool {168 if world.Presentation.Legend[i].Order != world.Presentation.Legend[j].Order {169 return world.Presentation.Legend[i].Order < world.Presentation.Legend[j].Order170 }171 return world.Presentation.Legend[i].Layer < world.Presentation.Legend[j].Layer172 })173 if err := relationshipClosure(world); err != nil {174 return vnext.Volume{}, err175 }176 return vnext.Volume{ID: project.ID, Title: project.Title, Worlds: []vnext.World{world}}, nil177}178179func geometryClaim(family string) vnext.Property {180 field := vnext.Field{181 ID: vnext.IDFromName("dev.atlas.attribute.featureSet", semconv.KeyGeometryKind),182 Name: semconv.KeyGeometryKind, Kind: vnext.KindString,183 }184 return vnext.Property{FieldID: field.ID, Field: field, Value: vnext.StringValue(family)}185}186187func appendProvenance(values []vnext.Provenance, item vnext.Provenance) []vnext.Provenance {188 for _, value := range values {189 if value == item {190 return values191 }192 }193 return append(values, item)194}195196func appendRelationship(values []vnext.Relationship, item vnext.Relationship) []vnext.Relationship {197 for _, value := range values {198 if value == item {199 return values200 }201 }202 return append(values, item)203}204205func relationshipClosure(world vnext.World) error {206 ids := make(map[string]bool)207 for _, set := range world.FeatureSets {208 for _, feature := range set.Features {209 ids[feature.ID] = true210 }211 }212 for _, set := range world.FeatureSets {213 for _, feature := range set.Features {214 for _, relation := range feature.Relationships {215 if !ids[relation.Target] {216 return fmt.Errorf("feature %s relationship %s targets missing %s", feature.ID, relation.Predicate, relation.Target)217 }218 }219 }220 }221 return nil222}223224func setID(world, set string) string { return world + "/set/" + set }225226func featureID(world, set, native string) string {227 digest := sha256.Sum256([]byte(native))228 return setID(world, set) + "/feature/" + hex.EncodeToString(digest[:12])229}230231func sortedSetNames(values map[string]int) []string {232 out := make([]string, 0, len(values))233 for key := range values {234 out = append(out, key)235 }236 sort.Strings(out)237 return out238}239240func sortedFeatureNames(values map[string]*assembledFeature) []string {241 out := make([]string, 0, len(values))242 for key := range values {243 out = append(out, key)244 }245 sort.Strings(out)246 return out247}248249func sortedPropertyNames(values map[string]vnext.Property) []string {250 out := make([]string, 0, len(values))251 for key := range values {252 out = append(out, key)253 }254 sort.Strings(out)255 return out256}