-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathbng.py
184 lines (150 loc) · 6.04 KB
/
bng.py
1
2
3
4
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
178
179
180
181
182
183
184
# -*- coding: utf-8 -*-
# ##########################################################################
#
# COPYRIGHT: (C) 2012-2019 John A Stevenson / @volcan01010
# Magnus Hagdorn / @mhagdorn
# Jumy Elǝrossë / @realjumy
# WEBSITE: http://all-geo.org/volcan01010
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# http://www.gnu.org/licenses/gpl-3.0.html
#
# ############################################################################
import math
import re
__version__ = '1.0.3'
__all__ = ['to_osgb36', 'from_osgb36']
class BNGError(Exception):
"""Exception raised by bng.py module"""
pass
def _init_regions_and_offsets():
# Region codes for 100 km grid squares.
regions = [['HL', 'HM', 'HN', 'HO', 'HP', 'JL', 'JM'],
['HQ', 'HR', 'HS', 'HT', 'HU', 'JQ', 'JR'],
['HV', 'HW', 'HX', 'HY', 'HZ', 'JV', 'JW'],
['NA', 'NB', 'NC', 'ND', 'NE', 'OA', 'OB'],
['NF', 'NG', 'NH', 'NJ', 'NK', 'OF', 'OG'],
['NL', 'NM', 'NN', 'NO', 'NP', 'OL', 'OM'],
['NQ', 'NR', 'NS', 'NT', 'NU', 'OQ', 'OR'],
['NV', 'NW', 'NX', 'NY', 'NZ', 'OV', 'OW'],
['SA', 'SB', 'SC', 'SD', 'SE', 'TA', 'TB'],
['SF', 'SG', 'SH', 'SJ', 'SK', 'TF', 'TG'],
['SL', 'SM', 'SN', 'SO', 'SP', 'TL', 'TM'],
['SQ', 'SR', 'SS', 'ST', 'SU', 'TQ', 'TR'],
['SV', 'SW', 'SX', 'SY', 'SZ', 'TV', 'TW']]
# Transpose so that index corresponds to offset
regions = list(zip(*regions[::-1]))
# Create mapping to access offsets from region codes
offset_map = {}
for i in range(len(regions)):
for j in range(len(regions[0])):
region = regions[i][j]
offset_map[region] = (1e5 * i, 1e5 * j)
return regions, offset_map
_regions, _offset_map = _init_regions_and_offsets()
def to_osgb36(gridref):
"""
Convert British National Grid references to OSGB36 numeric coordinates.
Grid references can be 4, 6, 8 or 10 figures.
:param gridref: str - BNG grid reference
:returns coords: tuple - x, y coordinates
Examples:
Single value
>>> to_osgb36('NT2755072950')
(327550, 672950)
For multiple values, use Python's zip function and list comprehension
>>> gridrefs = ['HU431392', 'SJ637560', 'TV374354']
>>> x, y = zip(*[bng.to_osgb36(g) for g in gridrefs])
>>> x
(443100, 363700, 537400)
>>> y
(1139200, 356000, 35400)
"""
# Validate input
bad_input_message = (
'Valid gridref inputs are 4, 6, 8 or 10-fig references as strings '
'e.g. "NN123321", or lists/tuples/arrays of strings. '
'[{}]'.format(gridref))
try:
gridref = gridref.upper()
pattern = r'^([A-Z]{2})(\d{4}|\d{6}|\d{8}|\d{10})$'
match = re.match(pattern, gridref)
except (TypeError, AttributeError):
# Non-string values will throw error
raise BNGError(bad_input_message)
if not match:
raise BNGError(bad_input_message)
# Extract data from gridref
region, coords = match.groups()
# Get offset from region
try:
x_offset, y_offset = _offset_map[region]
except KeyError:
raise BNGError('Invalid 100 km grid square code: {}'.format(region))
# Get easting and northing from text and convert to coords
half_figs = len(coords) // 2
easting, northing = int(coords[:half_figs]), int(coords[half_figs:])
scale_factor = 10 ** (5 - half_figs)
x = int(easting * scale_factor + x_offset)
y = int(northing * scale_factor + y_offset)
return x, y
def from_osgb36(coords, figs=6):
"""
Convert osgb36 numeric coordinates to British National Grid references.
Grid references can be 4, 6, 8 or 10 fig, specified by the figs keyword.
:param coords: tuple - x, y coordinates to convert
:param figs: int - number of figures to output
:return gridref: str - BNG grid reference
Examples:
Single value
>>> from_osgb36((327550, 672950))
'NT275729'
For multiple values, use Python's zip function and list comprehension
>>> x = [443143, 363723, 537395]
>>> y = [1139158, 356004, 35394]
>>> [bng.from_osgb36(coords, figs=4) for coords in zip(x, y)]
['HU4339', 'SJ6356', 'TV3735']
"""
# Validate input
bad_input_message = ('Valid inputs are x, y tuple e.g. (651409, 313177),'
' or list of x, y tuples. [{}]'.format(coords))
if not isinstance(coords, tuple):
raise BNGError(bad_input_message)
try:
x, y = coords
except ValueError:
raise BNGError(bad_input_message)
out_of_region_message = (
'Coordinate location outside UK region: {}'.format(coords))
if (x < 0) or (y < 0):
raise BNGError(out_of_region_message)
# Calculate region and SW corner offset
x_index = int(math.floor(x / 100000.0))
y_index = int(math.floor(y / 100000.0))
try:
region = _regions[x_index][y_index]
x_offset, y_offset = _offset_map[region]
except IndexError:
raise BNGError(out_of_region_message)
# Format the output based on figs
templates = {4: '{}{:02}{:02}', 6: '{}{:03}{:03}',
8: '{}{:04}{:04}', 10: '{}{:05}{:05}'}
factors = {4: 1000.0, 6: 100.0, 8: 10.0, 10: 1.0}
try: # Catch bad number of figures
coords = templates[figs].format(
region,
int(math.floor((x - x_offset) / factors[figs])),
int(math.floor((y - y_offset) / factors[figs]))
)
except KeyError:
raise BNGError('Valid inputs for figs are 4, 6, 8 or 10')
return coords