From e39dd481579d3dee296abd02dac602bdd31df04d Mon Sep 17 00:00:00 2001 From: Efrem Ropelato Date: Wed, 6 Nov 2024 23:42:33 +0100 Subject: [PATCH 1/2] add NearbyGPSOnPolyline function --- go.mod | 1 - pkg/ctx/context.go | 2 + pkg/functions/address.go | 118 ++++++++ pkg/functions/functions.go | 89 +++++- pkg/functions/functionsDescription.go | 14 +- testfiles/polyline.geojson | 421 ++++++++++++++++++++++++++ 6 files changed, 628 insertions(+), 17 deletions(-) create mode 100644 testfiles/polyline.geojson diff --git a/go.mod b/go.mod index 6ab561e..44d365b 100644 --- a/go.mod +++ b/go.mod @@ -173,7 +173,6 @@ require ( github.com/invopop/jsonschema v0.12.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/paulmach/go.geojson v1.5.0 github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/spf13/afero v1.11.0 // indirect diff --git a/pkg/ctx/context.go b/pkg/ctx/context.go index 1620b0f..7896c95 100644 --- a/pkg/ctx/context.go +++ b/pkg/ctx/context.go @@ -46,6 +46,7 @@ type Context struct { CtxGeoJsonLock sync.RWMutex CtxLastPointLat []float64 CtxLastPointLon []float64 + CtxForward bool LastIndex int CountryIndex int CityIndex int @@ -71,6 +72,7 @@ func init() { CtxGeoJsonLock: sync.RWMutex{}, CtxLastPointLat: []float64{}, CtxLastPointLon: []float64{}, + CtxForward: true, LastIndex: -1, CountryIndex: 232, CityIndex: -1, diff --git a/pkg/functions/address.go b/pkg/functions/address.go index 03c58cd..2160e1e 100644 --- a/pkg/functions/address.go +++ b/pkg/functions/address.go @@ -127,7 +127,125 @@ func NearbyGPS(latitude float64, longitude float64, radius int) string { newLongitude := longitude + (distanceInDegrees * math.Sin(randomAngle)) return fmt.Sprintf("%.4f %.4f", newLatitude, newLongitude) +} + +// NearbyGPSOnPolyline generates a random latitude and longitude within a specified radius (in meters) +// along a given polyline path. Upon reaching the end of the path, the function reverses direction and continues +// generating points along the same path, allowing for continuous point generation in a back-and-forth pattern. +func NearbyGPSOnPolyline(radius int) string { + ctx.JrContext.CtxGeoJsonLock.Lock() + defer ctx.JrContext.CtxGeoJsonLock.Unlock() + + // Ensure the path is available and has enough points + if len(ctx.JrContext.CtxGeoJson) < 2 { + println("Path must contain at least two points.") + os.Exit(1) + } + + // Get the current point on the path + currentPoint := ctx.JrContext.CtxGeoJson[0] + currentLat, currentLon := currentPoint[0], currentPoint[1] + + // Update last known point if there are recent saved coordinates + if len(ctx.JrContext.CtxLastPointLat) == 1 { + currentLat = ctx.JrContext.CtxLastPointLat[len(ctx.JrContext.CtxLastPointLat)-1] + } + if len(ctx.JrContext.CtxLastPointLon) == 1 { + currentLon = ctx.JrContext.CtxLastPointLon[len(ctx.JrContext.CtxLastPointLon)-1] + } + + // Convert radius to float for calculations + radiusInMeters := float64(radius) + + // Find the next point on the polyline at the specified distance + newLatitude, newLongitude, newDirection := findNextPointOnPolyline(ctx.JrContext.CtxGeoJson, []float64{currentLat, currentLon}, radiusInMeters, ctx.JrContext.CtxForward) + ctx.JrContext.CtxForward = newDirection + + // Update the context with the new valid point, maintaining a maximum ctx of 10 points + ctx.JrContext.CtxLastPointLat = append(ctx.JrContext.CtxLastPointLat, newLatitude) + ctx.JrContext.CtxLastPointLon = append(ctx.JrContext.CtxLastPointLon, newLongitude) + // Keep only the last point in the ctx + ctx.JrContext.CtxLastPointLat = ctx.JrContext.CtxLastPointLat[1:] + ctx.JrContext.CtxLastPointLon = ctx.JrContext.CtxLastPointLon[1:] + + // Return the coordinates of the valid point + return fmt.Sprintf("%.12f %.12f", newLatitude, newLongitude) +} + +// FindNextPointOnPolyline finds the next point on the polyline at a fixed distance, +// reversing direction when reaching the end of the path. +func findNextPointOnPolyline(polyline [][]float64, currentPoint []float64, distance float64, forward bool) (float64, float64, bool) { + if len(polyline) < 2 { + return currentPoint[0], currentPoint[1], forward // If the polyline is too short, return the current point + } + + totalDistance := 0.0 + foundCurrent := false + + // Traverse polyline based on the current direction + for i := 0; i < len(polyline)-1; i++ { + // Choose direction based on forward flag + index1, index2 := i, i+1 + if !forward { + index1, index2 = len(polyline)-1-i, len(polyline)-2-i + } + + segmentStart, segmentEnd := polyline[index1], polyline[index2] + + // Check if we are starting from the current point + if !foundCurrent && (comparePoints(segmentStart, currentPoint) || comparePoints(segmentEnd, currentPoint)) { + foundCurrent = true + } + + if foundCurrent { + // Calculate the distance to the end of this segment + segmentDistance := haversineDistance(segmentStart, segmentEnd) + + // Check if the target distance falls within this segment + if totalDistance+segmentDistance >= distance { + // Interpolate between segmentStart and segmentEnd + remainingDistance := distance - totalDistance + fraction := remainingDistance / segmentDistance + nextPoint := interpolate(segmentStart, segmentEnd, fraction) + return nextPoint[0], nextPoint[1], forward + } + + // Otherwise, accumulate distance and continue to the next segment + totalDistance += segmentDistance + } + } + + // If we reach the end of the polyline in forward direction, reverse direction + // and continue from the current point + return findNextPointOnPolyline(polyline, polyline[len(polyline)-1], distance-totalDistance, !forward) +} + +// haversineDistance calculates the Haversine distance (in meters) between two points. +func haversineDistance(p1, p2 []float64) float64 { + const R = 6371000 // Earth radius in meters + lat1, lon1 := p1[0]*math.Pi/180, p1[1]*math.Pi/180 + lat2, lon2 := p2[0]*math.Pi/180, p2[1]*math.Pi/180 + + dLat := lat2 - lat1 + dLon := lon2 - lon1 + + a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2) + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + + return R * c +} + +// ComparePoints checks if two points are approximately equal within a small tolerance, +// accounting for potential floating-point inaccuracies. +func comparePoints(p1, p2 []float64) bool { + return math.Abs(p1[0]-p2[0]) < 0 && math.Abs(p1[1]-p2[1]) < 0 +} +// interpolate calculates a point at a fraction of the distance between two points. +func interpolate(start, end []float64, fraction float64) []float64 { + lat := start[0] + fraction*(end[0]-start[0]) + lng := start[1] + fraction*(end[1]-start[1]) + return []float64{lat, lng} } // NearbyGPSIntoPolygon generates a random latitude and longitude within a specified radius (in meters) diff --git a/pkg/functions/functions.go b/pkg/functions/functions.go index 853dcce..ef3bfc7 100644 --- a/pkg/functions/functions.go +++ b/pkg/functions/functions.go @@ -36,7 +36,6 @@ import ( "github.com/google/uuid" "github.com/jrnd-io/jr/pkg/constants" "github.com/jrnd-io/jr/pkg/ctx" - geojson "github.com/paulmach/go.geojson" "github.com/rs/zerolog/log" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -141,6 +140,7 @@ var fmap = map[string]interface{}{ "nearby_gps": NearbyGPS, "nearby_gps_into_polygon": NearbyGPSIntoPolygon, "nearby_gps_into_polygon_without_start": NearbyGPSIntoPolygonWithoutStart, + "nearby_gps_on_polyline": NearbyGPSOnPolyline, "state": State, "state_at": StateAt, "state_short": StateShort, @@ -536,28 +536,89 @@ func InitCSV(csvpath string) { } func InitGeoJson(geojsonpath string) { - // Loads the csv file in the context if len(geojsonpath) == 0 { return } - file, err := os.Open(geojsonpath) - if err != nil { - println("Error reading GeoJson file:", geojsonpath, "error:", err) - os.Exit(1) + // Geometry represents a GeoJSON geometry object + type Geometry struct { + Type string `json:"type"` + Coordinates json.RawMessage `json:"coordinates"` } - defer file.Close() - var polygon struct { - Features []geojson.Feature `json:"features"` + // Feature represents a GeoJSON feature object + type Feature struct { + Type string `json:"type"` + Geometry Geometry `json:"geometry"` Properties map[string]interface{} `json:"properties"` - CRS map[string]interface{} `json:"crs,omitempty"` } - if err := json.NewDecoder(file).Decode(&polygon); err != nil { + + // FeatureCollection represents a GeoJSON feature collection + type FeatureCollection struct { + Type string `json:"type"` + Features []Feature `json:"features"` + } + + var geometry *Geometry + + // Read the GeoJSON file + data, err := os.ReadFile(geojsonpath) + if err != nil { println("Error decoding GeoJson file:", geojsonpath, "error:", err) os.Exit(1) } - geoTest := polygon.Features[0].Geometry - ctxgeojson := geoTest.Polygon[0] - ctx.JrContext.CtxGeoJson = ctxgeojson + // Check the type of GeoJSON object (FeatureCollection or single Feature) + var geoJSONType struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &geoJSONType); err != nil { + println("invalid GeoJSON format: %w", err) + os.Exit(1) + } + + switch geoJSONType.Type { + case "FeatureCollection": + // Parse as FeatureCollection + var featureCollection FeatureCollection + if err := json.Unmarshal(data, &featureCollection); err != nil { + println("error parsing FeatureCollection: %w", err) + os.Exit(1) + } + if len(featureCollection.Features) > 0 { + geometry = &featureCollection.Features[0].Geometry + } + case "Feature": + // Parse as single Feature + var feature Feature + if err := json.Unmarshal(data, &feature); err != nil { + println("error parsing Feature: %w", err) + os.Exit(1) + } + geometry = &feature.Geometry + default: + println("unsupported GeoJSON type: %w", geoJSONType.Type) + os.Exit(1) + } + + if geometry == nil { + println("no geometry found in GeoJSON") + os.Exit(1) + } + + var coordinates [][]float64 + switch geometry.Type { + case "Polygon": + var points [][][]float64 + json.Unmarshal(geometry.Coordinates, &points) + coordinates = points[0] + case "LineString": + var points [][]float64 + json.Unmarshal(geometry.Coordinates, &points) + coordinates = points + default: + println("unsupported GeoJSON type: %w", geoJSONType.Type) + os.Exit(1) + } + + ctx.JrContext.CtxGeoJson = coordinates } diff --git a/pkg/functions/functionsDescription.go b/pkg/functions/functionsDescription.go index 786173f..8a8ccc7 100644 --- a/pkg/functions/functionsDescription.go +++ b/pkg/functions/functionsDescription.go @@ -887,7 +887,7 @@ var funcDesc = map[string]FunctionDescription{ "nearby_gps_into_polygon": { Name: "nearby_gps_into_polygon", Category: "address", - Description: "returns a random latitude longitude within a given start point, radius in meters and poligon from a GeoJson file", + Description: "returns a random latitude longitude within a given start point, radius in meters and polygon from a GeoJson file", Parameters: "string", Localizable: false, Return: "string", @@ -897,13 +897,23 @@ var funcDesc = map[string]FunctionDescription{ "nearby_gps_into_polygon_without_start": { Name: "nearby_gps_into_polygon", Category: "address", - Description: "returns a random latitude longitude within a given start point, radius in meters and poligon from a GeoJson file", + Description: "returns a random latitude longitude within a given start point, radius in meters and polygon from a GeoJson file", Parameters: "string", Localizable: false, Return: "string", Example: `jr template run --embedded '{{nearby_gps_into_polygon_without_start 10}}' --geojson testfiles/polygon.geojson`, Output: "41.8963 12.4975", }, + "nearby_gps_on_polyline": { + Name: "nearby_gps_on_polyline", + Category: "address", + Description: "returns a random latitude longitude within a given radius in meters and polyline from a GeoJson file", + Parameters: "string", + Localizable: false, + Return: "string", + Example: `jr template run --embedded '{{nearby_gps_on_polyline 10}}' --geojson testfiles/polyline.geojson`, + Output: "41.8963 12.4975", + }, "now_add": { Name: "now_add", Category: "time", diff --git a/testfiles/polyline.geojson b/testfiles/polyline.geojson new file mode 100644 index 0000000..8137cf9 --- /dev/null +++ b/testfiles/polyline.geojson @@ -0,0 +1,421 @@ +{ + "type": "FeatureCollection", + "crs": { "type": "name", "properties": { "name": "EPSG:4326" } }, + "features": [ + { + "type": "Feature", + "properties": { + "name": "percorso test", + "stroke": "#ff0000", + "stroke-width": 3, + "stroke-opacity": 1 + }, + "geometry": { + "coordinates": [ + [ + 9.149096351139, + 45.884261154245 + ], + [ + 9.149066846842, + 45.884239397767 + ], + [ + 9.149044919173, + 45.884224531906 + ], + [ + 9.149077443089, + 45.884228627286 + ], + [ + 9.149097262801, + 45.884244123797 + ], + [ + 9.149106970205, + 45.884269579831 + ], + [ + 9.149115972106, + 45.884301094376 + ], + [ + 9.149072669748, + 45.884386376817 + ], + [ + 9.148950952161, + 45.884476493665 + ], + [ + 9.148839271788, + 45.884559255682 + ], + [ + 9.148788360134, + 45.884641355375 + ], + [ + 9.148743877475, + 45.884671685094 + ], + [ + 9.148678818173, + 45.884654789947 + ], + [ + 9.148597006444, + 45.884665290576 + ], + [ + 9.148523645205, + 45.884623610397 + ], + [ + 9.14853001479, + 45.884576704894 + ], + [ + 9.148534326752, + 45.884604362654 + ], + [ + 9.148521232251, + 45.884672313045 + ], + [ + 9.148542334481, + 45.884755058365 + ], + [ + 9.148589966651, + 45.884798782854 + ], + [ + 9.148631670484, + 45.884838693339 + ], + [ + 9.148669601782, + 45.884878433896 + ], + [ + 9.14863262336, + 45.884894949915 + ], + [ + 9.148556170462, + 45.884858960869 + ], + [ + 9.148423379456, + 45.884834813907 + ], + [ + 9.148268942936, + 45.884833292502 + ], + [ + 9.148173258536, + 45.884877823948 + ], + [ + 9.148035907466, + 45.884873019434 + ], + [ + 9.147859624149, + 45.884850054529 + ], + [ + 9.147715131227, + 45.884855329615 + ], + [ + 9.147586945762, + 45.884900793839 + ], + [ + 9.147418417288, + 45.88495773016 + ], + [ + 9.147249450605, + 45.885014916997 + ], + [ + 9.147146071907, + 45.885032716361 + ], + [ + 9.147014401846, + 45.885043250163 + ], + [ + 9.146863059428, + 45.885028327099 + ], + [ + 9.14671460443, + 45.88498990222 + ], + [ + 9.146581646477, + 45.884993180366 + ], + [ + 9.146410311718, + 45.88506826429 + ], + [ + 9.146233390809, + 45.885134613233 + ], + [ + 9.145986165414, + 45.885192256258 + ], + [ + 9.145747441948, + 45.885267989067 + ], + [ + 9.145439760455, + 45.88536741399 + ], + [ + 9.145181666002, + 45.885516483422 + ], + [ + 9.144929937479, + 45.885668484283 + ], + [ + 9.144721027971, + 45.885833027656 + ], + [ + 9.144529367024, + 45.886018607976 + ], + [ + 9.144342663688, + 45.886115963154 + ], + [ + 9.144165906097, + 45.886238198578 + ], + [ + 9.143934726884, + 45.886377028865 + ], + [ + 9.143614850655, + 45.886520811609 + ], + [ + 9.143330927394, + 45.886667037322 + ], + [ + 9.142960760091, + 45.886825549238 + ], + [ + 9.14260094767, + 45.886991947072 + ], + [ + 9.142639523852, + 45.887110547604 + ], + [ + 9.142638441286, + 45.887255950119 + ], + [ + 9.142611906251, + 45.887387145434 + ], + [ + 9.142604441843, + 45.887470490351 + ], + [ + 9.142582154939, + 45.887548378914 + ], + [ + 9.142636534664, + 45.887613072029 + ], + [ + 9.142651057127, + 45.887738661335 + ], + [ + 9.142699088883, + 45.887847629634 + ], + [ + 9.142716388288, + 45.88796447446 + ], + [ + 9.142774902229, + 45.888119155321 + ], + [ + 9.14285172096, + 45.88823437356 + ], + [ + 9.142954181821, + 45.888354170753 + ], + [ + 9.143007065356, + 45.88841951963 + ], + [ + 9.14302463546, + 45.888459937685 + ], + [ + 9.143013849649, + 45.888488763148 + ], + [ + 9.143060654936, + 45.888537256522 + ], + [ + 9.143090212216, + 45.888606249684 + ], + [ + 9.14317699033, + 45.88872610973 + ], + [ + 9.143317151163, + 45.88882294795 + ], + [ + 9.143401298859, + 45.888946765737 + ], + [ + 9.143538329694, + 45.889024223734 + ], + [ + 9.14365887685, + 45.889086213323 + ], + [ + 9.143762975573, + 45.889192015864 + ], + [ + 9.143838766811, + 45.889222484346 + ], + [ + 9.143903157317, + 45.88921777496 + ], + [ + 9.143947339318, + 45.889138056731 + ], + [ + 9.144006444844, + 45.889020791951 + ], + [ + 9.144097788497, + 45.888917200085 + ], + [ + 9.144184639487, + 45.888819026919 + ], + [ + 9.144282561898, + 45.888718164958 + ], + [ + 9.144389900092, + 45.888572275608 + ], + [ + 9.144490058436, + 45.888380816139 + ], + [ + 9.144631696322, + 45.88814475475 + ], + [ + 9.144728008342, + 45.887841225012 + ], + [ + 9.144829840633, + 45.887517006799 + ], + [ + 9.144929601974, + 45.887214280727 + ], + [ + 9.145019335975, + 45.886962076519 + ], + [ + 9.145137429916, + 45.886771373716 + ], + [ + 9.145185087273, + 45.886561247218 + ], + [ + 9.145160725228, + 45.886389118326 + ], + [ + 9.145184277064, + 45.886143382379 + ], + [ + 9.145245647675, + 45.88583782212 + ], + [ + 9.14535802808, + 45.885517275087 + ], + [ + 9.145421100647, + 45.885211723706 + ], + [ + 9.145413390199, + 45.88490889807 + ], + [ + 9.145407734315, + 45.884602648866 + ] + ], + "type": "LineString" + }, + "id": 0 + } + ] +} From e942725c583f2add7ba53a5a9874254a76bd9302 Mon Sep 17 00:00:00 2001 From: Efrem Ropelato Date: Sat, 9 Nov 2024 09:30:16 +0100 Subject: [PATCH 2/2] fix NearbyGPSOnPolyline --- pkg/ctx/context.go | 2 + pkg/functions/address.go | 149 +++++++++++++++++++++------------------ 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/pkg/ctx/context.go b/pkg/ctx/context.go index 7896c95..9eb1b8d 100644 --- a/pkg/ctx/context.go +++ b/pkg/ctx/context.go @@ -47,6 +47,7 @@ type Context struct { CtxLastPointLat []float64 CtxLastPointLon []float64 CtxForward bool + CtxIndex int LastIndex int CountryIndex int CityIndex int @@ -73,6 +74,7 @@ func init() { CtxLastPointLat: []float64{}, CtxLastPointLon: []float64{}, CtxForward: true, + CtxIndex: 0, LastIndex: -1, CountryIndex: 232, CityIndex: -1, diff --git a/pkg/functions/address.go b/pkg/functions/address.go index 2160e1e..dac77a6 100644 --- a/pkg/functions/address.go +++ b/pkg/functions/address.go @@ -158,94 +158,105 @@ func NearbyGPSOnPolyline(radius int) string { radiusInMeters := float64(radius) // Find the next point on the polyline at the specified distance - newLatitude, newLongitude, newDirection := findNextPointOnPolyline(ctx.JrContext.CtxGeoJson, []float64{currentLat, currentLon}, radiusInMeters, ctx.JrContext.CtxForward) + nextPoint, nuovoIndex, newDirection := findNextPoint(ctx.JrContext.CtxGeoJson, []float64{currentLat, currentLon}, ctx.JrContext.CtxForward, ctx.JrContext.CtxIndex, radiusInMeters) ctx.JrContext.CtxForward = newDirection + ctx.JrContext.CtxIndex = nuovoIndex // Update the context with the new valid point, maintaining a maximum ctx of 10 points - ctx.JrContext.CtxLastPointLat = append(ctx.JrContext.CtxLastPointLat, newLatitude) - ctx.JrContext.CtxLastPointLon = append(ctx.JrContext.CtxLastPointLon, newLongitude) + ctx.JrContext.CtxLastPointLat = append(ctx.JrContext.CtxLastPointLat, nextPoint[0]) + ctx.JrContext.CtxLastPointLon = append(ctx.JrContext.CtxLastPointLon, nextPoint[1]) // Keep only the last point in the ctx - ctx.JrContext.CtxLastPointLat = ctx.JrContext.CtxLastPointLat[1:] - ctx.JrContext.CtxLastPointLon = ctx.JrContext.CtxLastPointLon[1:] + if len(ctx.JrContext.CtxLastPointLat) > 1 { + ctx.JrContext.CtxLastPointLat = ctx.JrContext.CtxLastPointLat[1:] + } + if len(ctx.JrContext.CtxLastPointLon) > 1 { + ctx.JrContext.CtxLastPointLon = ctx.JrContext.CtxLastPointLon[1:] + } // Return the coordinates of the valid point - return fmt.Sprintf("%.12f %.12f", newLatitude, newLongitude) + return fmt.Sprintf("%.12f %.12f", nextPoint[0], nextPoint[1]) } -// FindNextPointOnPolyline finds the next point on the polyline at a fixed distance, -// reversing direction when reaching the end of the path. -func findNextPointOnPolyline(polyline [][]float64, currentPoint []float64, distance float64, forward bool) (float64, float64, bool) { - if len(polyline) < 2 { - return currentPoint[0], currentPoint[1], forward // If the polyline is too short, return the current point - } - - totalDistance := 0.0 - foundCurrent := false - - // Traverse polyline based on the current direction - for i := 0; i < len(polyline)-1; i++ { - // Choose direction based on forward flag - index1, index2 := i, i+1 - if !forward { - index1, index2 = len(polyline)-1-i, len(polyline)-2-i - } +// distanceEuclidean calculates the Euclidean distance between two points (p1 and p2) represented as []float64. +// It returns the straight-line distance between the points in meters. +func distanceEuclidean(p1, p2 []float64) float64 { + dx := p2[0] - p1[0] // Difference in the x-coordinates + dy := p2[1] - p1[1] // Difference in the y-coordinates + return math.Sqrt(dx*dx + dy*dy) // Apply the Euclidean distance formula +} - segmentStart, segmentEnd := polyline[index1], polyline[index2] +// distanceHaversine calculates the Haversine distance between two points (p1 and p2) on the Earth's surface, +// given their latitude and longitude coordinates in degrees. It returns the distance in meters. +func distanceHaversine(p1, p2 []float64) float64 { + lat1 := p1[0] * math.Pi / 180 // Convert latitude of point 1 from degrees to radians + lat2 := p2[0] * math.Pi / 180 // Convert latitude of point 2 from degrees to radians + deltaLat := (p2[0] - p1[0]) * math.Pi / 180 // Difference in latitudes (in radians) + deltaLng := (p2[1] - p1[1]) * math.Pi / 180 // Difference in longitudes (in radians) - // Check if we are starting from the current point - if !foundCurrent && (comparePoints(segmentStart, currentPoint) || comparePoints(segmentEnd, currentPoint)) { - foundCurrent = true - } - - if foundCurrent { - // Calculate the distance to the end of this segment - segmentDistance := haversineDistance(segmentStart, segmentEnd) - - // Check if the target distance falls within this segment - if totalDistance+segmentDistance >= distance { - // Interpolate between segmentStart and segmentEnd - remainingDistance := distance - totalDistance - fraction := remainingDistance / segmentDistance - nextPoint := interpolate(segmentStart, segmentEnd, fraction) - return nextPoint[0], nextPoint[1], forward - } + // Haversine formula for calculating the distance between two points on a sphere + a := math.Sin(deltaLat/2)*math.Sin(deltaLat/2) + + math.Cos(lat1)*math.Cos(lat2)* + math.Sin(deltaLng/2)*math.Sin(deltaLng/2) - // Otherwise, accumulate distance and continue to the next segment - totalDistance += segmentDistance - } - } + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) // Angular distance in radians - // If we reach the end of the polyline in forward direction, reverse direction - // and continue from the current point - return findNextPointOnPolyline(polyline, polyline[len(polyline)-1], distance-totalDistance, !forward) + // Earth’s radius in meters (mean radius) + return earthRadius * c // Final distance in meters } -// haversineDistance calculates the Haversine distance (in meters) between two points. -func haversineDistance(p1, p2 []float64) float64 { - const R = 6371000 // Earth radius in meters - lat1, lon1 := p1[0]*math.Pi/180, p1[1]*math.Pi/180 - lat2, lon2 := p2[0]*math.Pi/180, p2[1]*math.Pi/180 +// findNextPoint calculates the point on a polyline after traveling a specified distance. +// If the end of the polyline is reached, it reverses the direction and continues the search. +// Returns the destination point, the last index, and the final direction. +func findNextPoint(polyline [][]float64, startingPoint []float64, direction bool, currentIndex int, distanceToTravel float64) ([]float64, int, bool) { + currentPoint := startingPoint // Initialize the current point as the starting point + currentDirection := direction // Set the current direction (true for forward, false for backward) + + for distanceToTravel > 0 { // Continue until the remaining distance is exhausted + // Determine the next index based on the current direction + nextIndex := currentIndex + if currentDirection { + nextIndex++ // Move forward if the direction is true + } else { + nextIndex-- // Move backward if the direction is false + } - dLat := lat2 - lat1 - dLon := lon2 - lon1 + // Check if it’s necessary to reverse the direction at the end of the polyline + if nextIndex >= len(polyline) { // If we reach the end of the polyline + currentDirection = false // Reverse the direction + nextIndex = len(polyline) - 2 // Move to the second-to-last point + } else if nextIndex < 0 { // If we reach the beginning of the polyline + currentDirection = true // Reverse the direction + nextIndex = 1 // Move to the second point + } - a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1)*math.Cos(lat2)*math.Sin(dLon/2)*math.Sin(dLon/2) - c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + // Calculate the distance to the next point on the polyline + nextPoint := polyline[nextIndex] + segmentDistance := 0.0 + if distanceToTravel > 1000.00 { // Use Haversine for larger distances + segmentDistance = distanceHaversine(currentPoint, nextPoint) + } else { // Use Euclidean for smaller distances + segmentDistance = distanceHaversine(currentPoint, nextPoint) + } - return R * c -} + // If the remaining distance is within this segment, calculate the destination point + if distanceToTravel <= segmentDistance { + // Calculate the point along the segment where the remaining distance ends + ratio := distanceToTravel / segmentDistance + destinationPoint := []float64{ + currentPoint[0] + (nextPoint[0]-currentPoint[0])*ratio, + currentPoint[1] + (nextPoint[1]-currentPoint[1])*ratio, + } + return destinationPoint, currentIndex, currentDirection + } -// ComparePoints checks if two points are approximately equal within a small tolerance, -// accounting for potential floating-point inaccuracies. -func comparePoints(p1, p2 []float64) bool { - return math.Abs(p1[0]-p2[0]) < 0 && math.Abs(p1[1]-p2[1]) < 0 -} + // Update the remaining distance and move to the next point + distanceToTravel -= segmentDistance + currentPoint = nextPoint + currentIndex = nextIndex + } -// interpolate calculates a point at a fraction of the distance between two points. -func interpolate(start, end []float64, fraction float64) []float64 { - lat := start[0] + fraction*(end[0]-start[0]) - lng := start[1] + fraction*(end[1]-start[1]) - return []float64{lat, lng} + // Return the final point if the entire distance is traversed + return currentPoint, currentIndex, currentDirection } // NearbyGPSIntoPolygon generates a random latitude and longitude within a specified radius (in meters)