package main import ( "encoding/json" "encoding/xml" "fmt" "log" "os" "regexp" "strconv" ) type OsmBounds struct { MinLat float64 `xml:"minlat,attr"` MinLon float64 `xml:"minlon,attr"` MaxLat float64 `xml:"maxlat,attr"` MaxLon float64 `xml:"maxlon,attr"` } type OsmNode struct { Id int64 `xml:"id,attr"` Lat float64 `xml:"lat,attr"` Lon float64 `xml:"lon,attr"` Version int64 `xml:"version,attr"` Timestamp string `xml:"timestamp,attr"` Changeset int64 `xml:"changeset,attr"` Uid int64 `xml:"uid,attr"` User string `xml:"user,attr"` } type OsmMember struct { Type string `xml:"type,attr"` Ref int64 `xml:"ref,attr"` Role string `xml:"role,attr"` } type OsmTag struct { K string `xml:"k,attr"` V string `xml:"v,attr"` } type OsmWay struct { Id int64 `xml:"id,attr"` Version int64 `xml:"version,attr"` Timestamp string `xml:"timestamp,attr"` Changeset int64 `xml:"changeset,attr"` Uid int64 `xml:"uid,attr"` User string `xml:"user,attr"` Nds []OsmNd `xml:"nd"` } type OsmNd struct { Ref int64 `xml:"ref,attr"` } type OsmRelation struct { Id int64 `xml:"id,attr"` Version int32 `xml:"version,attr"` Timestamp string `xml:"timestamp,attr"` Changeset int64 `xml:"changeset,attr"` Uid int64 `xml:"uid,attr"` User string `xml:"user,attr"` Members []OsmMember `xml:"member"` Tags []OsmTag `xml:"tag"` } type OsmDocument struct { Note string `xml:"note"` Bounds OsmBounds `xml:"bounds"` Nodes []OsmNode `xml:"node"` Relations []OsmRelation `xml:"relation"` Ways []OsmWay `xml:"way"` } type CombiData struct { // Number that succeedes the Company name. // E.g.: 7 Id int // Name of the operator of the route. // E.g.: "C7 - AqpMasivo" Company string // Name of the route // E.g.: "Combi B: Villa Santa Rosa -> Terminal Terrestre" Name string // Indentifiable name of the route Ref string Color string Members *[]OsmMember } // Type to be consumed by the client // Represents a single company type CombiLine struct { Id int `json:"id"` Name string `json:"name"` District string `json:"district"` Color string `json:"color"` } type CombiRouteContainer struct { Routes []CombiRoute } type CombiRoute struct { Name string `json:"name"` Departure *[][]float64 `json:"departure"` Return *[][]float64 `json:"return"` } func main() { log.Println("Begin processing") xmlData, err := os.ReadFile("aqp_map.xml") if err != nil { panic(err) } var osmDocument OsmDocument err = xml.Unmarshal(xmlData, &osmDocument) if err != nil { panic(err) } log.Println("XML unmarshal finished") // Get the relation with id 17642638, // that relation hosts the SIT data var sitId int64 = 17642638 var sitRelation *OsmRelation for _, relation := range osmDocument.Relations { if relation.Id == sitId { sitRelation = &relation } } if sitRelation == nil { panic("SIT relation with id 17642638 not found!") } // (naively) get all the submembers sitMembers := make([]OsmRelation, 0) for _, member := range sitRelation.Members { // search the member // TODO: determine if this is a performance bottleneck, // and requires optimization (insertion sort & binary search) for _, relation := range osmDocument.Relations { if relation.Id == member.Ref { sitMembers = append(sitMembers, relation) } } } // transform sitMembers into CombiData combis := make([]CombiData, 0) for _, member := range sitMembers { combis = append(combis, parseCombiData(member)) } // Create a map from string to CombiLine combiLineMap := make(map[string]*CombiLine) // Create a map for CombiRoute, grouped by CombiLine.Id combiRoutesMap := make(map[int]*CombiRouteContainer) // Populate the map for _, combi := range combis { combiLine := combiLineMap[combi.Company] if combiLine == nil { // create the company in the map combiLine := CombiLine{ Id: combi.Id, Name: combi.Company, District: "", Color: combi.Color, } combiLineMap[combi.Company] = &combiLine // create the route container combiRoutesMap[combi.Id] = &CombiRouteContainer{ Routes: make([]CombiRoute, 0), } } } // Convert each CombiData into a CombiRoute and store it for idx, combi := range combis { if idx > 0 { break } log.Printf("Processing %s %s", combi.Company, combi.Ref) returnCoord := make([][]float64, 0) combiRoute := CombiRoute{ Name: combi.Ref, Departure: getCoordinates(&osmDocument.Ways, &osmDocument.Nodes, combi.Members), Return: &returnCoord, } combiLineSlice := combiRoutesMap[combi.Id] combiLineSlice.Routes = append(combiLineSlice.Routes, combiRoute) } writeOutput(combiLineMap, combiRoutesMap) } func getCoordinates(ways *[]OsmWay, nodes *[]OsmNode, membersSlice *[]OsmMember) *[][]float64 { coordinates := make([][]float64, 0) // The coordinate list may be reversed // we shold check that the end of the previous set // matches the beginning of the next set // stores all the osmways in order, // without duplicates orderedCoordinates := make([]*OsmNode, 0) var previousNodeId int64 = -1 // get all coordinates from each for _, member := range *membersSlice { // get the way way := findOsmway(member.Ref, ways) // get all its coordinates coords := make([]*OsmNode, 0) for _, node := range way.Nds { coordinate := findNode(node.Ref, nodes) coords = append(coords, coordinate) } coordsLen := len(coords) if previousNodeId == -1 { // just add all for _, c := range coords { orderedCoordinates = append(orderedCoordinates, c) } // keep track of the last id previousNodeId = coords[coordsLen-1].Id } else if coords[0].Id == previousNodeId { // check if the first coordinate is the same as the previous stored // if so, add them except the first one, // and keep track of the last one for i := 1; i < coordsLen; i += 1 { orderedCoordinates = append(orderedCoordinates, coords[i]) } // keep track of the last id previousNodeId = coords[coordsLen-1].Id } else if coords[coordsLen-1].Id == previousNodeId { // if not, they are reversed // add all except the last for i := coordsLen - 2; i >= 0; i -= 1 { orderedCoordinates = append(orderedCoordinates, coords[i]) } // keep track of the last id previousNodeId = coords[0].Id } else { log.Fatalf("Found a way whose ends didn't connect to the previous way. Expected to find %d in way with id %d", previousNodeId, way.Id) } } // Now compile all the coordinates for _, coordinate := range orderedCoordinates { coordsSlice := make([]float64, 2) coordsSlice[0] = coordinate.Lat coordsSlice[1] = coordinate.Lon coordinates = append(coordinates, coordsSlice) } return &coordinates } func findNode(id int64, nodes *[]OsmNode) *OsmNode { var coordinateNode *OsmNode for _, newNode := range *nodes { if newNode.Id == id { coordinateNode = &newNode } } if coordinateNode == nil { log.Fatalf("Node with id %s not found", id) } return coordinateNode } func findOsmway(id int64, ways *[]OsmWay) *OsmWay { var way *OsmWay for _, currentWay := range *ways { if currentWay.Id == id { way = ¤tWay } } if way == nil { log.Fatalf("Way with id %d not found", id) } return way } func writeOutput(lines map[string]*CombiLine, routes map[int]*CombiRouteContainer) { // Create output folder os.MkdirAll("output", os.ModePerm) // // Write the lines JSON file // // convert the map into an array combiLineSlice := make([]*CombiLine, 0) for _, combiLine := range lines { combiLineSlice = append(combiLineSlice, combiLine) } // print JSON jsonBytes, err := json.Marshal(combiLineSlice) if err != nil { panic(err) } os.WriteFile("output/lines.json", jsonBytes, 0644) // // Write each JSON file for the routes // for lineId, routeContainer := range routes { outFilename := fmt.Sprintf("output/routes_%d.json", lineId) jsonBytes, err := json.Marshal(routeContainer.Routes) if err != nil { panic(nil) } // write err = os.WriteFile(outFilename, jsonBytes, 0644) if err != nil { panic(err) } } log.Print("JSON files written to output/") } func parseCombiData(member OsmRelation) CombiData { var operatorTag *OsmTag var nameTag *OsmTag var refTag *OsmTag var colorTag *OsmTag for _, tag := range member.Tags { if tag.K == "operator" { operatorTag = &tag continue } if tag.K == "name" { nameTag = &tag continue } if tag.K == "ref" { refTag = &tag continue } if tag.K == "colour" { colorTag = &tag continue } } if operatorTag == nil { log.Fatalf("Found a SIT member without an operator tag, with id %d\n", member.Id) } if nameTag == nil { log.Fatalf("Found a SIT member without a name tag, with id %d\n", member.Id) } if refTag == nil { log.Fatalf("Found a SIT member without a ref tag, with id %d\n", member.Id) } if colorTag == nil { log.Fatalf("Found a SIT member without a colour tag, with id %d\n", member.Id) } return CombiData{ Id: parseLineId(operatorTag.V), Company: operatorTag.V, Name: nameTag.V, Ref: refTag.V, Color: colorTag.V, Members: &member.Members, } } // Extracts the id from a line name. // E.g.: "C11 - Cotum" -> 11 func parseLineId(lineName string) int { regex := regexp.MustCompile("C(\\d+) - .+") groups := regex.FindStringSubmatch(lineName) if groups == nil { panic(fmt.Sprintf("Found an invalid line name (doesnt match format): %s", lineName)) } number, err := strconv.Atoi(groups[1]) if err != nil { panic(err) } return number }