diff --git a/input/Advanced/Supermap.svg b/input/Advanced/Supermap.svg
index 3ae8fd0..3c4abb4 100644
--- a/input/Advanced/Supermap.svg
+++ b/input/Advanced/Supermap.svg
@@ -357,8 +357,8 @@
+
-
@@ -1410,18 +1410,16 @@
+
+
+
+
-
-
-
-
-
-
diff --git a/input/Advanced/Template provinces.svg b/input/Advanced/Template provinces.svg
index a140a4f..0471695 100644
--- a/input/Advanced/Template provinces.svg
+++ b/input/Advanced/Template provinces.svg
@@ -1739,10 +1739,8 @@
-
-
- Georgia
-
+
+ Georgia
diff --git a/src/zupplemental/compose_maps.py b/src/zupplemental/compose_maps.py
index 9348111..cdf0f79 100644
--- a/src/zupplemental/compose_maps.py
+++ b/src/zupplemental/compose_maps.py
@@ -4,7 +4,7 @@
import math
import re
-from generate_political_shapes import plot_political_shapes
+from generate_political_shapes import plot_political_shapes, load_political_shapes
from generate_graticule import generate_graticule
from generate_indicatrices import generate_indicatrices
from generate_orthodromes import generate_orthodromes
@@ -147,7 +147,9 @@ def main():
' \n'
' \n'
' \n'
- + plot_political_shapes('ne_50m_admin_0_countries', trim_antarctica=True, fuse_russia=True) +
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_50m_admin_0_countries', trim_antarctica=True, fuse_russia=True)) +
' \n'
' \n'
+ plot_shapes('ne_50m_lakes', max_rank=4) +
@@ -167,10 +169,15 @@ def main():
' \n'
' \n'
' \n'
- + 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') +
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_110m_admin_0_countries', trim_antarctica=True, fuse_russia=True),
+ add_title=True, mode="normal")
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_110m_admin_0_countries', small_country_filename='ne_10m_admin_0_countries',
+ trim_antarctica=True, fuse_russia=True),
+ add_title=True, mode="circle") +
' \n'
' \n'
+ plot_shapes('ne_110m_lakes', max_rank=4) +
@@ -190,17 +197,22 @@ def main():
' \n'
' \n'
' \n'
- + 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) +
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_50m_admin_1_states_provinces', trim_antarctica=True, fuse_russia=True,
+ filter_field="adm0_a3", filter_values=a3_with_provinces),
+ add_title=True, mode="normal") +
' \n'
' \n'
+ plot_shapes('ne_50m_admin_0_map_units', trim_antarctica=True, fuse_russia=True,
filter_field="adm0_a3", filter_values=a3_with_provinces) +
' \n'
' \n'
- + plot_political_shapes('ne_50m_admin_0_map_units', trim_antarctica=True, fuse_russia=True,
- add_title=True, filter_field="adm0_a3", filter_values=a3_with_provinces,
- filter_mode="out") +
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_50m_admin_0_map_units', trim_antarctica=True, fuse_russia=True,
+ filter_field="adm0_a3", filter_values=a3_with_provinces, filter_mode="out"),
+ add_title=True, mode="normal") +
' \n'
' \n'
+ plot_shapes('ne_50m_lakes', max_rank=4) +
@@ -219,12 +231,16 @@ def main():
' \n'
' \n'
' \n'
- + plot_political_shapes('ne_10m_admin_0_map_units', trim_antarctica=True, fuse_russia=True,
- mode="normal") +
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_10m_admin_0_map_units', trim_antarctica=True, fuse_russia=True),
+ mode="normal") +
' \n'
' \n'
- + plot_political_shapes('ne_10m_admin_0_map_units', trim_antarctica=True, fuse_russia=True,
- mode="trace") +
+ + plot_political_shapes(
+ load_political_shapes(
+ 'ne_10m_admin_0_map_units', trim_antarctica=True, fuse_russia=True),
+ mode="trace") +
' \n'
' \n'
+ plot_shapes('ne_10m_rivers_lake_centerlines', max_rank=5) +
diff --git a/src/zupplemental/generate_political_shapes.py b/src/zupplemental/generate_political_shapes.py
index 7fc4337..747ba43 100644
--- a/src/zupplemental/generate_political_shapes.py
+++ b/src/zupplemental/generate_political_shapes.py
@@ -1,12 +1,11 @@
import re
from typing import Optional, Any
-import shapefile
from numpy import pi, sqrt, cos, radians, inf
+from shapefile import Shape, NULL
from shapely import Polygon
-from helpers import plot, trim_edges, ShapeRecord, \
- load_shapes_from_one_place_and_records_from_another, load_shaperecords, fuse_edges
+from helpers import plot, trim_edges, load_shapes_from_one_place_and_records_from_another, load_shaperecords, fuse_edges
SIZE_CLASSES = [
'lg', 'md', 'sm', None, None, None]
@@ -16,7 +15,14 @@ ISO_A3_CODES_THAT_DONT_NEED_BE_UNIQUE = {
"AU1": "AUS", "CH1": "CHN", "CU1": "CUB", "DN1": "DNK", "FI1": "FIN", "FR1": "FRA",
"IS1": "ISR", "KA1": "KAZ", "GB1": "GBR", "NL1": "NLD", "NZ1": "NZL", "PN1": "PNG",
"PR1": "PRT", "US1": "USA",
- "FXX": "FRA", "NOW": "NOR", "PNX": "PNG", "SRS": "SRB", "SYX": "SYR",
+ "FXX": "FRA", "GEG": "GEO", "NOW": "NOR", "PNX": "PNG", "SRS": "SRB", "SYX": "SYR",
+}
+SUPRANATIONS = {
+ "EUE": {
+ "AUT", "BEL", "BGR", "HRV", "CYP", "CZE", "DNK", "EST", "FIN", "FRA", "DEU", "GRC", "HUN",
+ "IRL", "ITA", "LVA", "LTU", "LUX", "MLT", "NLD", "POL", "PRT", "ROU", "SVK", "SVN", "ESP",
+ "SWE",
+ }
}
ISO_A3_TO_A2 = { # why did I think hard-coding this table was a good idea...
"AFG": "AF", "AGO": "AO", "ALB": "AL", "AND": "AD", "ARE": "AE", "ARG": "AR", "ARM": "AM",
@@ -59,44 +65,34 @@ ISO_A3_TO_A2 = { # why did I think hard-coding this table was a good idea...
"CLP": "CP", "UMI": "UM",
"EUE": "EU",
}
-SUPRANATIONS = {
- "EUE": {
- "AUT", "BEL", "BGR", "HRV", "CYP", "CZE", "DNK", "EST", "FIN", "FRA", "DEU", "GRC", "HUN",
- "IRL", "ITA", "LVA", "LTU", "LUX", "MLT", "NLD", "POL", "PRT", "ROU", "SVK", "SVN", "ESP",
- "SWE",
- }
-}
-def plot_political_shapes(filename, mode="normal",
+def load_political_shapes(filename: str, small_country_filename: Optional[str] = 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
+ filter_field: Optional[str] = None,
+ filter_values: Optional[list[Any]] = None, filter_mode="in"
+ ) -> dict[str, tuple[Shape, Optional[dict[str, Any]], dict]]:
+ """ load the data from a shapefile and arrange the shape-records into a hierarchical dictionary,
+ such that they can be input to plot_political_shapes.
:param filename: the name of the natural earth dataset to use (minus the .shp)
- :param mode: one of "normal" to draw countries' shapes as one would expect,
- "trace" to redraw each border by copying an existing element of the same
- ID and clip to that existing shape, or
- "circle" to do circles at the center of mass specificly for small countries
- "bubble" to also do circles but their area is proportional to population
+ :param small_country_filename: an additional filename from which to borrow small objects. anything
+ present in this shapefile but not in the main one will be included
+ in the output as only a circle (assuming add_circles is True)
: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
- in the output as only a circle (assuming add_circles is True)
:param filter_field: a record key that will be used to select a subset of records to be used
:param filter_values: a list of values that will be compared against each record's
filter_field to determine whether to use it
:param filter_mode: if "in", use only records whose filter_field value is in filter_values.
if "out", use only records whose filter_field value is *not* in filter_values.
"""
- if include_circles_from is None:
+ if small_country_filename is None:
regions = load_shaperecords(filename)
else:
- regions = load_shapes_from_one_place_and_records_from_another(filename, include_circles_from)
+ regions = load_shapes_from_one_place_and_records_from_another(filename, small_country_filename)
+
# go thru the records and sort them into a dictionary by ISO 3166 codes
- hierarchically_arranged_regions: dict[tuple[tuple[str, ...], str], ShapeRecord] = {}
+ world: dict[str, tuple[Shape, Optional[dict[str, Any]], dict]] = {}
for shape, record in regions:
# if it is filtered out, skip it
if filter_field is not None:
@@ -126,165 +122,153 @@ def plot_political_shapes(filename, mode="normal",
record[key] = ISO_A3_CODES_THAT_DONT_NEED_BE_UNIQUE[record[key]]
# then figure out the keying
- sovereign_code = record["sov_a3"]
- country_code = record["adm0_a3"]
- geounit_code = record["gu_a3"]
- unique_identifier = geounit_code
- # key them by sovereign, admin0, geounit
- hierarchical_identifier = (sovereign_code, country_code, geounit_code)
+ key = [record["sov_a3"], record["adm0_a3"], record["gu_a3"]]
+ # put nations under supranations when applicable
+ for supranation_code, member_codes in SUPRANATIONS.items():
+ if key[0] in member_codes:
+ key = [supranation_code] + key
# and then by admin1 if applicable
if "iso_3166_2" in record:
province_code = record["iso_3166_2"]
if province_code.startswith("-99"):
- province_code = "__" + province_code[3:]
+ province_code = key[-1] + province_code[3:]
if province_code.endswith("~"):
province_code = province_code[:-1]
- unique_identifier = record["adm1_code"]
- hierarchical_identifier = hierarchical_identifier + (province_code,)
- # put nations under supranations when applicable
- for code, member_codes in SUPRANATIONS.items():
- if sovereign_code in member_codes:
- hierarchical_identifier = (code,) + hierarchical_identifier
- # remove redundant layers
- for i in range(len(hierarchical_identifier) - 1, 0, -1):
- if hierarchical_identifier[i] == hierarchical_identifier[i - 1]:
- hierarchical_identifier = hierarchical_identifier[:i] + hierarchical_identifier[i + 1:]
- elif hierarchical_identifier[i - 1] in ISO_A3_TO_A2:
- if hierarchical_identifier[i] == ISO_A3_TO_A2[hierarchical_identifier[i - 1]]:
- hierarchical_identifier = hierarchical_identifier[:i] + hierarchical_identifier[i + 1:]
- # key it in this dictionary by both its classes and its id
- key = (hierarchical_identifier, unique_identifier)
- hierarchically_arranged_regions[key] = (shape, record)
+ key = key + [province_code]
- # next, go thru and plot the borders
- current_state = []
- result = ""
- already_titled = set()
- # for each item
- for key in sorted(hierarchically_arranged_regions.keys()):
- hierarchy, identifier = key
- shape, record = hierarchically_arranged_regions[key]
-
- # decide whether it's "small"
- is_small = True
- for i in range(len(shape.parts)):
- if i + 1 < len(shape.parts):
- part = shape.points[shape.parts[i]:shape.parts[i + 1]]
- else:
- part = shape.points[shape.parts[i]:]
- area = Polygon(part).area*cos(radians(part[0][1]))
- if area > pi*CIRCLE_RADIUS**2:
- is_small = False
-
- # make some other decisions
- has_geometry = shape.shapeType != shapefile.NULL
- is_sovereign = len(hierarchy) == 1 or (len(hierarchy) == 2 and hierarchy[0] in SUPRANATIONS)
- is_inhabited = (record.get("pop_est", inf) > 500 and # Vatican is inhabited but US Minor Outlying I. are not
- record["type"] != "Lease" and # don't circle Baykonur or Guantanamo
- "Base" not in record["admin"] and # don't circle military bases
- (record["type"] != "Indeterminate" or record["pop_est"] > 100_000)) # don't circle Cyprus No Mans Land or Siachen Glacier
- if not is_sovereign and "note_adm0" in record and record["note_adm0"] != "":
- label = f"{record['name_long']} ({record['note_adm0']})" # indicate dependencies' sovereigns in parentheses
- elif "name_long" in record:
- label = record["name_long"]
+ # choose an appropriate unique ID
+ if "adm1_code" in record:
+ record["id"] = record["adm1_code"]
else:
- label = record["name"]
+ record["id"] = key[-1]
- # exit any s we're no longer in
- while current_state and (len(current_state) > len(hierarchy) or current_state[-1] != hierarchy[len(current_state) - 1]):
- result += '\t'*(3 + len(current_state)) + f'\n'
- current_state.pop()
- # enter any new s
- while len(current_state) < len(hierarchy):
- current_state.append(hierarchy[len(current_state)])
- clazz = current_state[-1]
- if current_state[-1] in ISO_A3_TO_A2.keys():
- clazz += " " + ISO_A3_TO_A2[clazz]
- elif len(clazz) < 4 and record["iso_a2"] != "-99":
- print(f"warning: no ISO 3166 alpha2 code found for {clazz}: {record['name']}, {record['iso_a2']}")
- result += '\t'*(3 + len(current_state)) + f'\n'
- indentation = '\t'*(4 + len(current_state))
+ # remove redundant layers
+ for i in range(len(key) - 1, 0, -1):
+ if key[i] == key[i - 1]:
+ key.pop(i)
+ elif key[i - 1] in ISO_A3_TO_A2:
+ if key[i] == ISO_A3_TO_A2[key[i - 1]]:
+ key.pop(i)
- # then put in whatever type of content is appropriate:
- any_content = False
- # the normal polygon
- if mode == "normal":
- if has_geometry:
- result += plot(shape.points, midx=shape.parts, close=False,
- fourmat='xd', tabs=4 + len(current_state), ident=identifier)
- any_content = True
- # or the clipped and copied thick border
- elif mode == "trace":
- if has_geometry:
- result += (
- f'{indentation}\n'
- f'{indentation}\n'
- f'{indentation}\n'
- f'{indentation}\n'
- )
- any_content = True
- # or just a circle
- elif mode == "circle":
- if is_small and is_inhabited:
- x_center, y_center = float(record["label_x"]), float(record["label_y"])
- if is_sovereign:
- radius = CIRCLE_RADIUS
+ # place it in the dictionary
+ parent = world
+ for code in key:
+ if code not in parent:
+ parent[code] = (Shape(NULL), None, {})
+ if code != key[-1]:
+ parent = parent[code][2]
+ existing_shape, existing_record, existing_children = parent.get(key[-1])
+ if existing_shape.shapeType != NULL or existing_record is not None:
+ raise ValueError(f"we've already put something at {'/'.join(*key)}")
+ else:
+ parent[key[-1]] = (shape, record, existing_children)
+
+ return world
+
+
+def plot_political_shapes(data: dict[str, tuple[Shape, Optional[dict[str, Any]], dict]],
+ num_tabs=4, is_sovereign=True, mode="normal", add_title=False) -> str:
+ """ it's like plot_shapes but it also can make circles and handles, like, dependencies and stuff
+ :param data: the hierarchicly arranged shape-records to plot. each is keyed with its alpha-3 code,
+ and each value is a tuple of the Shape, the record, and the dict of children. the
+ children dict, if not None, has the same structure as data.
+ :param mode: one of "normal" to draw countries' shapes as one would expect,
+ "trace" to redraw each border by copying an existing element of the same
+ ID and clip to that existing shape, or
+ "circle" to do circles at the center of mass specificly for small countries
+ :param is_sovereign: whether the shapes being plotted at the top level should be treated as sovereign
+ :param num_tabs: the number of tabs to put before each top-level group
+ :param add_title: add mouseover text
+ """
+ # next, go thru and plot the borders
+ result = ""
+ # for each item
+ for key in sorted(data.keys()):
+ shape, record, children = data[key]
+
+ # start the
+ clazz = key
+ if key in ISO_A3_TO_A2.keys():
+ clazz += " " + ISO_A3_TO_A2[clazz]
+ elif len(clazz) < 4 and record is not None and record["iso_a2"] != "-99":
+ print(f"warning: no ISO 3166 alpha2 code found for {clazz}: {record['name']}, {record['iso_a2']}")
+ result += '\t'*num_tabs + f'\n'
+
+ if record is not None:
+ # decide whether it's "small"
+ is_small = True
+ for i in range(len(shape.parts)):
+ if i + 1 < len(shape.parts):
+ part = shape.points[shape.parts[i]:shape.parts[i + 1]]
else:
- radius = round(CIRCLE_RADIUS/sqrt(2), 2)
- result += f'{indentation}\n'
- any_content = True
- # or a circle with its area set to the population
- elif mode == "bubble":
- x_center = record["label_x"] if "label_x" in record else record["longitude"]
- y_center = record["label_y"] if "label_y" in record else record["latitude"]
- area = record["pop_est"] if "pop_est" in record else 1e8
- result += f'{indentation}\n'
- any_content = True
+ part = shape.points[shape.parts[i]:]
+ area = Polygon(part).area*cos(radians(part[0][1]))
+ if area > pi*CIRCLE_RADIUS**2:
+ is_small = False
- # also a title if that's desired
- if add_title and any_content and tuple(hierarchy) not in already_titled:
- if has_geometry or is_inhabited:
- result += f'{indentation}{label}\n'
- already_titled.add(tuple(hierarchy)) # occasionally a thing can get two titles if the hierarchy isn't unique; only label the first one
+ # make some other decisions
+ has_geometry = shape.shapeType != NULL
+ is_inhabited = (record.get("pop_est", inf) > 500 and # Vatican is inhabited but US Minor Outlying I. are not
+ record["type"] != "Lease" and # don't circle Baykonur or Guantanamo
+ "Base" not in record["admin"] and # don't circle military bases
+ (record["type"] != "Indeterminate" or record["pop_est"] > 100_000)) # don't circle Cyprus No Mans Land or Siachen Glacier
+ if not is_sovereign and "note_adm0" in record and record["note_adm0"] != "":
+ label = f"{record['name_long']} ({record['note_adm0']})" # indicate dependencies' sovereigns in parentheses
+ elif "name_long" in record:
+ label = record["name_long"]
+ else:
+ label = record["name"]
+ identifier = record["id"]
- # exit all s before returning
- while len(current_state) > 0:
- result += '\t'*(3 + len(current_state)) + f'\n'
- current_state.pop()
+ # then put in whatever type of content is appropriate:
+ indentation = '\t'*(1 + num_tabs)
+ any_content = False
+ # the normal polygon
+ if mode == "normal":
+ if has_geometry:
+ result += plot(shape.points, midx=shape.parts, close=False,
+ fourmat='xd', tabs=num_tabs + 1, ident=identifier)
+ any_content = True
+ # or the clipped and copied thick border
+ elif mode == "trace":
+ if has_geometry:
+ result += (
+ f'{indentation}\n'
+ f'{indentation}\n'
+ f'{indentation}\n'
+ f'{indentation}\n'
+ )
+ any_content = True
+ # or just a circle
+ elif mode == "circle":
+ if is_small and is_inhabited:
+ x_center, y_center = float(record["label_x"]), float(record["label_y"])
+ if is_sovereign:
+ radius = CIRCLE_RADIUS
+ else:
+ radius = round(CIRCLE_RADIUS/sqrt(2), 2)
+ result += f'{indentation}\n'
+ any_content = True
- # remove any groups with no elements
- for _ in range(3):
- result = re.sub(r'(\t)*(\s*)\n',
- '', result)
+ # also a title if that's desired
+ if add_title and any_content:# and tuple(hierarchy) not in already_titled:
+ if has_geometry or is_inhabited:
+ result += f'{indentation}{label}\n'
+ # already_titled.add(tuple(hierarchy)) # occasionally a thing can get two titles if the hierarchy isn't unique; only label the first one
+
+ # if there are children, plot those recursive-like
+ result += plot_political_shapes(
+ children, num_tabs=num_tabs + 1, is_sovereign=key in SUPRANATIONS,
+ mode=mode, add_title=add_title)
+
+ # exit the
+ result += '\t'*num_tabs + f'\n'
+
+ # # remove any groups with no elements
+ result = re.sub(r'(\t)*(\s*)\n',
+ '', result)
# simplify any groups with only a single element
result = re.sub(r'(\s*)<([a-z]+) ([^\n]*)>(\s*)',
'<\\3 class="\\1" \\4>', result)
return result
-
-
-def complete_sovereign_code_if_necessary(sovereignty_code: str, regions: list[ShapeRecord]) -> str:
- """ so, under the NaturalEarth dataset system, countries are grouped together under
- sovereignties. and each sovereignty has one country within it which is in charge. let's call it
- the sovereign. to encode this, the dataset gives every region a sovereign code and an admin0 code,
- where the admin0 code is from some ISO standard, and the sovereign code = if it's the only region
- under this sovereign { the admin0 code } else { the admin0 code of the sovereign with its last
- letter replaced with a 1 }; I find this kind of weird; I can't find any basis for it in ISO
- standards. I get it, but I would rather the sovereign code just be the same as the admin0 code of
- the sovereign. so that's what this function does; it figures out what letter should go where that
- 1 is.
- """
- sovereign_code = None
- for shape, record in regions:
- if record["adm0_a3"][:2] == sovereignty_code[:2]:
- if sovereign_code is not None:
- if sovereignty_code == "KA1":
- return "KAZ"
- else:
- raise ValueError(f"there are two possible sovereign codes for {sovereignty_code} and I "
- f"don't know which to use: {sovereign_code} and {record['adm0_a3']}")
- else:
- sovereign_code = record['adm0_a3']
- if sovereign_code is None:
- raise ValueError(f"there are no possible sovereign codes for {sovereignty_code}")
- return sovereign_code