mirror of
https://github.com/csharpee/Map-Projections.git
synced 2025-12-08 00:00:14 -05:00
refactor political map generation
I refactord the ploitical map generation. I broke the plot_political_shapes function into two parts: the loading part (which generates a nested dictionary) and the plotting part. this will enable me to process the data between those two steps. I haven't gotten to that yet, but that's my next step in thinning the lines between geounits within countries.
This commit is contained in:
parent
eb2834bc60
commit
793e5e8be6
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 MiB After Width: | Height: | Size: 27 MiB |
@ -1739,10 +1739,8 @@
|
||||
</g>
|
||||
</g>
|
||||
<g class="GEO GE">
|
||||
<g class="GEG">
|
||||
<path id="GEG" d="M43.439,41.107 L43.442,41.126 L43.433,41.156 L43.402,41.177 L43.359,41.190 L43.279,41.185 L43.205,41.199 L43.153,41.236 L43.141,41.265 L43.171,41.288 L43.149,41.307 L43.057,41.353 L42.907,41.467 L42.822,41.492 L42.788,41.564 L42.754,41.579 L42.682,41.586 L42.607,41.579 L42.590,41.571 L42.567,41.559 L42.508,41.470 L42.466,41.440 L42.364,41.454 L42.280,41.475 L42.211,41.487 L42.078,41.494 L41.926,41.496 L41.824,41.432 L41.779,41.441 L41.702,41.472 L41.577,41.497 L41.510,41.517 L41.702,41.705 L41.759,41.817 L41.761,41.885 L41.763,41.970 L41.663,42.147 L41.578,42.398 L41.489,42.659 L41.419,42.738 L41.129,42.828 L41.062,42.931 L40.837,43.063 L40.524,43.121 L40.462,43.146 L40.191,43.312 L39.978,43.420 L40.024,43.485 L40.085,43.553 L40.150,43.570 L40.342,43.543 L40.519,43.512 L40.648,43.534 L40.802,43.480 L40.942,43.418 L41.083,43.374 L41.358,43.333 L41.461,43.276 L41.581,43.219 L42.050,43.190 L42.088,43.199 L42.122,43.207 L42.280,43.228 L42.419,43.224 L42.566,43.155 L42.660,43.159 L42.761,43.170 L42.890,43.133 L42.992,43.092 L43,43.050 L43.089,42.989 L43.348,42.897 L43.558,42.844 L43.623,42.808 L43.783,42.747 L43.799,42.728 L43.795,42.703 L43.750,42.658 L43.738,42.617 L43.760,42.594 L43.826,42.572 L43.957,42.567 L44.005,42.596 L44.103,42.616 L44.200,42.654 L44.329,42.704 L44.506,42.749 L44.576,42.748 L44.644,42.735 L44.692,42.710 L44.771,42.617 L44.850,42.747 L44.871,42.756 L44.943,42.730 L45.072,42.694 L45.160,42.675 L45.208,42.648 L45.344,42.530 L45.563,42.536 L45.656,42.518 L45.705,42.498 L45.728,42.475 L45.688,42.357 L45.634,42.235 L45.639,42.205 L45.727,42.159 L45.846,42.110 L45.910,42.071 L45.954,42.035 L46.048,42.009 L46.160,41.992 L46.213,41.990 L46.268,41.960 L46.412,41.905 L46.430,41.891 L46.405,41.855 L46.348,41.790 L46.303,41.757 L46.252,41.752 L46.202,41.737 L46.184,41.702 L46.182,41.657 L46.191,41.625 L46.204,41.613 L46.255,41.602 L46.305,41.508 L46.385,41.460 L46.509,41.406 L46.619,41.344 L46.673,41.287 L46.662,41.246 L46.626,41.160 L46.534,41.089 L46.458,41.070 L46.431,41.077 L46.381,41.099 L46.280,41.154 L46.171,41.198 L46.087,41.184 L46.031,41.167 L45.922,41.187 L45.793,41.224 L45.725,41.262 L45.696,41.289 L45.716,41.338 L45.422,41.425 L45.281,41.450 L45.217,41.423 L45.001,41.291 L44.976,41.277 L44.811,41.259 L44.811,41.249 L44.849,41.220 L44.841,41.211 L44.565,41.208 L44.473,41.191 L44.227,41.213 L44.146,41.203 L44.077,41.183 L43.909,41.159 L43.793,41.131 L43.645,41.117 L43.492,41.116 Z" />
|
||||
<title>Georgia</title>
|
||||
</g>
|
||||
<path id="GEO" d="M43.439,41.107 L43.442,41.126 L43.433,41.156 L43.402,41.177 L43.359,41.190 L43.279,41.185 L43.205,41.199 L43.153,41.236 L43.141,41.265 L43.171,41.288 L43.149,41.307 L43.057,41.353 L42.907,41.467 L42.822,41.492 L42.788,41.564 L42.754,41.579 L42.682,41.586 L42.607,41.579 L42.590,41.571 L42.567,41.559 L42.508,41.470 L42.466,41.440 L42.364,41.454 L42.280,41.475 L42.211,41.487 L42.078,41.494 L41.926,41.496 L41.824,41.432 L41.779,41.441 L41.702,41.472 L41.577,41.497 L41.510,41.517 L41.702,41.705 L41.759,41.817 L41.761,41.885 L41.763,41.970 L41.663,42.147 L41.578,42.398 L41.489,42.659 L41.419,42.738 L41.129,42.828 L41.062,42.931 L40.837,43.063 L40.524,43.121 L40.462,43.146 L40.191,43.312 L39.978,43.420 L40.024,43.485 L40.085,43.553 L40.150,43.570 L40.342,43.543 L40.519,43.512 L40.648,43.534 L40.802,43.480 L40.942,43.418 L41.083,43.374 L41.358,43.333 L41.461,43.276 L41.581,43.219 L42.050,43.190 L42.088,43.199 L42.122,43.207 L42.280,43.228 L42.419,43.224 L42.566,43.155 L42.660,43.159 L42.761,43.170 L42.890,43.133 L42.992,43.092 L43,43.050 L43.089,42.989 L43.348,42.897 L43.558,42.844 L43.623,42.808 L43.783,42.747 L43.799,42.728 L43.795,42.703 L43.750,42.658 L43.738,42.617 L43.760,42.594 L43.826,42.572 L43.957,42.567 L44.005,42.596 L44.103,42.616 L44.200,42.654 L44.329,42.704 L44.506,42.749 L44.576,42.748 L44.644,42.735 L44.692,42.710 L44.771,42.617 L44.850,42.747 L44.871,42.756 L44.943,42.730 L45.072,42.694 L45.160,42.675 L45.208,42.648 L45.344,42.530 L45.563,42.536 L45.656,42.518 L45.705,42.498 L45.728,42.475 L45.688,42.357 L45.634,42.235 L45.639,42.205 L45.727,42.159 L45.846,42.110 L45.910,42.071 L45.954,42.035 L46.048,42.009 L46.160,41.992 L46.213,41.990 L46.268,41.960 L46.412,41.905 L46.430,41.891 L46.405,41.855 L46.348,41.790 L46.303,41.757 L46.252,41.752 L46.202,41.737 L46.184,41.702 L46.182,41.657 L46.191,41.625 L46.204,41.613 L46.255,41.602 L46.305,41.508 L46.385,41.460 L46.509,41.406 L46.619,41.344 L46.673,41.287 L46.662,41.246 L46.626,41.160 L46.534,41.089 L46.458,41.070 L46.431,41.077 L46.381,41.099 L46.280,41.154 L46.171,41.198 L46.087,41.184 L46.031,41.167 L45.922,41.187 L45.793,41.224 L45.725,41.262 L45.696,41.289 L45.716,41.338 L45.422,41.425 L45.281,41.450 L45.217,41.423 L45.001,41.291 L44.976,41.277 L44.811,41.259 L44.811,41.249 L44.849,41.220 L44.841,41.211 L44.565,41.208 L44.473,41.191 L44.227,41.213 L44.146,41.203 L44.077,41.183 L43.909,41.159 L43.793,41.131 L43.645,41.117 L43.492,41.116 Z" />
|
||||
<title>Georgia</title>
|
||||
</g>
|
||||
<g class="GHA GH">
|
||||
<path id="GHA" d="M-0.069,11.116 L-0.005,11.056 L0.009,11.021 L-0.014,10.891 L-0.061,10.801 L-0.090,10.716 L-0.086,10.673 L-0.058,10.631 L0.039,10.564 L0.089,10.521 L0.148,10.455 L0.216,10.391 L0.332,10.307 L0.381,10.292 L0.379,10.269 L0.363,10.236 L0.352,9.925 L0.343,9.845 L0.335,9.804 L0.324,9.688 L0.312,9.671 L0.290,9.672 L0.270,9.668 L0.265,9.645 L0.273,9.621 L0.343,9.604 L0.327,9.587 L0.275,9.571 L0.252,9.536 L0.262,9.496 L0.233,9.464 L0.242,9.442 L0.260,9.426 L0.289,9.432 L0.371,9.486 L0.405,9.491 L0.448,9.480 L0.526,9.398 L0.529,9.358 L0.497,9.221 L0.466,9.115 L0.460,8.974 L0.493,8.895 L0.489,8.851 L0.453,8.814 L0.373,8.759 L0.379,8.722 L0.415,8.653 L0.483,8.575 L0.616,8.480 L0.686,8.355 L0.688,8.304 L0.647,8.253 L0.599,8.210 L0.584,8.146 L0.605,7.728 L0.500,7.547 L0.499,7.495 L0.510,7.435 L0.537,7.399 L0.591,7.389 L0.635,7.354 L0.620,7.227 L0.596,7.097 L0.592,7.034 L0.579,7.004 L0.538,6.980 L0.523,6.939 L0.533,6.888 L0.526,6.851 L0.548,6.802 L0.596,6.742 L0.673,6.593 L0.702,6.581 L0.715,6.549 L0.707,6.519 L0.737,6.453 L0.822,6.386 L0.912,6.329 L0.985,6.320 L1.002,6.269 L1.050,6.203 L1.084,6.174 L1.140,6.155 L1.185,6.145 L1.187,6.089 L1.106,6.051 L1.050,5.994 L1.008,5.906 L0.950,5.810 L0.749,5.760 L0.672,5.760 L0.260,5.757 L-0.127,5.568 L-0.349,5.501 L-0.485,5.394 L-0.669,5.319 L-0.798,5.227 L-1.064,5.183 L-1.502,5.038 L-1.638,4.981 L-1.777,4.880 L-2.002,4.762 L-2.090,4.764 L-2.266,4.874 L-2.399,4.929 L-2.723,5.014 L-2.965,5.046 L-3.082,5.082 L-3.114,5.089 L-3.087,5.128 L-3.019,5.131 L-2.948,5.119 L-2.895,5.149 L-2.816,5.153 L-2.795,5.185 L-2.789,5.264 L-2.790,5.328 L-2.762,5.357 L-2.755,5.433 L-2.794,5.600 L-2.821,5.619 L-2.962,5.643 L-2.973,5.676 L-2.998,5.711 L-3.025,5.798 L-3.056,5.926 L-3.106,6.086 L-3.201,6.348 L-3.224,6.441 L-3.240,6.536 L-3.244,6.649 L-3.224,6.691 L-3.227,6.749 L-3.236,6.807 L-3.169,6.941 L-3.038,7.105 L-3.010,7.164 L-2.986,7.205 L-2.982,7.264 L-2.959,7.455 L-2.896,7.685 L-2.857,7.772 L-2.830,7.819 L-2.798,7.896 L-2.790,7.932 L-2.669,8.022 L-2.613,8.047 L-2.601,8.082 L-2.620,8.121 L-2.612,8.148 L-2.583,8.161 L-2.538,8.172 L-2.506,8.209 L-2.557,8.493 L-2.598,8.776 L-2.600,8.800 L-2.625,8.840 L-2.649,8.957 L-2.690,9.025 L-2.747,9.045 L-2.747,9.110 L-2.689,9.219 L-2.674,9.283 L-2.702,9.302 L-2.706,9.351 L-2.686,9.432 L-2.696,9.481 L-2.706,9.534 L-2.766,9.658 L-2.781,9.746 L-2.750,9.797 L-2.751,9.910 L-2.783,10.083 L-2.788,10.193 L-2.767,10.238 L-2.777,10.282 L-2.820,10.323 L-2.823,10.363 L-2.787,10.402 L-2.791,10.432 L-2.837,10.455 L-2.878,10.508 L-2.915,10.592 L-2.907,10.728 L-2.839,10.977 L-2.830,10.998 L-2.752,10.997 L-2.752,10.986 L-2.509,10.989 L-2.232,10.991 L-1.901,10.995 L-1.600,10.998 L-1.586,11.009 L-1.537,11.023 L-1.233,10.997 L-1.042,11.010 L-0.962,11.002 L-0.903,10.985 L-0.772,10.995 L-0.701,10.989 L-0.649,10.927 L-0.627,10.927 L-0.598,10.954 L-0.545,10.984 L-0.492,11.008 L-0.454,11.056 L-0.430,11.093 L-0.396,11.086 L-0.346,11.088 L-0.313,11.119 L-0.299,11.167 Z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
@ -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():
|
||||
' </clipPath>\n'
|
||||
' <g clip-path="url(#clipPath)">\n'
|
||||
' <g class="country">\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)) +
|
||||
' </g>\n'
|
||||
' <g class="water">\n'
|
||||
+ plot_shapes('ne_50m_lakes', max_rank=4) +
|
||||
@ -167,10 +169,15 @@ def main():
|
||||
' </clipPath>\n'
|
||||
' <g clip-path="url(#clipPath)">\n'
|
||||
' <g class="country">\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") +
|
||||
' </g>\n'
|
||||
' <g class="water">\n'
|
||||
+ plot_shapes('ne_110m_lakes', max_rank=4) +
|
||||
@ -190,17 +197,22 @@ def main():
|
||||
' </clipPath>\n'
|
||||
' <g clip-path="url(#clipPath)">\n'
|
||||
' <g class="province">\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") +
|
||||
' </g>\n'
|
||||
' <g class="country-outline">\n'
|
||||
+ plot_shapes('ne_50m_admin_0_map_units', 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_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") +
|
||||
' </g>\n'
|
||||
' <g class="water">\n'
|
||||
+ plot_shapes('ne_50m_lakes', max_rank=4) +
|
||||
@ -219,12 +231,16 @@ def main():
|
||||
' </clipPath>\n'
|
||||
' <g clip-path="url(#clipPath)">\n'
|
||||
' <g class="country">\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") +
|
||||
' </g>\n'
|
||||
' <g class="thick-country-border">\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") +
|
||||
' </g>\n'
|
||||
' <g class="river">\n'
|
||||
+ plot_shapes('ne_10m_rivers_lake_centerlines', max_rank=5) +
|
||||
|
||||
@ -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 <g>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'</g>\n'
|
||||
current_state.pop()
|
||||
# enter any new <g>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'<g class="{clazz}">\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}<clipPath id="{identifier}-clipPath">\n'
|
||||
f'{indentation}<use href="#{identifier}" />\n'
|
||||
f'{indentation}</clipPath>\n'
|
||||
f'{indentation}<use href="#{identifier}" style="clip-path:url(#{identifier}-clipPath);" />\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 <g>
|
||||
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'<g class="{clazz}">\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}<circle id="{identifier}-circle" cx="{x_center:.3f}" cy="{y_center:.3f}" r="{radius}" />\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}<circle id="{identifier}-bubble" cx="{x_center:.3f}" cy="{y_center:.3f}" r="{2e-4*sqrt(area):.3f}" />\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}<title>{label}</title>\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 <g>s before returning
|
||||
while len(current_state) > 0:
|
||||
result += '\t'*(3 + len(current_state)) + f'</g>\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}<clipPath id="{identifier}-clipPath">\n'
|
||||
f'{indentation}<use href="#{identifier}" />\n'
|
||||
f'{indentation}</clipPath>\n'
|
||||
f'{indentation}<use href="#{identifier}" style="clip-path:url(#{identifier}-clipPath);" />\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}<circle id="{identifier}-circle" cx="{x_center:.3f}" cy="{y_center:.3f}" r="{radius}" />\n'
|
||||
any_content = True
|
||||
|
||||
# remove any groups with no elements
|
||||
for _ in range(3):
|
||||
result = re.sub(r'(\t)*<g class="([A-Za-z _-]+)">(\s*)</g>\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}<title>{label}</title>\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 <g>
|
||||
result += '\t'*num_tabs + f'</g>\n'
|
||||
|
||||
# # remove any groups with no elements
|
||||
result = re.sub(r'(\t)*<g class="([A-Za-z _-]+)">(\s*)</g>\n',
|
||||
'', result)
|
||||
# simplify any groups with only a single element
|
||||
result = re.sub(r'<g class="([A-Za-z _-]+)">(\s*)<([a-z]+) ([^\n]*)>(\s*)</g>',
|
||||
'<\\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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user