Table of Contents
Target
How often do you have to do a key map, something like this?
There is an interactive version, also, showing added goodies of zoom, re-centering, re-scaling all based on the leaflet Javascript library.
Pattern work
Hand editing HTML
is a PITA and that hasn't changed much in the 35+ years since I started doing it with the vi
editor. The good news is that it has always been possible to tame the problem by isolating the static portion that doesn't change every time from the few, frequently changing portions.
What stays the same
All the usual scaffolding
<document_content>
<!DOCTYPE html>
<html>
<head>
<title>Leaflet Template</title>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js">
</script>
<style>
.flex-container {
display: flex;
align-items: flex-start; /* Align items at the start of the container */
}
#map {
width: 500px;
height: 580px;
margin-right: 20px; /* Add some space between the map and the tables */
}
.tables-container {
display: flex;
flex-wrap: wrap; /* Allow tables to wrap if there's not enough space */
gap: 10px; /* Space between tables */
}
table {
border-collapse: collapse;
width: 200px; /* Adjust based on your preference */
}
th, td {
border: 1px solid black;
padding: 8px;
text-align: right;
}
</style>
</head>
…
</script>
</body>
</html>
</document_content>
What changes
A few lines like this
addConcentricCircles([addConcentricCircles([41.258611111, -95.9375], radii, colors);
], radii, colors);
How to isolate
The spot
The purpose of map illustration is to show a point of interest and its distance from others.
from = "Omaha"
Locating the POI is done with specifying latitude and longitude. For example, Omaha is
centerpoint = "41.258611111, -95.9375"
The bands
bands = "50, 100, 200, 300, 400"
band_colors = "'Red', 'Green', 'Yellow', 'Blue', 'Purple'"
How to insert
String interpolation
We've all done this in word processing:
Dear SALUTATION:
in a boilerplate (form) document and then do a search and replace.
In programming, however, we define string variables
centerpoint = "41.258611111, 95.9375"
from = "Omaha"
bands = "50, 100, 200, 300, 400"
band_colors = "'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF']"
that we can use as placeholder variables in other strings
snippet = " The coordinates of $from are $centerpoint"
from which we will get
The coordinates of Ohama are 41.258611111, 95.9375
Where to insert it
In a script, of course, in any language that does string interpolation. According to my buddy Claude these include
Groovy:
def name = "John"
def age = 25
println "My name is $name and I'm $age years old."
Perl:
my $name = "John";
my $age = 25;
print "My name is $name and I'm $age years old.";
PHP:
$name = "John";
$age = 25;
echo "My name is $name and I'm $age years old.";
Scala:
val name = "John"
val age = 25
println(s"My name is $name and I'm $age years old.")
Kotlin:
val name = "John"
val age = 25
println("My name is $name and I'm $age years old.")
Swift:
let name = "John"
let age = 25
print("My name is \(name) and I'm \(age) years old.")
C# (using string interpolation):
string name = "John";
int age = 25;
Console.WriteLine($"My name is {name} and I'm {age} years old.");
Ruby:
name = "John"
age = 25
puts "My name is #{name} and I'm #{age} years old."
JavaScript (using template literals):
const name = "John";
const age = 25;
console.log(`My name is ${name} and I'm ${age} years old.`);
Python (using f-strings):
name = "John"
age = 25
print(f"My name is {name} and I'm {age} years old.")
And my favorite
from * " is located at " * coords
"Omaha is located at 41° 15′ 31″ N, 95° 56′ 15″ W"
and bash
is also good.
So, the script
centerpoint = "41.258611111, 95.9375"
from = "Omaha"
bands = "50, 100, 200, 300, 400"
band_colors = "'#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF']"
include("partials.jl")
where partials.jl
is a separate file with the template that contains all of the constant stuff, with `"something = $centerpoint".
bullseye =
"""
<document_content>
<!DOCTYPE html>
<html>
<head>
<title>Leaflet Template</title>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css" />
<script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js">
</script>
<style>
.flex-container {
display: flex;
align-items: flex-start; /* Align items at the start of the container */
}
#map {
width: 500px;
height: 580px;
margin-right: 20px; /* Add some space between the map and the tables */
}
.tables-container {
display: flex;
flex-wrap: wrap; /* Allow tables to wrap if there's not enough space */
gap: 10px; /* Space between tables */
}
table {
border-collapse: collapse;
width: 200px; /* Adjust based on your preference */
}
th, td {
border: 1px solid black;
padding: 8px;
text-align: right;
}
</style>
</head>
<body>
<div class="flex-container">
<div id="map">
</div>
<div class="tables-container">
</div>
</div>
<script>
// Creating map options
var mapOptions = {
center: [$centerpoint],
zoom: 5
};
var map = new L.map('map', mapOptions);
var layer = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
map.addLayer(layer);
var marker = L.marker([$centerpoint]);
marker.addTo(map);
marker.bindPopup($from).openPopup();
function milesToMeters(miles) {
return miles * 1609.34;
}
var colors = [$band_colors];
var radii = [$bands];
function addConcentricCircles(center, radii, colors) {
radii.forEach(function(radius, index) {
L.circle(center, milesToMeters(radius), {
color: colors[index],
fillColor: colors[index],
fillOpacity: 0
}).addTo(map);
});
}
addConcentricCircles([$centerpoint], radii, colors);
// Adding a legend
var legend = L.control({position: 'bottomleft'}); // Change position to 'bottomleft'
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend'),
labels = ['<strong>Distances</strong>'],
distances = [$bands];
for (var i = 0; i < distances.length; i++) {
div.innerHTML +=
'<i style="background:' + colors[i] + '; width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7;"></i> ' +
distances[i] + (distances[i + 1] ? '–' + distances[i + 1] + ' miles<br>' : '+ miles');
}
return div;
};
legend.addTo(map);
</script>
</body>
</html>
</document_content>
"""
Then, the rest of the script to generate the page and open it in your browser is just
open("omaha.html", "w") do file
write(file, bullseye)
end
file_path = "omaha.html"
run(`open $file_path`)
(This is for macOS; consult your bot for other platform.)
What if?
Ok, you want different colors.
Colors
Your coordinates don't coordinate
Yeah, we need decimal coordinates and if given degrees, minutes, seconds, no joy.
"""
dms_to_decimal(coords::AbstractString) -> AbstractString
Convert latitude and longitude coordinates from degrees, minutes, seconds (DMS) format
to decimal degrees (DD) format as a string.
# Arguments
- `coords`: A string representing the latitude and longitude coordinates in the format
"41° 15′ 31″ N, 95° 56′ 15″ W".
# Returns
- A string containing the latitude and longitude coordinates in decimal degrees format,
separated by a comma, e.g., "41.258611111, -95.9375".
# Example
```julia
coords = "41° 15′ 31″ N, 95° 56′ 15″ W"
result = dms_to_decimal(coords)
println(result) # Output: "41.258611111111111, -95.9375"
"""
function dms_to_decimal(coords::AbstractString)
# Split the input string into latitude and longitude parts
lat_dms, lon_dms = split(coords, ",")
# Function to convert DMS to decimal degrees
function to_decimal(dms::AbstractString)
# Remove any extra whitespace
dms = strip(dms)
# Extract the degree, minute, and second values
deg, min, sec, dir = match(r"(\d+).\s*(\d+)′\s*(\d+(?:\.\d+)?)″\s*([NSEW])", dms).captures
# Convert the values to floats
deg = parse(Float64, deg)
min = parse(Float64, min)
sec = parse(Float64, sec)
# Calculate the decimal degrees
decimal = deg + (min / 60) + (sec / 3600)
# Adjust the sign based on the direction
decimal *= (dir == "S" || dir == "W") ? -1 : 1
return decimal
end
# Convert latitude and longitude to decimal degrees
lat_decimal = to_decimal(lat_dms)
lon_decimal = to_decimal(lon_dms)
# Format the output string
result = "$(lat_decimal), $(lon_decimal)"
return result
end
coords = "41° 15′ 31″ N, 95° 56′ 15″ W"
result = dms_to_decimal(coords)
println(result) # Output: "41.258611111111111, -95.9375"
Aside
If you've worked in other languages, the Julia
is fairly easy to read, but what's up with the function signature?
function dms_to_decimal(coords::AbstractString)
`
Julia is somewhat of an oddball. The function could have been just ducktyped.
function dms_to_decimal(coords)
But on the advice of Claude, I've strictly typed it.
function dms_to_decimal(coords::AbstractString)
And AbstractString
? Here's what he says about that.
In duck-typed languages like Python
or Ruby
, the type of a variable is determined by its behavior rather than being explicitly declared. In other words, if an object quacks like a duck and walks like a duck, it is considered a duck, regardless of its actual type.
In contrast, Julia is a statically-typed language, which means that the types of variables are known at compile-time. However, Julia also supports multiple dispatch, allowing functions to have different behavior based on the types of their arguments.
Now, let's examine the function signature:
function dms_to_decimal(coords::AbstractString)
function
is the keyword used to define a function in Julia.dms_to_decimal
is the name of the function.coords
is the name of the argument that the function takes.::AbstractString
is a type annotation specifying that thecoords
argument must be a subtype ofAbstractString
.
In this case, AbstractString
is an abstract type in Julia that represents any string-like type. It is a supertype of concrete string types such as String
(for ASCII strings) and UTF8String
(for UTF-8 encoded strings).
By specifying coords::AbstractString
, the function signature indicates that the coords
argument must be a string or any subtype of AbstractString
. This provides a way to enforce type constraints on the argument.
For someone coming from a duck-typed language, this type annotation might seem unusual. In duck-typed languages, you would typically just pass an argument to a function without explicitly specifying its type. The function would then operate on the argument based on its expected behavior.
However, in Julia, the type annotation ::AbstractString
serves as a contract or a guarantee that the coords
argument will behave like a string. It allows the Julia compiler to perform type checks and optimizations based on the specified type.
If you try to pass an argument that is not a subtype of AbstractString
to the dms_to_decimal
function, Julia will raise a type error at compile-time or runtime, depending on the situation.
Here's an example of how you would use this function in Julia:
coordinates = "41° 15′ 31″ N, 95° 56′ 15″ W"
result = dms_to_decimal(coordinates)
In this case, the coordinates
variable is a string literal, which is a subtype of AbstractString
, so it can be passed to the dms_to_decimal
function without any issues.
To summarize, the ::AbstractString
type annotation in the function signature specifies that the coords
argument must be a string or any subtype of AbstractString
. This allows Julia to perform type checks and ensures that the function is used correctly with the expected type of argument.