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") // Create a map of IDs to Nodes nodesMap := make(map[int64]*OsmNode) for _, node := range osmDocument.Nodes { nodesMap[node.Id] = &node } // 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 > 10 { break } log.Printf("Processing %s %s", combi.Company, combi.Ref) returnCoord := make([][]float64, 0) combiRoute := CombiRoute{ Name: combi.Ref, Departure: getCoordinates(&osmDocument.Ways, nodesMap, combi.Members), Return: &returnCoord, } combiLineSlice := combiRoutesMap[combi.Id] combiLineSlice.Routes = append(combiLineSlice.Routes, combiRoute) } writeOutput(combiLineMap, combiRoutesMap) } func getCoordinates(ways *[]OsmWay, nodes map[int64]*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 var previousWayId 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 secondWay := findOsmway((*membersSlice)[1].Ref, ways) coords = findFirstCoord(way, secondWay, nodes) for _, c := range coords { orderedCoordinates = append(orderedCoordinates, c) } // keep track of the last id previousNodeId = coords[coordsLen-1].Id previousWayId = way.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 previousWayId = way.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 previousWayId = way.Id } else { log.Fatalf("Found a way that didn't connect to the previous way. Expected to find node(%d) in way(%d), the previous way id is (%d)", previousNodeId, way.Id, previousWayId) } } // 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 findFirstCoord(firstWay, secondWay *OsmWay, nodes map[int64]*OsmNode) []*OsmNode { // find the common point first := make([]*OsmNode, 0) for _, node := range firstWay.Nds { coordinate := findNode(node.Ref, nodes) first = append(first, coordinate) } firstLast := len(first) - 1 second := make([]*OsmNode, 0) for _, node := range secondWay.Nds { coordinate := findNode(node.Ref, nodes) second = append(second, coordinate) } secondLast := len(second) - 1 if first[firstLast].Id == second[0].Id || first[firstLast].Id == second[secondLast].Id { return first } else if first[0].Id == second[0].Id || first[0].Id == second[secondLast].Id { // reverse the slice newFirst := make([]*OsmNode, len(first)) for i := 0; i < len(first); i += 1 { newFirst[i] = first[firstLast-i] } return newFirst } else { log.Fatalf("Could not find 2 points in common between 2 ways with ids %d & %d", firstWay.Id, secondWay.Id) panic("") } } func findNode(id int64, nodes map[int64]*OsmNode) *OsmNode { node := nodes[id] if node == nil { log.Fatalf("Node with id %s not found", id) } return node } 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 }