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

Add functionality for calculating Aerial Distances #47

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dig pi @dns.toys
dig 100dec-hex.base @dns.toys

dig fun.dict @dns.toys

dig A12.9352,77.6245/12.9698,77.7500.aerial @dns.toys
```

## Running locally
Expand Down
2 changes: 1 addition & 1 deletion cmd/dnstoys/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type handlers struct {
help []dns.RR
}

var reClean = regexp.MustCompile("[^a-zA-Z0-9/\\-\\.:]")
var reClean = regexp.MustCompile("[^a-zA-Z0-9/\\-\\.:,]")

// register registers a Service for a given query suffix on the DNS server.
// A Service responds to a DNS query via Query().
Expand Down
9 changes: 9 additions & 0 deletions cmd/dnstoys/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/knadh/dns.toys/internal/geo"
"github.com/knadh/dns.toys/internal/services/aerial"
"github.com/knadh/dns.toys/internal/services/base"
"github.com/knadh/dns.toys/internal/services/cidr"
"github.com/knadh/dns.toys/internal/services/coin"
Expand Down Expand Up @@ -306,6 +307,14 @@ func main() {
help = append(help, []string{"convert epoch / UNIX time to human readable time.", "dig 784783800.epoch @%s"})
}

// Aerial Distance between Lat,Lng
if ko.Bool("aerial.enabled") {
a := aerial.New()
h.register("aerial", a, mux)

help = append(help, []string{"get aerial distance between lat lng pair", "dig A12.9352,77.6245/12.9698,77.7500.aerial @%s"})
}

// Prepare the static help response for the `help` query.
for _, l := range help {
r, err := dns.NewRR(fmt.Sprintf("help. 1 TXT \"%s\" \"%s\"", l[0], fmt.Sprintf(l[1], h.domain)))
Expand Down
3 changes: 3 additions & 0 deletions config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ enabled = true
[epoch]
enabled = true
send_local_time = true

[aerial]
enabled = true
8 changes: 8 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ <h2>Epoch/Unix timestamp conversion</h2>
<p>Convert an epoch/unix timestamp into a human readable date. Supports Unix timestamps in s, ms, µs and ns. </p>
</section>

<section class="box">
<h2>Calculate Aerial Distance</h2>
<code class="block">
<p>dig A12.9352,77.6245/12.9698,77.7500.aerial @dns.toys</p>
</code>
<p>Calculate Aerial Distance between a Lat Lng Pair</p>
</section>

<section class="box">
<h2>Help</h2>
<code class="block">
Expand Down
137 changes: 137 additions & 0 deletions internal/services/aerial/aerial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package aerial

import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
)

type Aerial struct{}

type Location struct {
Lat float64
Lng float64
}

// New returns a new instance of Aerial.
func New() *Aerial {
return &Aerial{}
}

var validPointRegex = "(-?\\d+.\\d+)"
var delimiter = ","
var separator = "/"
var latlngpair = validPointRegex + delimiter + validPointRegex

var reParse = regexp.MustCompile("A" + latlngpair + separator + latlngpair)

// Query returns the aerial distance in KMs between lat lng pair
func (a *Aerial) Query(q string) ([]string, error) {
regexGroups := reParse.FindStringSubmatch(q)

if len(regexGroups) != 5 {
return nil, errors.New("invalid lat lng format")
}

res := regexGroups[1:]
cord := make([]float64, 0, len(res))
for _, p := range res {
// iterate overy every point to convert into float
f, err := strconv.ParseFloat(p, 64)
if err != nil {
return nil, fmt.Errorf("invalid point %s; Error: %w", p, err)
}
cord = append(cord, f)
}

l1 := Location{Lat: cord[0], Lng: cord[1]}
l2 := Location{Lat: cord[2], Lng: cord[3]}

d, e := CalculateAerialDistance(l1, l2)
if e != nil {
return nil, e
}

result := "aerial distance = " + strconv.FormatFloat(d, 'f', 2, 64) + " KMs"

r := fmt.Sprintf(`%s 1 TXT "%s"`, q, result)
return []string{r}, nil
}

// Dump is not implemented in this package.
func (n *Aerial) Dump() ([]byte, error) {
return nil, nil
}

// calculates aerial distance in KMs
func CalculateAerialDistance(l1, l2 Location) (float64, error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add reciever here as well?


e1 := validateLocation(l1)
e2 := validateLocation(l2)

if e1 != nil || e2 != nil {
errString := "";
if (e1 != nil) {
errString += e1.Error();
}
if (e2 != nil) {
if (e1 != nil) {
errString += "; "
}
errString += e2.Error()
}

return 0, errors.New(errString)
}

lat1 := l1.Lat
lng1 := l1.Lng
lat2 := l2.Lat
lng2 := l2.Lng

radlat1 := float64(math.Pi * lat1 / 180)
radlat2 := float64(math.Pi * lat2 / 180)

radtheta := float64(math.Pi * float64(lng1-lng2) / 180)

d := math.Sin(radlat1)*math.Sin(radlat2) + math.Cos(radlat1)*math.Cos(radlat2)*math.Cos(radtheta)
if d > 1 {
d = 1
}

d = math.Acos(d)
d = d * 180 / math.Pi
d = d * 60 * 1.1515 * 1.609344

return d, nil
knadh marked this conversation as resolved.
Show resolved Hide resolved
}

func isValidPoint(point, maxVal float64) bool {
absoluteVal := math.Abs(point)
return absoluteVal <= maxVal
}

func validateLocation(l Location) error {
errString := ""

isLatValid := isValidPoint(l.Lat, 90)
if !isLatValid {
errString += strconv.FormatFloat(l.Lat, 'f', -1, 64) + " lat out of bounds"
}

isLngValid := isValidPoint(l.Lng, 180)
if !isLngValid {
if (!isLatValid) {
errString += " "
}
errString += strconv.FormatFloat(l.Lng, 'f', -1, 64) + " lng out of bounds"
}

if (errString != "") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: to be consistent with the code we can remove parantheses for if statements

return errors.New(errString)
}

return nil
}
112 changes: 112 additions & 0 deletions internal/services/aerial/aerial_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package aerial_test

import (
"errors"
"testing"

"github.com/knadh/dns.toys/internal/services/aerial"
)

var tests = []struct {
l1 aerial.Location
l2 aerial.Location
d float64
e error
}{
{
aerial.Location{Lat: 30.2458, Lng: 75.8421}, // Sangrur
aerial.Location{Lat: 30.2001, Lng: 75.6755}, // Longowal
16.793459061041027,
nil,
},
{
aerial.Location{Lat: 12.9352, Lng: 77.6245}, // Kormangala
aerial.Location{Lat: 12.9698, Lng: 77.7500}, // Whitefield
14.132940521067107,
nil,
},
{
aerial.Location{Lat: 12.9716, Lng: 77.5946}, // Bengaluru
aerial.Location{Lat: 28.7041, Lng: 77.1025}, // New Delhi
1750.0305628709923,
nil,
},
{
aerial.Location{Lat: -120.9716, Lng: 77.5946}, // Wrong Lat
aerial.Location{Lat: 28.7041, Lng: 77.1025}, // New Delhi
0,
errors.New("-120.9716 lat out of bounds"),
},
{
aerial.Location{Lat: 120.9716, Lng: 77.5946}, // Wrong Lat
aerial.Location{Lat: 28.7041, Lng: 77.1025}, // New Delhi
0,
errors.New("120.9716 lat out of bounds"),
},
{
aerial.Location{Lat: 12.9716, Lng: 277.5946}, // Wrong Lng
aerial.Location{Lat: 28.7041, Lng: 77.1025}, // New Delhi
0,
errors.New("277.5946 lng out of bounds"),
},
{
aerial.Location{Lat: 12.9716, Lng: -277.5946}, // Wrong Lng
aerial.Location{Lat: 28.7041, Lng: 77.1025}, // New Delhi
0,
errors.New("-277.5946 lng out of bounds"),
},
{
aerial.Location{Lat: 12.9716, Lng: 77.5946}, // Bengaluru
aerial.Location{Lat: 128.7041, Lng: 77.1025}, // Wrong Lat
0,
errors.New("128.7041 lat out of bounds"),
},
{
aerial.Location{Lat: 12.9716, Lng: 77.5946}, // Bengaluru
aerial.Location{Lat: -128.7041, Lng: 77.1025}, // Wrong Lat
0,
errors.New("-128.7041 lat out of bounds"),
},
{
aerial.Location{Lat: 12.9716, Lng: 77.5946}, // Bengaluru
aerial.Location{Lat: 28.7041, Lng: -187.1025}, // Wrong Lng
0,
errors.New("-187.1025 lng out of bounds"),
},
{
aerial.Location{Lat: 12.9716, Lng: 77.5946}, // Bengaluru
aerial.Location{Lat: 28.7041, Lng: 187.1025}, // Wrong Lng
0,
errors.New("187.1025 lng out of bounds"),
},
{
aerial.Location{Lat: -120.9716, Lng: 77.5946}, // Wrong Lat
aerial.Location{Lat: 28.7041, Lng: 187.1025}, // Wrong Lng
0,
errors.New("-120.9716 lat out of bounds; 187.1025 lng out of bounds"),
},
{
aerial.Location{Lat: -120.9716, Lng: 277.5946}, // Wrong Lat Lng
aerial.Location{Lat: 28.7041, Lng: 187.1025}, // Wrong Lng
0,
errors.New("-120.9716 lat out of bounds 277.5946 lng out of bounds; 187.1025 lng out of bounds"),
},
}

func TestCalculateAerialDistance(t *testing.T) {
for _, input := range tests {
d, e := aerial.CalculateAerialDistance(input.l1, input.l2)

if e != nil && input.e == nil {
t.Errorf("fail: %v %v -> expected %v -> got error:%s;", input.l1, input.l2, input.d, e.Error())
}

if e != nil && input.e != nil && input.e.Error() != e.Error() {
t.Errorf("fail: %v %v ->expected error:%s-> got error:%s;", input.l1, input.l2, input.e.Error(), e.Error())
}

if input.d != d {
t.Errorf("fail: want %v %v -> %v got %v", input.l1, input.l2, input.d, d)
}
}
}