Skip to content
Snippets Groups Projects

Where should the code added on feature/#578_UTM_zone_tools live?

Closed Nicola Jordan requested to merge feature/#578_UTM_zone_tools into develop

Created by: das-g

Do not merge

For now, for discussing what to do with this. (Might be turned into a merge request later, depending on result.)

This new code for #578 (closed) is fairly independent of the rest of the project. Should it live within the osmaxx repo or in a separate (and POPyO) PyPI package?

Some of the tests would have to remain in osmaxx as they depend on PostGIS, lest we want the PyPI package to have that as a test dependency, too.

Do not merge

Merge request reports

Approval is optional

Closed by avatar (Jun 15, 2025 1:54am UTC)

Merge details

  • The changes were not merged into develop.

Activity

Filter activity
  • Approvals
  • Assignees & reviewers
  • Comments (from bots)
  • Comments (from users)
  • Commits & branches
  • Edits
  • Labels
  • Lock status
  • Mentions
  • Merge request status
  • Tracking
  • Created by: hixi

    When you merge develop into your branch, the slow tests should work as well.

  • Created by: hixi

    IMHO think this is a good candidate for an external package.

    I'd make an PyPI-Package (using twine to push to PyPI).

    I see two options (and a combination thereof):

    1. making it an independent package, no connection to django/django-rest-framework we're depending on GeoDjango.
    2. making it a django-rest-framework-package
    3. doing both, and using the first as a dependency on the second

    I would favor option 2 Option 2 is the only Option: Having it ready as a REST-Service, this can easily be included in a Dockerimage for simple deployment and be used for the OSMaxx project right away, but doesn't limit usage in any other way (one should easily be able to use the package without installing rest-framework, if the viewsets aren't being used).

    Doing the review I discovered my assumption that this can be independent of Django doesn't hold. Hence the crossed out lines above.

    Since we rely on GeoDjango, we should make that a dependency and the tests should cover that (relying on postgis!), escpecially since that isn't that hard to do with automatic testing even on travis

  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
1 1 def pytest_addoption(parser):
2 2 parser.addoption("--runslow", action="store_true",
3 3 help="run slow tests")
4 parser.addoption("--all-utm-zones", action="store_true",
  • Created by: hixi

    instead of running the test with really all utm zones, I wonder if it would make sense to test the edge cases, boundary cases and happy path cases only. This would require more thought on what those cases actually are, but just because we can test all cases, doesn't mean we necessarily need to (they seem to me quite similar in what they test).

  • Nicola Jordan
  • Nicola Jordan
  • Nicola Jordan
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 43 )
    44 if xmin <= xmax:
    45 domain = Polygon.from_bbox((xmin, ymin, xmax, ymax))
    46 domain.srid = WGS_84
    47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    59
    60 @property
    61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    • Created by: hixi

      What does the subtraction of 0.5 do (or why is it needed)? I would guess, to get to the center, but then, why exactly 0.5?

  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    59
    60 @property
    61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    63
    64 def __eq__(self, other):
  • Nicola Jordan
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    63
    64 def __eq__(self, other):
    65 return self.hemisphere, self.utm_zone_number == other.hemisphere, other.utm_zone_number
    66
    67 def __hash__(self):
    68 return hash((self.hemisphere, self.utm_zone_number))
    69
    70 def __str__(self):
    71 return "UTM Zone {zone_number}, {hemisphere}ern hemisphere".format(
    72 zone_number=self.utm_zone_number,
    73 hemisphere=self.hemisphere,
    74 )
    75
    76 def __repr__(self):
    • Created by: hixi

      From the __repr__ documentation: I think this should return UniversalTransverseMercatorZone({hemisphere}, {zone_number}).

      I know, there is a shorthand below, but this class shouldn't know about that.

      Alternatively, rename this class.

  • Nicola Jordan
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 80 )
    81
    82 UTMZone = UniversalTransverseMercatorZone
    83 UTM_ZONE_NUMBERS = UTMZone.VALID_ZONE_NUMBERS
    84 ALL_UTM_ZONES = frozenset(UTMZone(hs, nr) for hs in UTMZone.HEMISPHERE_PREFIXES for nr in UTM_ZONE_NUMBERS)
    85
    86
    87 def utm_zones_for_representing(geom):
    88 return frozenset(zone for zone in ALL_UTM_ZONES if zone.can_represent(geom))
    89
    90
    91 def wrap_longitude_degrees(longitude_degrees):
    92 return confine(longitude_degrees, MIN_LONGITUDE_DEGREES, MAX_LONGITUDE_DEGREES)
    93
    94
    95 def confine(value, lower_bound, upper_bound):
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 43 )
    44 if xmin <= xmax:
    45 domain = Polygon.from_bbox((xmin, ymin, xmax, ymax))
    46 domain.srid = WGS_84
    47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    • Created by: das-g

      Would string concatenation (and conversion back to int) make more sense? (What I'm doing here is probably better performant, but that shouldn't matter as this isn't called in high frequency.)

  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 4 4 import sqlalchemy
    5 5 from sqlalchemy.sql.schema import Table as DbTable
    6 6
    7 from tests.inside_worker_test.conftest import slow
    7 from tests.utils import slow
  • Nicola Jordan
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 43 )
    44 if xmin <= xmax:
    45 domain = Polygon.from_bbox((xmin, ymin, xmax, ymax))
    46 domain.srid = WGS_84
    47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    59
    60 @property
    61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    • Created by: das-g

      UTM Zone 1 goes from longitude (-180°) to longitude (-180° + zone width). Its central meridian is centered between these boundaries.

      • 180° + (1 - 0) × zone width gives us the east edge of zone 1.
      • 180° + (1 - ½) × zone width gives us the central meridian of zone 1.
      • 180° + (1 - 1) × zone width gives us the west edge of zone 1.

      where the first 1 is the zone number.

      Not all other zones have the same width (at least not everywhere), but their "central meridians" are defined consistent with that formula (and thus not always actually central).

  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    63
    64 def __eq__(self, other):
    65 return self.hemisphere, self.utm_zone_number == other.hemisphere, other.utm_zone_number
    66
    67 def __hash__(self):
    68 return hash((self.hemisphere, self.utm_zone_number))
    69
    70 def __str__(self):
    71 return "UTM Zone {zone_number}, {hemisphere}ern hemisphere".format(
    72 zone_number=self.utm_zone_number,
    73 hemisphere=self.hemisphere,
    74 )
    75
    76 def __repr__(self):
    • Created by: das-g

      How does using the alias class name violate

      [T]his should look like a valid Python expression that could be used to recreate an object with the same value (given an appropriate environment).

      in any way? Or is it in opposition to something else in the documentation?

  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    59
    60 @property
    61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 80 )
    81
    82 UTMZone = UniversalTransverseMercatorZone
    83 UTM_ZONE_NUMBERS = UTMZone.VALID_ZONE_NUMBERS
    84 ALL_UTM_ZONES = frozenset(UTMZone(hs, nr) for hs in UTMZone.HEMISPHERE_PREFIXES for nr in UTM_ZONE_NUMBERS)
    85
    86
    87 def utm_zones_for_representing(geom):
    88 return frozenset(zone for zone in ALL_UTM_ZONES if zone.can_represent(geom))
    89
    90
    91 def wrap_longitude_degrees(longitude_degrees):
    92 return confine(longitude_degrees, MIN_LONGITUDE_DEGREES, MAX_LONGITUDE_DEGREES)
    93
    94
    95 def confine(value, lower_bound, upper_bound):
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    63
    64 def __eq__(self, other):
    65 return self.hemisphere, self.utm_zone_number == other.hemisphere, other.utm_zone_number
    66
    67 def __hash__(self):
    68 return hash((self.hemisphere, self.utm_zone_number))
    69
    70 def __str__(self):
    71 return "UTM Zone {zone_number}, {hemisphere}ern hemisphere".format(
    72 zone_number=self.utm_zone_number,
    73 hemisphere=self.hemisphere,
    74 )
    75
    76 def __repr__(self):
    • Created by: hixi

      #fake code
      UniversalTransverseMercatorZone('north', 32).__repr__
      > UTM('north', 32)

      Doesn't seem very logical to me to use the alias as __repr__, when the class definition doesn't know it's being aliased.

  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    59
    60 @property
    61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    • Created by: das-g

      Only if we can think of a good one.

      Alternatively, the geometric interpretation of the formula is maybe more apparent if written as

      return MIN_LONGITUDE_DEGREES \
          + (self.utm_zone_number - 1) * self.ZONE_WIDTH_DEGREES \
          + self.ZONE_WIDTH_DEGREES / 2
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 9afe844f
  • 47 return domain
    48 else:
    49 # cut at idealized international date line
    50 return MultiPolygon(
    51 Polygon.from_bbox((xmin, ymin, MAX_LONGITUDE_DEGREES, ymax)),
    52 Polygon.from_bbox((MIN_LONGITUDE_DEGREES, ymin, xmax, ymax)),
    53 srid=WGS_84,
    54 )
    55
    56 @property
    57 def srid(self):
    58 return self.HEMISPHERE_PREFIXES[self.hemisphere] * 100 + self.utm_zone_number
    59
    60 @property
    61 def central_meridian_longitude_degrees(self):
    62 return MIN_LONGITUDE_DEGREES + (self.utm_zone_number - 0.5) * self.ZONE_WIDTH_DEGREES
    • Created by: hixi

      or a docstring:

      """
      Some short, meaningful description.
      
      UTM Zone 1 goes from longitude (-180°) to longitude (-180° + zone width). Its central meridian is centered between these boundaries.
      
      180° + (1 - 0) × zone width gives us the east edge of zone 1.
      180° + (1 - ½) × zone width gives us the central meridian of zone 1.
      180° + (1 - 1) × zone width gives us the west edge of zone 1.
      where the first 1 is the zone number.
      
      returns `Bob Lawbla`
      """
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 4d28c910
  • 7 7 MAX_LONGITUDE_DEGREES = +180
    8 8
    9 9
    10 class UniversalTransverseMercatorZone:
    11 HEMISPHERE_PREFIXES = dict(
    12 north=326,
    13 south=327,
    14 )
    10 class UTMZone:
    11 """
    12 Universal Transverse Mercator (UTM) zone
  • Nicola Jordan
    Nicola Jordan @nicola.jordan started a thread on commit 4d28c910
  • 1 from django.contrib.gis.geos.collections import MultiPolygon
    2 from django.contrib.gis.geos.polygon import Polygon
    3
    4 from osmaxx.conversion_api.coordinate_reference_systems import WGS_84
    5
    6 MIN_LONGITUDE_DEGREES = -180
    7 MAX_LONGITUDE_DEGREES = +180
    8
    9
    10 class UTMZone:
    11 """
    12 Universal Transverse Mercator (UTM) zone
  • Created by: das-g

    DRF package it is: https://github.com/geometalab/drf-utm-zone-info

  • Please register or sign in to reply
    Loading