Skip to content

Commit

Permalink
Merge pull request #201 from OS-Mind/feature/NearbyGPSIntoPolygon
Browse files Browse the repository at this point in the history
New functions to generate random latitude and longitude coordinates within a specified polygon
  • Loading branch information
vmarchese authored Nov 10, 2024
2 parents 8fe223c + 41064df commit a67c27f
Show file tree
Hide file tree
Showing 12 changed files with 1,920 additions and 25 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ _testmain.go
*.iml
.idea/
.idea
.vscode/
.vscode
.DS_Store

# Specific project files
Expand All @@ -53,4 +55,4 @@ pkg/producers/redis/config.json
pkg/producers/elastic/*.json
pkg/producers/mongoDB/*.json
pkg/producers/gcs/*.json
pkg/producers/s3/*.json
pkg/producers/s3/*.json
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,4 @@ install:
install build/jr /usr/local/bin

all: hello install-gogen generate compile
all_offline: hello generate compile
all_offline: hello generate compile
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,15 @@ require (

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cnkei/gospline v0.0.0-20191204052713-d67fac29a294
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
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
39 changes: 39 additions & 0 deletions go.sum

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pkg/cmd/templateRun.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jr template run --template "{{name}}"
preload, _ := cmd.Flags().GetInt("preload")

csv, _ := cmd.Flags().GetString("csv")
geojson, _ := cmd.Flags().GetString("geojson")

if kcat {
oneline = true
Expand Down Expand Up @@ -145,6 +146,7 @@ jr template run --template "{{name}}"
Kcat: kcat,
Oneline: oneline,
Csv: csv,
GeoJson: geojson,
}

functions.SetSeed(seed)
Expand All @@ -164,6 +166,8 @@ func init() {

templateRunCmd.Flags().String("csv", "", "Path to csv file to use")

templateRunCmd.Flags().String("geojson", "", "Path to geojson file to use")

templateRunCmd.Flags().StringP("kafkaConfig", "F", "", "Kafka configuration")
templateRunCmd.Flags().String("registryConfig", "", "Kafka configuration")
templateRunCmd.Flags().Bool("embedded", false, "If enabled, [template] must be a string containing a template, to be embedded directly in the script")
Expand Down
10 changes: 9 additions & 1 deletion pkg/ctx/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,18 @@ type Context struct {
CtxListLock sync.RWMutex
CtxCSV map[int]map[string]string
CtxCSVLock sync.RWMutex
CtxGeoJson [][]float64
CtxGeoJsonLock sync.RWMutex
CtxLastPointLat []float64
CtxLastPointLon []float64
LastIndex int
CountryIndex int
CityIndex int
CurrentIterationLoopIndex int
}

func init() {

var ctxgeojson [][]float64
JrContext = &Context{
StartTime: time.Now(),
GeneratedBytes: 0,
Expand All @@ -63,6 +67,10 @@ func init() {
CtxListLock: sync.RWMutex{},
CtxCSV: make(map[int]map[string]string),
CtxCSVLock: sync.RWMutex{},
CtxGeoJson: ctxgeojson,
CtxGeoJsonLock: sync.RWMutex{},
CtxLastPointLat: []float64{},
CtxLastPointLon: []float64{},
LastIndex: -1,
CountryIndex: 232,
CityIndex: -1,
Expand Down
7 changes: 5 additions & 2 deletions pkg/emitter/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ package emitter
import (
"context"
"fmt"
"github.com/jrnd-io/jr/pkg/producers/wasm"
"os"
"time"

"github.com/jrnd-io/jr/pkg/producers/wasm"

"github.com/jrnd-io/jr/pkg/configuration"
"github.com/jrnd-io/jr/pkg/constants"
jtctx "github.com/jrnd-io/jr/pkg/ctx"
Expand Down Expand Up @@ -65,6 +66,7 @@ type Emitter struct {
Kcat bool `mapstructure:"kcat"`
Oneline bool `mapstructure:"oneline"`
Csv string `mapstructure:"csv"`
GeoJson string `mapstructure:"geojson"`
Producer Producer
KTpl tpl.Tpl
VTpl tpl.Tpl
Expand All @@ -74,6 +76,8 @@ func (e *Emitter) Initialize(ctx context.Context, conf configuration.GlobalConfi

functions.InitCSV(e.Csv)

functions.InitGeoJson(e.GeoJson)

templateName := e.ValueTemplate
if e.EmbeddedTemplate == "" {
path := os.ExpandEnv(fmt.Sprintf("%s/%s", constants.JR_SYSTEM_DIR, "templates"))
Expand Down Expand Up @@ -283,7 +287,6 @@ func createWASMProducer(ctx context.Context, config string) Producer {

func createKafkaProducer(ctx context.Context, conf configuration.GlobalConfiguration, topic string, templateType string) *kafka.Manager {


kManager := &kafka.Manager{
Serializer: conf.Serializer,
Topic: topic,
Expand Down
198 changes: 197 additions & 1 deletion pkg/functions/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ package functions

import (
"fmt"
"github.com/jrnd-io/jr/pkg/ctx"
"math"
"os"

"github.com/cnkei/gospline"
"github.com/jrnd-io/jr/pkg/ctx"
)

const (
Expand Down Expand Up @@ -127,6 +130,199 @@ func NearbyGPS(latitude float64, longitude float64, radius int) string {

}

// NearbyGPSIntoPolygon generates a random latitude and longitude within a specified radius (in meters)
// from an initial point and checks if the generated point falls within the boundaries of a polygon
// defined in a GeoJSON file. If successful, it returns the coordinates as a formatted string.
func NearbyGPSIntoPolygon(latitude float64, longitude float64, radius int) string {
// Lock the GeoJSON context to ensure thread safety
ctx.JrContext.CtxGeoJsonLock.Lock()
defer ctx.JrContext.CtxGeoJsonLock.Unlock()

// Default starting point: either use the provided coordinates or the last known point if available from ctx
lastLat := latitude
lastLon := longitude

// Update last known point if there are recent saved coordinates
if len(ctx.JrContext.CtxLastPointLat) == 1 {
lastLat = ctx.JrContext.CtxLastPointLat[len(ctx.JrContext.CtxLastPointLat)-1]
}
if len(ctx.JrContext.CtxLastPointLon) == 1 {
lastLon = ctx.JrContext.CtxLastPointLon[len(ctx.JrContext.CtxLastPointLon)-1]
}

// Predict the next point if there is enough data for interpolation
if len(ctx.JrContext.CtxLastPointLat) >= 2 && len(ctx.JrContext.CtxLastPointLon) >= 2 {
lastLat, lastLon = predictNextPoint(ctx.JrContext.CtxLastPointLat, ctx.JrContext.CtxLastPointLon)
}

// Ensure that the GeoJSON polygon has enough vertices (at least 3) to form a valid shape
if len(ctx.JrContext.CtxGeoJson) < 3 {
return fmt.Sprintf("%.12f %.12f", lastLat, lastLon)
}

// Convert radius to float for calculations
radiusInMeters := float64(radius)

attempts := 0

// Loop until a valid point within the polygon is found
for {
if attempts > 10 {
// Slightly expand the search radius to ensure coverage
radiusInMeters *= 1.1
}
// Generate a random angle and distance within the specified radius
randomAngle := Random.Float64() * 2 * math.Pi
distanceInMeters := Random.Float64() * radiusInMeters

// Convert the distance from meters to degrees (assuming small distances for simplicity)
distanceInDegrees := distanceInMeters * degreesPerMeter

// Calculate new latitude and longitude based on the random angle and distance
newLatitude := lastLat + (distanceInDegrees * math.Cos(randomAngle))
newLongitude := lastLon + (distanceInDegrees * math.Sin(randomAngle))

// Check if the generated point lies within the specified polygon
if isPointInPolygon([]float64{newLatitude, newLongitude}, ctx.JrContext.CtxGeoJson) {
// 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 the last 10 points in the ctx
if len(ctx.JrContext.CtxLastPointLat) > 10 {
ctx.JrContext.CtxLastPointLat = ctx.JrContext.CtxLastPointLat[1:]
}
if len(ctx.JrContext.CtxLastPointLon) > 10 {
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 %d %.12f", newLatitude, newLongitude, attempts, radiusInMeters)
}
attempts++
// Retry if the generated point is not within the polygon boundaries
}
}

// NearbyGPSIntoPolygonWithoutStart
func NearbyGPSIntoPolygonWithoutStart(radius int) string {
latitude, longitude := selectRandomPoint(ctx.JrContext.CtxGeoJson)
return NearbyGPSIntoPolygon(latitude, longitude, radius)
}

// isPointInPolygon checks if a given point lies within a specified polygon.
func isPointInPolygon(point []float64, vertices [][]float64) bool {
x, y := point[1], point[0]
n := len(vertices)
// A polygon must have at least 3 vertices
if n < 3 {
return false
}
intersections := 0
for i := 0; i < n; i++ {
x1, y1 := vertices[i][0], vertices[i][1]
x2, y2 := vertices[(i+1)%n][0], vertices[(i+1)%n][1]
if (y1 > y) != (y2 > y) {
xInt := (y-y1)*(x2-x1)/(y2-y1) + x1
if x < xInt {
intersections++
}
}
}
return intersections%2 == 1
}

// predictNextPoint predicts the next latitude and longitude using cubic spline interpolation
func predictNextPoint(latitudes, longitudes []float64) (float64, float64) {
if len(longitudes) != len(latitudes) {
println("Need at least two points and matching latitude/longitude arrays: latitudes: ", len(latitudes), " e longitudes:", len(longitudes))
os.Exit(1)
}

if len(latitudes) < 2 && len(longitudes) < 2 {
fmt.Println("Need at least two points !")
return 0, 0
}

// Create X values based on indices (0, 1, 2, ...) for even spacing assumption
x := make([]float64, len(latitudes))
for i := 0; i < len(latitudes); i++ {
x[i] = float64(i)
}

// Create splines for latitude and longitude
latSpline := gospline.NewCubicSpline(x, latitudes)
lonSpline := gospline.NewCubicSpline(x, longitudes)

// Predict the next index position (next point in the sequence)
nextX := float64(len(latitudes))

// Interpolate to get the predicted latitude and longitude for nextX
nextLatitude := latSpline.At(nextX)
nextLongitude := lonSpline.At(nextX)

return nextLatitude, nextLongitude
}

// selectRandomPoint selects a random point within the polygon defined by the given coordinates.
func selectRandomPoint(coords [][]float64) (float64, float64) {
if len(coords) == 0 {
return 0, 0 // Return zero values if no coordinates are provided
}

// Calculate the bounding box of the coordinates
minX, minY, maxX, maxY := boundingBox(coords)

var randX, randY float64

// Loop until a valid point within the polygon is found
for {
// Generate a random point within the bounding box
x := Random.Float64()*(maxX-minX) + minX
y := Random.Float64()*(maxY-minY) + minY

// Check if the generated point is within the polygon
if isPointInPolygon([]float64{y, x}, coords) {
randX = x
randY = y
break // Exit the loop once a valid point is found
}
}

return randY, randX // Return as (latitude, longitude)
}

// boundingBox calculates the minimum and maximum coordinates (bounding box) of the provided vertices.
// It returns the coordinates of the bottom-left (minX, minY) and top-right (maxX, maxY) corners
// of the bounding box that encompasses all the given vertices.
func boundingBox(vertices [][]float64) (minX, minY, maxX, maxY float64) {
if len(vertices) == 0 {
return 0, 0, 0, 0 // Return zero values if no vertices are provided
}

// Initialize min and max values based on the first vertex
minX, minY = vertices[0][0], vertices[0][1]
maxX, maxY = minX, minY

// Iterate through vertices to find the bounding box
for _, vertex := range vertices {
x, y := vertex[0], vertex[1]
if x < minX {
minX = x
}
if y < minY {
minY = y
}
if x > maxX {
maxX = x
}
if y > maxY {
maxY = y
}
}
return
}

// State returns a random State
func State() string {
s := Word("state")
Expand Down
Loading

0 comments on commit a67c27f

Please sign in to comment.