import array
import logging
import six
from bisect import bisect_left
from collections import defaultdict, OrderedDict
from itertools import izip, combinations
from geodata.coordinates.conversion import latlon_to_decimal
from geodata.encoding import safe_encode, safe_decode
from geodata.file_utils import ensure_dir
from geodata.graph.scc import strongly_connected_components
from geodata.i18n.languages import osm_admin1_ids
from geodata.math.floats import isclose
from geodata.osm.definitions import osm_definitions
from geodata.osm.extract import *
class OSMPolygonReader(object):
def __init__(self, filename):
self.filename = filename
self.node_ids = array.array('l')
self.way_ids = array.array('l')
self.coords = array.array('d')
self.nodes = {}
self.way_deps = array.array('l')
self.way_coords = array.array('d')
self.way_indptr = array.array('i', [0])
self.logger = logging.getLogger('osm_admin_polys')
def binary_search(self, a, x):
i = bisect_left(a, x)
if i != len(a) and a[i] == x:
return i
raise ValueError
def node_coordinates(self, coords, indptr, idx):
start_index = indptr[idx] * 2
end_index = indptr[idx + 1] * 2
node_coords = coords[start_index:end_index]
return zip(node_coords[::2], node_coords[1::2])
def sparse_deps(self, data, indptr, idx):
return [data[i] for i in xrange(indptr[idx], indptr[idx + 1])]
def create_polygons(self, ways):
end_nodes = defaultdict(list)
polys = []
way_indices = {}
start_end_nodes = {}
for way_id in ways:
try:
way_index = self.binary_search(self.way_ids, way_id)
except ValueError:
continue
way_indices[way_id] = way_index
start_node_id = self.way_deps[self.way_indptr[way_index]]
end_node_id = self.way_deps[self.way_indptr[way_index + 1] - 1]
start_end_nodes[way_id] = (start_node_id, end_node_id)
if start_node_id == end_node_id:
way_node_points = self.node_coordinates(self.way_coords, self.way_indptr, way_index)
polys.append(way_node_points)
continue
end_nodes[start_node_id].append(way_id)
end_nodes[end_node_id].append(way_id)
way_graph = defaultdict(OrderedDict)
for node_id, ways in end_nodes.iteritems():
for w1, w2 in combinations(ways, 2):
way_graph[w1][w2] = None
way_graph[w2][w1] = None
way_graph = {v: w.keys() for v, w in way_graph.iteritems()}
for component in strongly_connected_components(way_graph):
poly_nodes = []
seen = set()
if not component:
continue
q = [(c, False) for c in component[:1]]
while q:
way_id, reverse = q.pop()
way_index = way_indices[way_id]
node_coords = self.node_coordinates(self.way_coords, self.way_indptr, way_index)
head, tail = start_end_nodes[way_id]
if reverse:
node_coords = node_coords[::-1]
head, tail = tail, head
for neighbor in way_graph[way_id]:
if neighbor in seen:
continue
neighbor_head, neighbor_tail = start_end_nodes[neighbor]
neighbor_reverse = neighbor_head == head or neighbor_tail == tail
q.append((neighbor, neighbor_reverse))
way_start = 0 if q else 1
poly_nodes.extend(node_coords[way_start:-1])
seen.add(way_id)
polys.append(poly_nodes)
return polys
def include_polygon(self, props):
raise NotImplementedError('Children must implement')
def polygons(self, properties_only=False):
i = 0
for element_id, props, deps in parse_osm(self.filename, dependencies=True):
props = {safe_decode(k): safe_decode(v) for k, v in six.iteritems(props)}
if element_id.startswith('node'):
node_id = long(element_id.split(':')[-1])
lat = props.get('lat')
lon = props.get('lon')
if lat is None or lon is None:
continue
lat, lon = latlon_to_decimal(lat, lon)
if lat is None or lon is None:
continue
if isclose(lat, 90.0):
lat = 89.999
if isclose(lon, 180.0):
lon = 179.999
if 'name' in props and 'place' in props:
self.nodes[node_id] = props
self.coords.append(lon)
self.coords.append(lat)
self.node_ids.append(node_id)
elif element_id.startswith('way'):
way_id = long(element_id.split(':')[-1])
try:
node_indices = [self.binary_search(self.node_ids, node_id) for node_id in deps]
except ValueError:
continue
self.way_ids.append(way_id)
for node_id, node_index in izip(deps, node_indices):
self.way_deps.append(node_id)
self.way_coords.append(self.coords[node_index * 2])
self.way_coords.append(self.coords[node_index * 2 + 1])
self.way_indptr.append(len(self.way_deps))
if deps[0] == deps[-1] and self.include_polygon(props):
way_id_offset = WAY_OFFSET + way_id
if not properties_only:
outer_polys = self.create_polygons([way_id])
inner_polys = []
yield way_id_offset, props, {}, outer_polys, inner_polys
else:
yield way_id_offset, props, {}
elif element_id.startswith('relation'):
if self.node_ids is not None:
self.node_ids = None
if self.coords is not None:
self.coords = None
relation_id = long(element_id.split(':')[-1])
if len(deps) == 0 or not self.include_polygon(props) or props.get('type', '').lower() == 'multilinestring':
continue
outer_ways = []
inner_ways = []
admin_centers = []
for elem_id, elem_type, role in deps:
if role in ('outer', '') and elem_type == 'way':
outer_ways.append(elem_id)
elif role == 'inner' and elem_type == 'way':
inner_ways.append(elem_id)
elif role == 'admin_centre' and elem_type == 'node':
val = self.nodes.get(long(elem_id))
if val is not None:
val['type'] = 'node'
val['id'] = long(elem_id)
admin_centers.append(val)
elif role == 'label' and elem_type == 'node':
val = self.nodes.get(long(elem_id))
if val is not None and val.get('name', six.u('')).lower() == props.get('name', six.u('')).lower():
props.update({k: v for k, v in six.iteritems(val)
if k not in props})
admin_center = {}
if len(admin_centers) == 1:
admin_center = admin_centers[0]
relation_id_offset = RELATION_OFFSET + relation_id
if not properties_only:
outer_polys = self.create_polygons(outer_ways)
inner_polys = self.create_polygons(inner_ways)
yield relation_id_offset, props, admin_center, outer_polys, inner_polys
else:
yield relation_id_offset, props, admin_center
if i % 1000 == 0 and i > 0:
self.logger.info('doing {}s, at {}'.format(element_id.split(':')[0], i))
i += 1
class OSMAdminPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return 'boundary' in props or 'place' in props
class OSMSubdivisionPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return 'landuse' in props or 'place' in props or 'amenity' in props
class OSMBuildingPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return 'building' in props or 'building:part' in props or props.get('type', None) == 'building'
class OSMCountryPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return 'ISO3166-1:alpha2' in props or 'ISO3166-2' in props or (props.get('type', 'relation'), safe_encode(props.get('id', ''))) in osm_admin1_ids
class OSMNeighborhoodPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return osm_definitions.meets_definition(props, osm_definitions.NEIGHBORHOOD)
class OSMPostalCodesPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return props.get('boundary') == 'postal_code'
class OSMAirportsPolygonReader(OSMPolygonReader):
def include_polygon(self, props):
return 'aerodrome' in props