Skip to content

Geographical unit

GeographicalUnit

Represents a single geographical unit at any level (SGU, MGU, LGU). Generic class that works with any geography, past or present.

Source code in may/geography/geographical_unit.py
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class GeographicalUnit:
    """
    Represents a single geographical unit at any level (SGU, MGU, LGU).
    Generic class that works with any geography, past or present.
    """

    __slots__ = [
        'id',
        'name',
        'level',
        'coordinates',
        'parent',
        'children',
        'venues',
        'people',
        'properties',
    ]

    def __init__(self, id, name, level, coordinates=None, parent=None, properties=None):
        self.id = id              # Unique numeric ID (generated by code)
        self.name = name          # Unique name/identifier (e.g., "E00004320", "London")
        self.level = level        # e.g. "SGU", "MGU", or "LGU"
        self.coordinates = coordinates  # Tuple of (latitude, longitude) or None
        self.parent = parent      # Reference to parent unit (one level up)
        self.children = []        # List of child units (one level down)
        self.venues = []          # List of venues in this geographical unit
        self.people = []          # List of people in this geographical unit (populated at smallest level)
        self.properties = properties if properties is not None else {}      # Extensible dict for any additional data

    def add_child(self, child):
        """Add a child unit and set this as its parent"""
        self.children.append(child)
        child.parent = self

    def add_venue(self, venue):
        """Add a venue to this geographical unit"""
        self.venues.append(venue)

    def add_person(self, person):
        """Add a person to this geographical unit"""
        self.people.append(person)

    def get_venues_by_type(self, venue_type):
        """Get all venues of a specific type in this geographical unit"""
        return [v for v in self.venues if v.type == venue_type]

    def get_ancestors(self):
        """Get all ancestor units up the hierarchy"""
        ancestors = []
        current = self.parent
        while current is not None:
            ancestors.append(current)
            current = current.parent
        return ancestors

    def get_ancestor_by_level(self, level):
        """
        Get ancestor unit at a specific level.

        Args:
            level: Level name (e.g., "LGU", "MGU", "SGU")

        Returns:
            GeographicalUnit at that level or None if not found
        """
        # Check if we're already at the requested level
        if self.level == level:
            return self

        # Traverse up the hierarchy
        current = self.parent
        while current is not None:
            if current.level == level:
                return current
            current = current.parent

        return None

    def get_descendants(self, level=None):
        """
        Get all descendant units. If level specified, only return that level.
        """
        descendants = []
        for child in self.children:
            if level is None or child.level == level:
                descendants.append(child)
            descendants.extend(child.get_descendants(level))
        return descendants

    def get_people(self):
        """ Get all people in the geo_unit and/or all its descendents """
        people = set()
        if self.people:
            people.update(set(self.people))
        if self.children:
            for child in self.children:
                people.update(child.get_people())
        return people

    def __repr__(self):
        venue_info = f", {len(self.venues)} venues" if self.venues else ""
        people_info = f", {len(self.people)} people" if self.people else ""
        return f"<{self.level} #{self.id}: {self.name} ({len(self.children)} children{venue_info}{people_info})>"

    def __eq__(self, other) -> bool:
        """
        Check if two GeographicalUnit objects are equal.

        Compares intrinsic properties (id, name, level, coordinates, properties)
        and structural position (parent). Does NOT compare collections (children,
        venues, people) as those should be compared at a higher level.

        Args:
            other: Another object to compare with

        Returns:
            bool: True if both units have identical intrinsic properties

        Example:
            >>> unit1 = GeographicalUnit(id=1, name='E00001', level='SGU')
            >>> unit2 = GeographicalUnit(id=1, name='E00001', level='SGU')
            >>> unit1 == unit2
            True
        """
        if not isinstance(other, GeographicalUnit):
            return False

        # Compare core identifying attributes
        if (self.id != other.id or
            self.name != other.name or
            self.level != other.level):
            return False

        # Compare coordinates
        # Handle None and tuple comparison
        if self.coordinates != other.coordinates:
            return False

        # Compare properties dictionary
        if self.properties != other.properties:
            return False

        # Compare parent (by id/name only, not object identity)
        # This avoids circular reference issues and handles different object instances
        if self.parent is None and other.parent is None:
            pass  # Both have no parent, equal
        elif self.parent is None or other.parent is None:
            return False  # One has parent, other doesn't
        elif (self.parent.id != other.parent.id or
              self.parent.name != other.parent.name):
            return False  # Parents differ

        # NOTE: We do NOT compare children, venues, or people collections here.
        # These should be compared at higher levels (Geography/World level)
        # to avoid circular comparison issues and performance problems.

        return True

    def __hash__(self) -> int:
        """
        Return hash of GeographicalUnit based on immutable identifiers.

        Uses id and name which should remain constant after creation.
        Allows GeographicalUnit objects to be used in sets and as dict keys.

        Returns:
            int: Hash value based on id and name

        Note:
            Two units with same id and name will have same hash, even if
            other attributes differ. The __eq__ method provides full comparison.
        """
        return hash((self.id, self.name))

__eq__(other)

Check if two GeographicalUnit objects are equal.

Compares intrinsic properties (id, name, level, coordinates, properties) and structural position (parent). Does NOT compare collections (children, venues, people) as those should be compared at a higher level.

Parameters:

Name Type Description Default
other

Another object to compare with

required

Returns:

Name Type Description
bool bool

True if both units have identical intrinsic properties

Example

unit1 = GeographicalUnit(id=1, name='E00001', level='SGU') unit2 = GeographicalUnit(id=1, name='E00001', level='SGU') unit1 == unit2 True

Source code in may/geography/geographical_unit.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def __eq__(self, other) -> bool:
    """
    Check if two GeographicalUnit objects are equal.

    Compares intrinsic properties (id, name, level, coordinates, properties)
    and structural position (parent). Does NOT compare collections (children,
    venues, people) as those should be compared at a higher level.

    Args:
        other: Another object to compare with

    Returns:
        bool: True if both units have identical intrinsic properties

    Example:
        >>> unit1 = GeographicalUnit(id=1, name='E00001', level='SGU')
        >>> unit2 = GeographicalUnit(id=1, name='E00001', level='SGU')
        >>> unit1 == unit2
        True
    """
    if not isinstance(other, GeographicalUnit):
        return False

    # Compare core identifying attributes
    if (self.id != other.id or
        self.name != other.name or
        self.level != other.level):
        return False

    # Compare coordinates
    # Handle None and tuple comparison
    if self.coordinates != other.coordinates:
        return False

    # Compare properties dictionary
    if self.properties != other.properties:
        return False

    # Compare parent (by id/name only, not object identity)
    # This avoids circular reference issues and handles different object instances
    if self.parent is None and other.parent is None:
        pass  # Both have no parent, equal
    elif self.parent is None or other.parent is None:
        return False  # One has parent, other doesn't
    elif (self.parent.id != other.parent.id or
          self.parent.name != other.parent.name):
        return False  # Parents differ

    # NOTE: We do NOT compare children, venues, or people collections here.
    # These should be compared at higher levels (Geography/World level)
    # to avoid circular comparison issues and performance problems.

    return True

__hash__()

Return hash of GeographicalUnit based on immutable identifiers.

Uses id and name which should remain constant after creation. Allows GeographicalUnit objects to be used in sets and as dict keys.

Returns:

Name Type Description
int int

Hash value based on id and name

Note

Two units with same id and name will have same hash, even if other attributes differ. The eq method provides full comparison.

Source code in may/geography/geographical_unit.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def __hash__(self) -> int:
    """
    Return hash of GeographicalUnit based on immutable identifiers.

    Uses id and name which should remain constant after creation.
    Allows GeographicalUnit objects to be used in sets and as dict keys.

    Returns:
        int: Hash value based on id and name

    Note:
        Two units with same id and name will have same hash, even if
        other attributes differ. The __eq__ method provides full comparison.
    """
    return hash((self.id, self.name))

add_child(child)

Add a child unit and set this as its parent

Source code in may/geography/geographical_unit.py
34
35
36
37
def add_child(self, child):
    """Add a child unit and set this as its parent"""
    self.children.append(child)
    child.parent = self

add_person(person)

Add a person to this geographical unit

Source code in may/geography/geographical_unit.py
43
44
45
def add_person(self, person):
    """Add a person to this geographical unit"""
    self.people.append(person)

add_venue(venue)

Add a venue to this geographical unit

Source code in may/geography/geographical_unit.py
39
40
41
def add_venue(self, venue):
    """Add a venue to this geographical unit"""
    self.venues.append(venue)

get_ancestor_by_level(level)

Get ancestor unit at a specific level.

Parameters:

Name Type Description Default
level

Level name (e.g., "LGU", "MGU", "SGU")

required

Returns:

Type Description

GeographicalUnit at that level or None if not found

Source code in may/geography/geographical_unit.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def get_ancestor_by_level(self, level):
    """
    Get ancestor unit at a specific level.

    Args:
        level: Level name (e.g., "LGU", "MGU", "SGU")

    Returns:
        GeographicalUnit at that level or None if not found
    """
    # Check if we're already at the requested level
    if self.level == level:
        return self

    # Traverse up the hierarchy
    current = self.parent
    while current is not None:
        if current.level == level:
            return current
        current = current.parent

    return None

get_ancestors()

Get all ancestor units up the hierarchy

Source code in may/geography/geographical_unit.py
51
52
53
54
55
56
57
58
def get_ancestors(self):
    """Get all ancestor units up the hierarchy"""
    ancestors = []
    current = self.parent
    while current is not None:
        ancestors.append(current)
        current = current.parent
    return ancestors

get_descendants(level=None)

Get all descendant units. If level specified, only return that level.

Source code in may/geography/geographical_unit.py
83
84
85
86
87
88
89
90
91
92
def get_descendants(self, level=None):
    """
    Get all descendant units. If level specified, only return that level.
    """
    descendants = []
    for child in self.children:
        if level is None or child.level == level:
            descendants.append(child)
        descendants.extend(child.get_descendants(level))
    return descendants

get_people()

Get all people in the geo_unit and/or all its descendents

Source code in may/geography/geographical_unit.py
 94
 95
 96
 97
 98
 99
100
101
102
def get_people(self):
    """ Get all people in the geo_unit and/or all its descendents """
    people = set()
    if self.people:
        people.update(set(self.people))
    if self.children:
        for child in self.children:
            people.update(child.get_people())
    return people

get_venues_by_type(venue_type)

Get all venues of a specific type in this geographical unit

Source code in may/geography/geographical_unit.py
47
48
49
def get_venues_by_type(self, venue_type):
    """Get all venues of a specific type in this geographical unit"""
    return [v for v in self.venues if v.type == venue_type]