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 functions to generate random latitude and longitude coordinates within a specified polygon #201

Merged
merged 15 commits into from
Nov 10, 2024
Merged
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
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
hifly81 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading