Making a map out of dots

(0 comments)

The code below generates stylised maps with land regions depicted by spaced markers (circles, polygons, etc). It requires the Python library, global-land-mask:

pip install global-land-mask

To create images, use the below DotMap class as follows:

from dotmap import DotMap

# British Isles
dot_map = DotMap(lat_range=(50, 60),
                 lon_range=(-12, 2),
                 d=20, r=6
                )
dot_map.stroke = '#4b2a8e'
dot_map.fill = '#4b2a8e'
dot_map.make_svg("uk-20.svg", marker='o')

enter image description here

# Africa
dot_map = DotMap(width=600,
                 lat_range=(-45, 40),
                 lon_range=(-24, 52),
                 d=12)
dot_map.fill = "none"
dot_map.stroke = '#d44627'
dot_map.make_svg("africa-12.svg", marker='p_6_15')

enter image description here

The DotMap class is defined in the file dotmap.py:

import re
from functools import partial
import numpy as np
import matplotlib.pyplot as plt
from global_land_mask import globe

class DotMap:
    """A class to create a map in "dots"."""

    def __init__(self, width=800, lat_range=None, lon_range=None,
                 d=None, r=None):
        """
        Initialize the DotMap instance with the SVG image width (px),
        latitude range (-90 to 90 degrees), longitude range (-180 to 180 degrees),
        marker spacing and marker size (radius, in the case of circles), both in px.
        """

        if lat_range is None:
            lat_range = -90, 90
        if lon_range is None:
            lon_range = -180, 180
        self.lat_min, self.lat_max = lat_range
        self.lon_min, self.lon_max = lon_range

        self.width = width
        if d is None:
            # Default marker spacing.
            self.d = width / 100
        else:
            self.d = d
        if r is None:
            # Default marker size.
            self.r = self.d * 0.4
        else:
            self.r = r

        # Latitude and longitude ranges, degrees.
        self.Dlat = self.lat_max - self.lat_min
        self.Dlon = self.lon_max - self.lon_min
        # Calculate the height of the SVG image (px).
        self.height = int(self.width * self.Dlat / self.Dlon)
        # Number of maker sites along each axis.
        self.Nx = int(self.width / self.d + 1)
        self.Ny = int(self.height / self.d + 1)
        # Latitude and longitude spacings, degrees.
        self.dlat = self.Dlat / (self.Ny - 1)
        self.dlon = self.Dlon / (self.Nx - 1)

        # Default SVG marker styles.
        self.fill = "black"
        self.stroke = "black"
        self.stroke_width = "2px"

    def circle_at(self, ix, iy):
        """Circle marker."""
        cx, cy= ix * self.d, self.height - iy* self.d
        return f'<circle cx="{cx}" cy="{cy}" r="{self.r}"/>'

    def poly_at(self, ix, iy, nvert=6, phase=0):
        """Polygon marker with nvert vertices, rotated by phase degrees."""
        # Convert phase from degrees to radians.
        phase = np.radians(phase)
        cx, cy= ix * self.d, self.height - iy* self.d
        dtheta = 2 * np.pi / nvert
        points = []
        for j in range(nvert):
            theta = j * dtheta
            vx = cx + self.r * np.cos(theta + phase)
            vy = cy + self.r * np.sin(theta + phase)
            points.append(f"{vx}, {vy}")
        points = " ".join(points)
        return f'<polygon points="{points}"/>'

    def make_svg(self, filename="dot-map.svg", marker='o'):
        """Make the SVG image and save as filename.

        Available markers are:
        'o': circle
        'h': hexagon
        '^': up-triangle
        'v': down-triangle
        'p_<NVERT>_<PHASE>': polygon with <NVERT> vertices, rotated by <PHASE> degrees

        """

        poly_patt = r'p_(\d+)_(\d+)'
        if marker == 'o':
            marker_at = self.circle_at
        elif marker == 'h':
            marker_at = self.poly_at
        elif marker == 's':
            marker_at = partial(self.poly_at, nvert=4, phase=45)
        elif marker == '^':
            marker_at = partial(self.poly_at, nvert=3, phase=30)
        elif marker == 'v':
            marker_at = partial(self.poly_at, nvert=3, phase=90)
        elif m := re.match(poly_patt, marker):
            nvert = int(m.group(1))
            phase = float(m.group(2))
            marker_at = partial(self.poly_at, nvert=nvert, phase=phase)
        else:
            raise ValueError(f"Unrecognised marker: {marker}")

        # SVG preamble including marker styles.
        svg = [ '<?xml version="1.0" encoding="utf-8"?>',
                '<svg xmlns="http://www.w3.org/2000/svg"',
               f'     xmlns:xlink="http://www.w3.org/1999/xlink" '
               f'width="{self.width}" height="{self.height}" >\n',
                '<defs>\n    <style type="text/css"><![CDATA[',
               f'        circle {{stroke: {self.stroke}; fill: {self.fill};'
               f' stroke-width: {self.stroke_width};}}'
               f'        polygon {{stroke: {self.stroke}; fill: {self.fill};'
               f' stroke-width: {self.stroke_width};}}'
                ']]>\n    </style>\n</defs>'
              ]

        for iy in range(self.Ny):
            lat = self.lat_min + iy * self.dlat
            for ix in range(self.Nx):
                lon = self.lon_min + ix * self.dlon
                if globe.is_land(lat, lon):
                    svg.append(marker_at(ix, iy))

        svg.append('</svg>')

        print(f"Writing SVG file {filename}")
        with open(filename, 'w') as fo:
            print('\n'.join(svg), file=fo)

Some more usage examples:

# Europe
dot_map = DotMap(lat_range=(30, 70), lon_range=(-10, 20), d=4)
dot_map.stroke_width = '1px'
dot_map.make_svg("europe-4.svg", marker='o')

# World (without Antarctica)
dot_map = DotMap(lat_range=(-60, 90), d=12)
dot_map.stroke_width = '1px'
dot_map.make_svg("world-12.svg", marker='o')
Current rating: 4

Comments

Comments are pre-moderated. Please be patient and your comment will appear soon.

There are currently no comments

New Comment

required

required (not published)

optional

required