Current section
5 Versions
Jump to
Current section
5 Versions
Compare versions
3
files changed
+128
additions
-136
deletions
| @@ -2,7 +2,7 @@ | |
| 2 2 | [{<<"GitHub">>,<<"https://github.com/gmcintire/gridsquare">>}, |
| 3 3 | {<<"HexDocs">>,<<"https://hexdocs.pm/gridsquare">>}]}. |
| 4 4 | {<<"name">>,<<"gridsquare">>}. |
| 5 | - {<<"version">>,<<"0.2.0">>}. |
| 5 | + {<<"version">>,<<"0.3.0">>}. |
| 6 6 | {<<"description">>, |
| 7 7 | <<"GridSquare calculator for encoding/decoding between latitude/longitude and Maidenhead Locator System grid references">>}. |
| 8 8 | {<<"elixir">>,<<"~> 1.16">>}. |
| @@ -124,10 +124,11 @@ defmodule Gridsquare do | |
| 124 124 | %Gridsquare.EncodeResult{grid_reference: "DN40bi", subsquare: "dn40bi"} |
| 125 125 | |
| 126 126 | iex> Gridsquare.encode(-111.866785, 40.363840, 10) |
| 127 | - %Gridsquare.EncodeResult{grid_reference: "DN40BI00OR", subsquare: "dn40bi"} |
| 127 | + %Gridsquare.EncodeResult{grid_reference: "DN40BI57XH", subsquare: "dn40bi"} |
| 128 128 | """ |
| 129 129 | @spec encode(longitude(), latitude(), precision()) :: encode_result() |
| 130 | - def encode(lon, lat, precision \\ 6) when precision >= 6 and precision <= 20 do |
| 130 | + def encode(lon, lat, precision \\ 6) |
| 131 | + when precision >= 6 and precision <= 20 and rem(precision, 2) == 0 do |
| 131 132 | {normalized_lon, normalized_lat} = normalize_coordinates(lon, lat) |
| 132 133 | {field_lon, field_lat} = calculate_fields(normalized_lon, normalized_lat) |
| 133 134 | |
| @@ -207,15 +208,23 @@ defmodule Gridsquare do | |
| 207 208 | subsquare_lon = from_base24(String.at(grid_reference, 4)) |
| 208 209 | subsquare_lat = from_base24(String.at(grid_reference, 5)) |
| 209 210 | |
| 210 | - # Calculate center coordinates (Ruby gem logic) |
| 211 | - lon = -180 + field_lon * 20 + square_lon * 2 + subsquare_lon * (2 / 24) + 2 / 24 / 2 |
| 212 | - lat = -90 + field_lat * 10 + square_lat + subsquare_lat * (1 / 24) + 1 / 24 / 2 |
| 211 | + # Calculate base offset (without centering) |
| 212 | + base_lon = -180 + field_lon * 20 + square_lon * 2 + subsquare_lon * (2 / 24) |
| 213 | + base_lat = -90 + field_lat * 10 + square_lat + subsquare_lat * (1 / 24) |
| 213 214 | |
| 214 | - # Calculate dimensions |
| 215 | - # 5 minutes |
| 216 | - width = 2 / 24 |
| 217 | - # 2.5 minutes |
| 218 | - height = 1 / 24 |
| 215 | + # Starting divisors at subsquare level |
| 216 | + base_lon_div = 2 / 24 |
| 217 | + base_lat_div = 1 / 24 |
| 218 | + |
| 219 | + # Parse extended precision pairs if present |
| 220 | + {lon, lat, width, height} = |
| 221 | + decode_extended_pairs( |
| 222 | + grid_reference, |
| 223 | + base_lon, |
| 224 | + base_lat, |
| 225 | + base_lon_div, |
| 226 | + base_lat_div |
| 227 | + ) |
| 219 228 | |
| 220 229 | %DecodeResult{latitude: lat, longitude: lon, width: width, height: height} |
| 221 230 | end |
| @@ -284,17 +293,22 @@ defmodule Gridsquare do | |
| 284 293 | """ |
| 285 294 | @spec distance_between(grid_reference(), grid_reference()) :: distance_result() |
| 286 295 | def distance_between(grid_ref1, grid_ref2) when is_binary(grid_ref1) and is_binary(grid_ref2) do |
| 296 | + # Use full-precision centers for distance calculation |
| 287 297 | grid1 = new(grid_ref1) |
| 288 298 | grid2 = new(grid_ref2) |
| 289 299 | |
| 290 300 | c1 = grid1.center |
| 291 301 | c2 = grid2.center |
| 292 | - w_deg = grid1.width |
| 293 | - h_deg = grid1.height |
| 294 302 | |
| 295 | - # Calculate deltas in longitude and latitude (in grid units) |
| 296 | - lon_delta = abs(c1.longitude - c2.longitude) |
| 297 | - lat_delta = abs(c1.latitude - c2.latitude) |
| 303 | + # Normalize to 6-char subsquare level for adjacency detection |
| 304 | + sub1 = new(String.slice(grid_ref1, 0, 6)) |
| 305 | + sub2 = new(String.slice(grid_ref2, 0, 6)) |
| 306 | + w_deg = sub1.width |
| 307 | + h_deg = sub1.height |
| 308 | + |
| 309 | + # Calculate deltas using subsquare-level centers for adjacency detection |
| 310 | + lon_delta = abs(sub1.center.longitude - sub2.center.longitude) |
| 311 | + lat_delta = abs(sub1.center.latitude - sub2.center.latitude) |
| 298 312 | |
| 299 313 | calculate_distance_result(c1, c2, lon_delta, lat_delta, w_deg, h_deg) |
| 300 314 | end |
| @@ -423,32 +437,7 @@ defmodule Gridsquare do | |
| 423 437 | end |
| 424 438 | |
| 425 439 | @spec from_base18(base18_char()) :: field_index() |
| 426 | - defp from_base18(char) |
| 427 | - when char in [ |
| 428 | - "A", |
| 429 | - "B", |
| 430 | - "C", |
| 431 | - "D", |
| 432 | - "E", |
| 433 | - "F", |
| 434 | - "G", |
| 435 | - "H", |
| 436 | - "I", |
| 437 | - "J", |
| 438 | - "K", |
| 439 | - "L", |
| 440 | - "M", |
| 441 | - "N", |
| 442 | - "O", |
| 443 | - "P", |
| 444 | - "Q", |
| 445 | - "R" |
| 446 | - ] do |
| 447 | - Enum.find_index( |
| 448 | - ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R"], |
| 449 | - &(&1 == char) |
| 450 | - ) |
| 451 | - end |
| 440 | + defp from_base18(<<c>>) when c in ?A..?R, do: c - ?A |
| 452 441 | |
| 453 442 | @spec to_base24(subsquare_index()) :: base24_char() |
| 454 443 | defp to_base24(n) when n >= 0 and n < 24 do |
| @@ -456,63 +445,7 @@ defmodule Gridsquare do | |
| 456 445 | end |
| 457 446 | |
| 458 447 | @spec from_base24(base24_char()) :: subsquare_index() |
| 459 | - defp from_base24(char) |
| 460 | - when char in [ |
| 461 | - "A", |
| 462 | - "B", |
| 463 | - "C", |
| 464 | - "D", |
| 465 | - "E", |
| 466 | - "F", |
| 467 | - "G", |
| 468 | - "H", |
| 469 | - "I", |
| 470 | - "J", |
| 471 | - "K", |
| 472 | - "L", |
| 473 | - "M", |
| 474 | - "N", |
| 475 | - "O", |
| 476 | - "P", |
| 477 | - "Q", |
| 478 | - "R", |
| 479 | - "S", |
| 480 | - "T", |
| 481 | - "U", |
| 482 | - "V", |
| 483 | - "W", |
| 484 | - "X" |
| 485 | - ] do |
| 486 | - Enum.find_index( |
| 487 | - [ |
| 488 | - "A", |
| 489 | - "B", |
| 490 | - "C", |
| 491 | - "D", |
| 492 | - "E", |
| 493 | - "F", |
| 494 | - "G", |
| 495 | - "H", |
| 496 | - "I", |
| 497 | - "J", |
| 498 | - "K", |
| 499 | - "L", |
| 500 | - "M", |
| 501 | - "N", |
| 502 | - "O", |
| 503 | - "P", |
| 504 | - "Q", |
| 505 | - "R", |
| 506 | - "S", |
| 507 | - "T", |
| 508 | - "U", |
| 509 | - "V", |
| 510 | - "W", |
| 511 | - "X" |
| 512 | - ], |
| 513 | - &(&1 == char) |
| 514 | - ) |
| 515 | - end |
| 448 | + defp from_base24(<<c>>) when c in ?A..?X, do: c - ?A |
| 516 449 | |
| 517 450 | @spec add_extended_precision(coordinates(), precision()) :: grid_reference() |
| 518 451 | defp add_extended_precision( |
| @@ -538,41 +471,99 @@ defmodule Gridsquare do | |
| 538 471 | current_lon = lon - (-180 + field_lon * 20 + square_lon * 2 + subsquare_lon * (2 / 24)) |
| 539 472 | current_lat = lat - (-90 + field_lat * 10 + square_lat + subsquare_lat * (1 / 24)) |
| 540 473 | |
| 541 | - {extended, _} = |
| 542 | - Enum.reduce(1..remaining_pairs, {base_reference, {current_lon, current_lat}}, fn i, |
| 543 | - {acc, |
| 544 | - {curr_lon, |
| 545 | - curr_lat}} -> |
| 546 | - if rem(i, 2) == 1 do |
| 547 | - # Base 10 pair |
| 548 | - lon_divisor = 2 / (24 * :math.pow(10, div(i - 1, 2))) |
| 549 | - lat_divisor = 1 / (24 * :math.pow(10, div(i - 1, 2))) |
| 474 | + # Start with subsquare divisors and chain through each pair |
| 475 | + init_lon_div = 2 / 24 |
| 476 | + init_lat_div = 1 / 24 |
| 550 477 | |
| 551 | - extended_lon = trunc(curr_lon / lon_divisor) |
| 552 | - extended_lat = trunc(curr_lat / lat_divisor) |
| 478 | + {extended, _, _} = |
| 479 | + Enum.reduce( |
| 480 | + 1..remaining_pairs, |
| 481 | + {base_reference, {current_lon, current_lat}, {init_lon_div, init_lat_div}}, |
| 482 | + fn i, {acc, {curr_lon, curr_lat}, {prev_lon_div, prev_lat_div}} -> |
| 483 | + if rem(i, 2) == 1 do |
| 484 | + # Base 10 pair - subdivide previous level by 10 |
| 485 | + lon_divisor = prev_lon_div / 10 |
| 486 | + lat_divisor = prev_lat_div / 10 |
| 553 487 | |
| 554 | - new_lon = curr_lon - extended_lon * lon_divisor |
| 555 | - new_lat = curr_lat - extended_lat * lat_divisor |
| 488 | + extended_lon = min(trunc(curr_lon / lon_divisor), 9) |
| 489 | + extended_lat = min(trunc(curr_lat / lat_divisor), 9) |
| 556 490 | |
| 557 | - {acc <> "#{extended_lon}#{extended_lat}", {new_lon, new_lat}} |
| 558 | - else |
| 559 | - # Base 24 pair |
| 560 | - lon_divisor = 2 / (24 * :math.pow(10, div(i - 1, 2)) * 24) |
| 561 | - lat_divisor = 1 / (24 * :math.pow(10, div(i - 1, 2)) * 24) |
| 491 | + new_lon = curr_lon - extended_lon * lon_divisor |
| 492 | + new_lat = curr_lat - extended_lat * lat_divisor |
| 562 493 | |
| 563 | - extended_lon = trunc(curr_lon / lon_divisor) |
| 564 | - extended_lat = trunc(curr_lat / lat_divisor) |
| 494 | + {acc <> "#{extended_lon}#{extended_lat}", {new_lon, new_lat}, |
| 495 | + {lon_divisor, lat_divisor}} |
| 496 | + else |
| 497 | + # Base 24 pair - subdivide previous level by 24 |
| 498 | + lon_divisor = prev_lon_div / 24 |
| 499 | + lat_divisor = prev_lat_div / 24 |
| 565 500 | |
| 566 | - new_lon = curr_lon - extended_lon * lon_divisor |
| 567 | - new_lat = curr_lat - extended_lat * lat_divisor |
| 501 | + extended_lon = min(trunc(curr_lon / lon_divisor), 23) |
| 502 | + extended_lat = min(trunc(curr_lat / lat_divisor), 23) |
| 568 503 | |
| 569 | - {acc <> "#{to_base24(extended_lon)}#{to_base24(extended_lat)}", {new_lon, new_lat}} |
| 504 | + new_lon = curr_lon - extended_lon * lon_divisor |
| 505 | + new_lat = curr_lat - extended_lat * lat_divisor |
| 506 | + |
| 507 | + {acc <> "#{to_base24(extended_lon)}#{to_base24(extended_lat)}", {new_lon, new_lat}, |
| 508 | + {lon_divisor, lat_divisor}} |
| 509 | + end |
| 570 510 | end |
| 571 | - end) |
| 511 | + ) |
| 572 512 | |
| 573 513 | extended |
| 574 514 | end |
| 575 515 | |
| 516 | + @spec decode_extended_pairs(String.t(), float(), float(), float(), float()) :: |
| 517 | + {float(), float(), float(), float()} |
| 518 | + defp decode_extended_pairs(grid_reference, base_lon, base_lat, lon_div, lat_div) do |
| 519 | + len = String.length(grid_reference) |
| 520 | + |
| 521 | + if len <= 6 do |
| 522 | + # No extended pairs - center within subsquare |
| 523 | + {base_lon + lon_div / 2, base_lat + lat_div / 2, lon_div, lat_div} |
| 524 | + else |
| 525 | + remaining = String.slice(grid_reference, 6, len - 6) |
| 526 | + |
| 527 | + pairs = |
| 528 | + for i <- 0..(div(String.length(remaining), 2) - 1), |
| 529 | + do: String.slice(remaining, i * 2, 2) |
| 530 | + |
| 531 | + {final_lon, final_lat, final_lon_div, final_lat_div} = |
| 532 | + Enum.reduce( |
| 533 | + Enum.with_index(pairs, 1), |
| 534 | + {base_lon, base_lat, lon_div, lat_div}, |
| 535 | + &decode_pair/2 |
| 536 | + ) |
| 537 | + |
| 538 | + # Center within the final grid cell |
| 539 | + {final_lon + final_lon_div / 2, final_lat + final_lat_div / 2, final_lon_div, |
| 540 | + final_lat_div} |
| 541 | + end |
| 542 | + end |
| 543 | + |
| 544 | + defp decode_pair({pair, i}, {curr_lon, curr_lat, prev_lon_div, prev_lat_div}) |
| 545 | + when rem(i, 2) == 1 do |
| 546 | + # Base 10 pair |
| 547 | + curr_lon_div = prev_lon_div / 10 |
| 548 | + curr_lat_div = prev_lat_div / 10 |
| 549 | + val_lon = String.to_integer(String.at(pair, 0)) |
| 550 | + val_lat = String.to_integer(String.at(pair, 1)) |
| 551 | + |
| 552 | + {curr_lon + val_lon * curr_lon_div, curr_lat + val_lat * curr_lat_div, curr_lon_div, |
| 553 | + curr_lat_div} |
| 554 | + end |
| 555 | + |
| 556 | + defp decode_pair({pair, _i}, {curr_lon, curr_lat, prev_lon_div, prev_lat_div}) do |
| 557 | + # Base 24 pair |
| 558 | + curr_lon_div = prev_lon_div / 24 |
| 559 | + curr_lat_div = prev_lat_div / 24 |
| 560 | + val_lon = from_base24(String.at(pair, 0)) |
| 561 | + val_lat = from_base24(String.at(pair, 1)) |
| 562 | + |
| 563 | + {curr_lon + val_lon * curr_lon_div, curr_lat + val_lat * curr_lat_div, curr_lon_div, |
| 564 | + curr_lat_div} |
| 565 | + end |
| 566 | + |
| 576 567 | @spec format_grid_reference(grid_reference()) :: grid_reference() |
| 577 568 | defp format_grid_reference(extended_reference) do |
| 578 569 | if String.length(extended_reference) == 6 do |
| @@ -586,23 +577,24 @@ defmodule Gridsquare do | |
| 586 577 | @spec normalize_coordinates(longitude(), latitude()) :: |
| 587 578 | {normalized_longitude(), normalized_latitude()} |
| 588 579 | defp normalize_coordinates(lon, lat) do |
| 589 | - # Normalize longitude to -180 to 180 range |
| 590 | - cond_result = |
| 591 | - cond do |
| 592 | - lon < -180 -> lon + 360 |
| 593 | - lon > 180 -> lon - 360 |
| 594 | - true -> lon |
| 595 | - end |
| 580 | + # Normalize longitude to -180 to 180 range using modular arithmetic |
| 581 | + temp = :math.fmod(lon + 180, 360) |
| 582 | + temp = if temp < 0, do: temp + 360, else: temp |
| 583 | + normalized_lon = temp - 180 |
| 596 584 | |
| 585 | + # fmod maps both 180.0 and -180.0 to -180.0; preserve the convention |
| 586 | + # that positive 180.0 input maps to 179.999999 (eastern edge) |
| 597 587 | normalized_lon = |
| 598 | - then(cond_result, fn l -> |
| 599 | - if l == 180, do: 179.999999, else: l |
| 600 | - end) |
| 588 | + cond do |
| 589 | + normalized_lon == -180.0 and lon > 0 -> 179.999999 |
| 590 | + normalized_lon == 180.0 -> 179.999999 |
| 591 | + true -> normalized_lon |
| 592 | + end |
| 601 593 | |
| 602 594 | # Clamp latitude to just below 90 |
| 603 595 | normalized_lat = |
| 604 596 | cond do |
| 605 | - lat < -90 -> -90 |
| 597 | + lat < -90 -> -90.0 |
| 606 598 | lat >= 90 -> 89.999999 |
| 607 599 | true -> lat |
| 608 600 | end |
| @@ -1,7 +1,7 @@ | |
| 1 1 | defmodule Gridsquare.MixProject do |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 | - @version "0.2.0" |
| 4 | + @version "0.3.0" |
| 5 5 | @source_url "https://github.com/gmcintire/gridsquare" |
| 6 6 | |
| 7 7 | def project do |
| @@ -42,7 +42,7 @@ defmodule Gridsquare.MixProject do | |
| 42 42 | # Run "mix help deps" to learn about dependencies. |
| 43 43 | defp deps do |
| 44 44 | [ |
| 45 | - {:styler, "~> 1.4.2", only: [:dev, :test], runtime: false}, |
| 45 | + {:styler, "~> 1.10", only: [:dev, :test], runtime: false}, |
| 46 46 | {:mix_test_watch, "~> 1.1", only: [:dev, :test]}, |
| 47 47 | {:dialyxir, "~> 1.0", only: :dev, runtime: false}, |
| 48 48 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, |