begone, line!
I set the political svgs to remove the divide thru russia at the antimeridian. I'm a bit surprised I didn't try this sooner since it's just kind of an ugly erroneus line in most of my map projections. unfortunately, the solution I found doesn't work for the physical maps. that's a bit of a bummer for the tissot ellipse maps specificly. however, it was most notable (and most potentially confusing) in the blank political templates, so I'm very glad that I fixd that.
|
Before Width: | Height: | Size: 27 MiB After Width: | Height: | Size: 27 MiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
@ -115,7 +115,7 @@ def main():
|
||||
' <g transform="matrix(1,0,0,-1,180,90)">\n'
|
||||
' <rect class="water" x="-180" y="-90" width="360" height="180" />\n'
|
||||
' <g class="country">\n'
|
||||
+ plot_political_shapes('ne_50m_admin_0_countries', trim_antarctica=True) +
|
||||
+ plot_political_shapes('ne_50m_admin_0_countries', trim_antarctica=True, fuse_russia=True) +
|
||||
' </g>\n'
|
||||
' <g class="water">\n'
|
||||
+ plot_shapes('ne_50m_lakes', max_rank=4) +
|
||||
@ -130,10 +130,10 @@ def main():
|
||||
' <g transform="matrix(1,0,0,-1,180,90)">\n'
|
||||
' <rect class="water" x="-180" y="-90" width="360" height="180" />\n'
|
||||
' <g class="country">\n'
|
||||
+ plot_political_shapes('ne_110m_admin_0_countries', trim_antarctica=True, add_title=True,
|
||||
mode="normal")
|
||||
+ plot_political_shapes('ne_110m_admin_0_countries', trim_antarctica=True, add_title=True,
|
||||
mode="circle", include_circles_from='ne_10m_admin_0_countries') +
|
||||
+ plot_political_shapes('ne_110m_admin_0_countries', trim_antarctica=True, fuse_russia=True,
|
||||
add_title=True, mode="normal")
|
||||
+ plot_political_shapes('ne_110m_admin_0_countries', trim_antarctica=True, fuse_russia=True,
|
||||
add_title=True, mode="circle", include_circles_from='ne_10m_admin_0_countries') +
|
||||
' </g>\n'
|
||||
' <g class="water">\n'
|
||||
+ plot_shapes('ne_110m_lakes', max_rank=4) +
|
||||
@ -151,16 +151,17 @@ def main():
|
||||
' <g transform="matrix(1,0,0,-1,180,90)">\n'
|
||||
' <rect class="water" x="-180" y="-90" width="360" height="180" />\n'
|
||||
' <g class="province">\n'
|
||||
+ plot_political_shapes('ne_50m_admin_1_states_provinces', trim_antarctica=True, add_title=True,
|
||||
filter_field="adm0_a3", filter_values=a3_with_provinces) +
|
||||
+ plot_political_shapes('ne_50m_admin_1_states_provinces', trim_antarctica=True, fuse_russia=True,
|
||||
add_title=True, filter_field="adm0_a3", filter_values=a3_with_provinces) +
|
||||
' </g>\n'
|
||||
' <g class="country-outline">\n'
|
||||
+ plot_shapes('ne_50m_admin_0_countries', trim_antarctica=True,
|
||||
+ plot_shapes('ne_50m_admin_0_countries', trim_antarctica=True, fuse_russia=True,
|
||||
filter_field="adm0_a3", filter_values=a3_with_provinces) +
|
||||
' </g>\n'
|
||||
' <g class="country">\n'
|
||||
+ plot_political_shapes('ne_50m_admin_0_countries', trim_antarctica=True, add_title=True,
|
||||
filter_field="adm0_a3", filter_values=a3_with_provinces, filter_mode="out") +
|
||||
+ plot_political_shapes('ne_50m_admin_0_countries', trim_antarctica=True, fuse_russia=True,
|
||||
add_title=True, filter_field="adm0_a3", filter_values=a3_with_provinces,
|
||||
filter_mode="out") +
|
||||
' </g>\n'
|
||||
' <g class="water">\n'
|
||||
+ plot_shapes('ne_50m_lakes', max_rank=4) +
|
||||
@ -174,10 +175,12 @@ def main():
|
||||
' <g transform="matrix(1,0,0,-1,180,90)">\n'
|
||||
' <rect class="water" x="-180" y="-90" width="360" height="180" />\n'
|
||||
' <g class="country">\n'
|
||||
+ plot_political_shapes('ne_10m_admin_0_countries', trim_antarctica=True, mode="normal") +
|
||||
+ plot_political_shapes('ne_10m_admin_0_countries', trim_antarctica=True, fuse_russia=True,
|
||||
mode="normal") +
|
||||
' </g>\n'
|
||||
' <g class="thick-country-border">\n'
|
||||
+ plot_political_shapes('ne_10m_admin_0_countries', trim_antarctica=True, mode="trace") +
|
||||
+ plot_political_shapes('ne_10m_admin_0_countries', trim_antarctica=True, fuse_russia=True,
|
||||
mode="trace") +
|
||||
' </g>\n'
|
||||
' <g class="river">\n'
|
||||
+ plot_shapes('ne_10m_rivers_lake_centerlines', max_rank=5) +
|
||||
@ -193,7 +196,7 @@ def main():
|
||||
+ plot_shapes('ne_10m_admin_0_boundary_lines_disputed_areas') +
|
||||
' </g>\n'
|
||||
' <g class="coastline">\n'
|
||||
+ plot_shapes('ne_10m_coastline', trim_antarctica=True) +
|
||||
+ plot_shapes('ne_10m_coastline', trim_antarctica=True, fuse_russia=True) +
|
||||
' </g>\n'
|
||||
' <g class="lake">\n'
|
||||
+ plot_shapes('ne_10m_lakes', max_rank=4) +
|
||||
|
||||
@ -6,7 +6,7 @@ from numpy import pi, sqrt, cos, radians, inf
|
||||
from shapely import Polygon
|
||||
|
||||
from helpers import plot, trim_edges, ShapeRecord, \
|
||||
load_shapes_from_one_place_and_records_from_another, load_shaperecords
|
||||
load_shapes_from_one_place_and_records_from_another, load_shaperecords, fuse_edges
|
||||
|
||||
SIZE_CLASSES = [
|
||||
'lg', 'md', 'sm', None, None, None]
|
||||
@ -49,7 +49,8 @@ ISO_A3_TO_A2 = {
|
||||
|
||||
|
||||
def plot_political_shapes(filename, mode="normal",
|
||||
trim_antarctica=False, add_title=False, include_circles_from=None,
|
||||
trim_antarctica=False, fuse_russia=False,
|
||||
add_title=False, include_circles_from=None,
|
||||
filter_field: Optional[str] = None,
|
||||
filter_values: Optional[list[Any]] = None, filter_mode="in") -> str:
|
||||
""" it's like plot_shapes but it also can make circles and handles, like, dependencies and stuff
|
||||
@ -59,6 +60,7 @@ def plot_political_shapes(filename, mode="normal",
|
||||
ID and clip to that existing shape, or
|
||||
"circle" to do circles at the center of mass specificly for small countries
|
||||
:param trim_antarctica: whether to adjust antarctica's shape
|
||||
:param fuse_russia: whether to reattach the bit of Russia that's in the western hemisphere
|
||||
:param add_title: add mouseover text
|
||||
:param include_circles_from: an additional filename from which to borrow small objects. anything
|
||||
present in this shapefile but not in the main one will be included
|
||||
@ -91,8 +93,11 @@ def plot_political_shapes(filename, mode="normal",
|
||||
|
||||
# if it Antarctica, trim it
|
||||
if trim_antarctica:
|
||||
if record["sov_a3"] == 'ATA':
|
||||
shape.points = trim_edges(shape.points, shape.parts)
|
||||
if record["sov_a3"] == "ATA":
|
||||
shape.points, shape.parts = trim_edges(shape.points, shape.parts)
|
||||
if fuse_russia:
|
||||
if record["sov_a3"] == "RUS":
|
||||
shape.points, shape.parts = fuse_edges(shape.points, shape.parts)
|
||||
sovereign_code = record["sov_a3"]
|
||||
if sovereign_code in SOVEREIGN_CODES:
|
||||
sovereign_code = SOVEREIGN_CODES[sovereign_code] # convert US1 to USA
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
# read data from a shapefile into SVG format
|
||||
|
||||
from helpers import plot, trim_edges, lengthen_edges, load_shaperecords
|
||||
from helpers import plot, trim_edges, lengthen_edges, load_shaperecords, fuse_edges
|
||||
|
||||
|
||||
def plot_shapes(
|
||||
filename, max_rank=float('inf'), clazz=None,
|
||||
trim_antarctica=False, flesh_out_antarctica=False, mark_antarctica=False,
|
||||
filter_field=None, filter_values=None) -> str:
|
||||
trim_antarctica=False, flesh_out_antarctica=False, fuse_russia=False,
|
||||
mark_antarctica=False, filter_field=None, filter_values=None) -> str:
|
||||
result = ""
|
||||
for shape, record in load_shaperecords(filename):
|
||||
if filter_field is not None:
|
||||
@ -27,7 +27,7 @@ def plot_shapes(
|
||||
is_antarctica = shape.points[0][1] < -60
|
||||
if is_antarctica:
|
||||
if trim_antarctica:
|
||||
shape.points = trim_edges(shape.points, shape.parts)
|
||||
shape.points, shape.parts = trim_edges(shape.points, shape.parts)
|
||||
elif flesh_out_antarctica:
|
||||
shape.points = lengthen_edges(shape.points)
|
||||
if mark_antarctica:
|
||||
@ -35,6 +35,8 @@ def plot_shapes(
|
||||
clazz_for_this_section = "antarctic"
|
||||
elif "antarctic" not in clazz_for_this_section:
|
||||
clazz_for_this_section += " antarctic"
|
||||
if fuse_russia:
|
||||
shape.points, shape.parts = fuse_edges(shape.points, shape.parts)
|
||||
|
||||
result += plot(shape.points, midx=shape.parts, close=False,
|
||||
clazz=clazz_for_this_section, fourmat='xd')
|
||||
|
||||
@ -4,7 +4,8 @@ import random as rng
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
import shapefile
|
||||
from numpy import pi, sin, cos, tan, arcsin, arccos, degrees, ceil, radians, arctan2, hypot
|
||||
from matplotlib import pyplot as plt
|
||||
from numpy import pi, sin, cos, tan, arcsin, arccos, degrees, ceil, radians, arctan2, hypot, cumsum, transpose
|
||||
|
||||
|
||||
def load_shaperecords(filename) -> Iterable[ShapeRecord]:
|
||||
@ -125,15 +126,67 @@ def plot(coords: list[tuple[float, float]], midx: Optional[list[int]] = None, cl
|
||||
def trim_edges(coast, coast_parts):
|
||||
"""remove the extra points placed along the edges of the Plate Carree map"""
|
||||
for i in range(len(coast)-1, -1, -1):
|
||||
if i in coast_parts:
|
||||
continue
|
||||
x0, y0 = coast[i-1] if i > 0 else coast[i]
|
||||
x1, y1 = coast[i]
|
||||
x2, y2 = coast[i+1] if i < len(coast)-1 else coast[i]
|
||||
if (i not in coast_parts) and (abs(x0) > 179.99 or abs(y0) > 89.99) and (abs(x1) > 179.99 or abs(y1) > 89.99) and (abs(x2) > 179.99 or abs(y2) > 89.99):
|
||||
if (abs(x0) > 179.99 or abs(y0) > 89.99) and \
|
||||
(abs(x1) > 179.99 or abs(y1) > 89.99) and \
|
||||
(abs(x2) > 179.99 or abs(y2) > 89.99):
|
||||
coast.pop(i)
|
||||
for j in range(len(coast_parts)):
|
||||
if coast_parts[j] > i:
|
||||
coast_parts[j] -= 1
|
||||
return coast
|
||||
return coast, coast_parts
|
||||
|
||||
|
||||
def fuse_edges(points: list[tuple[float, float]], part_indices: list[int]) -> tuple[list[tuple[float, float]], list[int]]:
|
||||
"""look for parts with matching vertices on either side of the antimeridian and combine them"""
|
||||
# first, we must apply the trimming algorithm.
|
||||
points, parts = trim_edges(points, part_indices)
|
||||
# break the data up into the individual parts
|
||||
parts = []
|
||||
for i in range(len(part_indices)):
|
||||
start = part_indices[i]
|
||||
end = part_indices[i + 1] if i + 1 < len(part_indices) else len(points)
|
||||
parts.append(points[start:end])
|
||||
if points[start] == points[end - 1]:
|
||||
parts[-1] = parts[-1][:-1] # also remove the duplicate endpoint
|
||||
|
||||
# look for pairs of parts with matching antimeridianal points, and stitch them together
|
||||
parts = fuse_edges_of_parts(parts)
|
||||
|
||||
# put it all back into a single list of points and a list of moveto indices
|
||||
for part in parts:
|
||||
part.append(part[0]) # don't forget to undo the part where we removed the duplicate endpoint
|
||||
points = sum(parts, start=[])
|
||||
part_indices = cumsum([len(part) for part in parts])
|
||||
part_indices = [0] + list(part_indices[:-1])
|
||||
return points, part_indices
|
||||
|
||||
|
||||
def fuse_edges_of_parts(parts: list[list[tuple[float, float]]]) -> list[list[tuple[float, float]]]:
|
||||
"""look for parts with matching vertices on either side of the antimeridian and combine them"""
|
||||
for i in range(len(parts)):
|
||||
# look at every point in part i
|
||||
for j in range(0, len(parts[i])):
|
||||
# search for any that are on the antimeridian, and after another on the antimeridian
|
||||
if abs(parts[i][j][0]) > 179.99 and abs(parts[i][j - 1][0]) > 179.99:
|
||||
for k in range(i):
|
||||
# look at every point in part k
|
||||
for l in range(len(parts[k])):
|
||||
# search for any that are on the antimeridian, and after another on the antimeridian
|
||||
if abs(parts[k][l][0]) > 179.99 and abs(parts[k][l - 1][0]) > 179.99:
|
||||
# if the two points' latitudes match
|
||||
if abs(parts[i][j - 1][1] - parts[k][l][1]) < 0.01:
|
||||
# do the stitching
|
||||
parts[i] = parts[i][:j] + parts[k][l:] + parts[k][:l] + parts[i][j:]
|
||||
parts.pop(k)
|
||||
# recurse in case we need to stitch together a few in a row
|
||||
return fuse_edges_of_parts(parts)
|
||||
# return the unmodified list if we found absolutely noting to fuse
|
||||
return parts
|
||||
|
||||
|
||||
def lengthen_edges(coast):
|
||||
|
||||