diff --git a/monitor/map.go b/monitor/map.go index 4404b1d..f50a7e5 100644 --- a/monitor/map.go +++ b/monitor/map.go @@ -1,7 +1,10 @@ package monitor import ( + "fmt" "io/ioutil" + "os" + "path/filepath" "predictor/env" "predictor/log" "predictor/predictions" @@ -11,13 +14,19 @@ import ( geojson "github.com/paulmach/go.geojson" ) +// Interface to overwrite for testing purposes. +var getAllThings = things.Things.Range + +// Interface to overwrite for testing purposes. +var getCurrentPrediction = predictions.GetCurrentPrediction + // Write geojson data that can be used to visualize the predictions. // The geojson file is written to the static directory. func WriteGeoJSONMap() { // Write the geojson to the file. locationFeatureCollection := geojson.NewFeatureCollection() // Locations of traffic lights. laneFeatureCollection := geojson.NewFeatureCollection() // Lanes of traffic lights. - things.Things.Range(func(key, value interface{}) bool { + getAllThings(func(key, value interface{}) bool { thingName := key.(string) thing := value.(things.Thing) @@ -30,7 +39,7 @@ func WriteGeoJSONMap() { lat, lng := coordinate[1], coordinate[0] // Check if there is a prediction for this thing. - prediction, predictionOk := predictions.GetCurrentPrediction(thingName) + prediction, predictionOk := getCurrentPrediction(thingName) // Build the properties. properties := make(map[string]interface{}) if predictionOk { @@ -62,19 +71,33 @@ func WriteGeoJSONMap() { return true }) + // Make sure the directory exists, otherwise create it. + locationsFilePath := fmt.Sprintf("%s/status/predictions-locations.geojson", env.StaticPath) + err := os.MkdirAll(filepath.Dir(locationsFilePath), os.ModePerm) + if err != nil { + log.Error.Println("Error creating dirs for geojson:", err) + return + } locationsGeoJson, err := locationFeatureCollection.MarshalJSON() if err != nil { log.Error.Println("Error marshalling geojson:", err) return } - ioutil.WriteFile(env.StaticPath+"/status/predictions-locations.geojson", locationsGeoJson, 0644) + ioutil.WriteFile(locationsFilePath, locationsGeoJson, 0644) + // Make sure the directory exists, otherwise create it. + lanesFilePath := fmt.Sprintf("%s/status/predictions-lanes.geojson", env.StaticPath) + err = os.MkdirAll(filepath.Dir(lanesFilePath), os.ModePerm) + if err != nil { + log.Error.Println("Error creating dirs for geojson:", err) + return + } lanesGeoJson, err := laneFeatureCollection.MarshalJSON() if err != nil { log.Error.Println("Error marshalling geojson:", err) return } - ioutil.WriteFile(env.StaticPath+"/status/predictions-lanes.geojson", lanesGeoJson, 0644) + ioutil.WriteFile(lanesFilePath, lanesGeoJson, 0644) } func UpdateGeoJSONMapPeriodically() { diff --git a/monitor/map_test.go b/monitor/map_test.go new file mode 100644 index 0000000..899b7c4 --- /dev/null +++ b/monitor/map_test.go @@ -0,0 +1,138 @@ +package monitor + +import ( + "fmt" + "os" + "predictor/env" + "predictor/predictions" + "predictor/things" + "testing" + "time" + + geojson "github.com/paulmach/go.geojson" +) + +func TestWriteGeoJSONMap(t *testing.T) { + laneTopology := things.LocationMultiLineString{ + Type: "MultiLineString", + // Mock values + Coordinates: [][][]float64{ + // Ingress lane + { + { + 0, 0, + }, + { + 1, 0, + }, + }, + // Connection lane + { + { + 1, 0, + }, + { + 2, 0, + }, + }, + // Egress lane + { + { + 2, 0, + }, + { + 3, 0, + }, + }, + }, + } + getAllThings = func(callback func(key, value interface{}) bool) { + callback( + "1337_1", things.Thing{ + Name: "1337_1", + Properties: things.ThingProperties{ + LaneType: "Radfahrer", + }, + Locations: []things.Location{ + { + Location: things.LocationGeoJson{ + Geometry: laneTopology, + }, + }, + }, + }, + ) + } + mockPrediction := predictions.Prediction{ + ThingName: "1337_1", + Now: []byte{1, 1, 1, 1, 1, 3, 3, 3, 3, 3}, + NowQuality: []byte{100, 100, 100, 100, 100, 100, 100, 100, 100, 100}, + Then: []byte{1, 1, 1, 1, 1, 3, 3, 3, 3, 3}, + ThenQuality: []byte{100, 100, 100, 100, 100, 100, 100, 100, 100, 100}, + ReferenceTime: time.Unix(0, 0), + } + getCurrentPrediction = func(thingName string) (predictions.Prediction, bool) { + return mockPrediction, true + } + + tempDir := t.TempDir() + env.StaticPath = tempDir + + locationsGeoJSONFilePath := fmt.Sprintf("%s/status/predictions-locations.geojson", tempDir) + lanesGeoJSONFilePath := fmt.Sprintf("%s/status/predictions-lanes.geojson", tempDir) + + WriteGeoJSONMap() + + locationsData, err := os.ReadFile(locationsGeoJSONFilePath) + if err != nil { + t.Errorf("failed to read locations geojson file") + t.FailNow() + } + lanesData, err := os.ReadFile(lanesGeoJSONFilePath) + if err != nil { + t.Errorf("failed to read lanes geojson file") + t.FailNow() + } + locationsGeoJSON, err := geojson.UnmarshalFeatureCollection(locationsData) + if err != nil { + t.Errorf("failed to unmarshal geojson feature collection") + t.FailNow() + } + lanesGeoJSON, err := geojson.UnmarshalFeatureCollection(lanesData) + if err != nil { + t.Errorf("failed to unmarshal geojson feature collection") + } + if len(locationsGeoJSON.Features) != 1 || len(lanesGeoJSON.Features) != 1 { + t.Errorf("more or less than one geojson feature detected") + t.FailNow() + } + unmarshaledLocationFeature := locationsGeoJSON.Features[0] + unmarshaledLaneFeature := lanesGeoJSON.Features[0] + + type checker func(v interface{}) bool + propertyChecks := map[string]checker{ + "prediction_available": func(v interface{}) bool { + return v.(bool) == true + }, + "prediction_quality": func(v interface{}) bool { + return v.(float64) == 1.0 + }, + "prediction_time_diff": func(v interface{}) bool { + return v.(float64) > 0 + }, + "prediction_sg_id": func(v interface{}) bool { + return v.(string) == "1337_1" + }, + } + + for key, check := range propertyChecks { + v := unmarshaledLocationFeature.Properties[key] + if !check(v) { + t.Errorf("property check failed: %v", v) + } + v = unmarshaledLaneFeature.Properties[key] + if !check(v) { + t.Errorf("property check failed: %v", v) + } + } +} diff --git a/things/location.go b/things/location.go index c127429..a48dfa7 100644 --- a/things/location.go +++ b/things/location.go @@ -2,16 +2,20 @@ package things // A location model from the SensorThings API. type Location struct { - Description string `json:"description"` - EncodingType string `json:"encodingType"` - IotId int `json:"@iot.id"` - Location struct { // GeoJSON - Type string `json:"type"` - Geometry struct { - Type string `json:"type"` // MultiLineString - Coordinates [][][]float64 `json:"coordinates"` - } `json:"geometry"` - Name string `json:"name"` - SelfLink string `json:"@iot.selfLink"` - } `json:"location"` + Description string `json:"description"` + EncodingType string `json:"encodingType"` + IotId int `json:"@iot.id"` + Location LocationGeoJson `json:"location"` +} + +type LocationGeoJson struct { + Type string `json:"type"` + Geometry LocationMultiLineString `json:"geometry"` + Name string `json:"name"` + SelfLink string `json:"@iot.selfLink"` +} + +type LocationMultiLineString struct { + Type string `json:"type"` // MultiLineString + Coordinates [][][]float64 `json:"coordinates"` } diff --git a/things/thing.go b/things/thing.go index 10a5cac..8cbcd11 100644 --- a/things/thing.go +++ b/things/thing.go @@ -4,15 +4,17 @@ import "fmt" // A traffic light thing from the SensorThings API. type Thing struct { - IotId int `json:"@iot.id"` - Name string `json:"name"` - Description string `json:"description"` - Properties struct { - LaneType string `json:"laneType"` - TrafficLightsId string `json:"trafficLightsId"` // This is the crossing. - } `json:"properties"` - Datastreams []Datastream `json:"Datastreams"` - Locations []Location `json:"Locations"` + IotId int `json:"@iot.id"` + Name string `json:"name"` + Description string `json:"description"` + Properties ThingProperties `json:"properties"` + Datastreams []Datastream `json:"Datastreams"` + Locations []Location `json:"Locations"` +} + +type ThingProperties struct { + LaneType string `json:"laneType"` + TrafficLightsId string `json:"trafficLightsId"` // This is the crossing. } // Get the lane of a thing. This is the connection lane of the thing.