Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New function to generate random Latitude and Longitude coordinates along a specified Polyline path #202

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/ctx/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Context struct {
CtxGeoJsonLock sync.RWMutex
CtxLastPointLat []float64
CtxLastPointLon []float64
CtxForward bool
CtxIndex int
LastIndex int
CountryIndex int
CityIndex int
Expand All @@ -71,6 +73,8 @@ func init() {
CtxGeoJsonLock: sync.RWMutex{},
CtxLastPointLat: []float64{},
CtxLastPointLon: []float64{},
CtxForward: true,
CtxIndex: 0,
LastIndex: -1,
CountryIndex: 232,
CityIndex: -1,
Expand Down
129 changes: 129 additions & 0 deletions pkg/functions/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,136 @@ 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
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, nextPoint[0])
ctx.JrContext.CtxLastPointLon = append(ctx.JrContext.CtxLastPointLon, nextPoint[1])
// Keep only the last point in the ctx
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", nextPoint[0], nextPoint[1])
}

// 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
}

// 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)

// 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)

c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) // Angular distance in radians

// Earth’s radius in meters (mean radius)
return earthRadius * c // Final distance in meters
}

// 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
}

// 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
}

// 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)
}

// 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
}

// Update the remaining distance and move to the next point
distanceToTravel -= segmentDistance
currentPoint = nextPoint
currentIndex = nextIndex
}

// 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)
Expand Down
89 changes: 75 additions & 14 deletions pkg/functions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
14 changes: 12 additions & 2 deletions pkg/functions/functionsDescription.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading
Loading