# This is the release version of the plugin file io_import_scene_osm_dev.py # If you would like to make edits, make them in the file io_import_scene_osm_dev.py and the other related modules # To create the release version of io_import_scene_osm_dev.py, executed: # python plugin_builder.py io_import_scene_osm_dev.py: bl_info = { "name": "Import OpenStreetMap (.osm)", "author": "Vladimir Elistratov <vladimir.elistratov@gmail.com>", "version": (1, 0, 0), "blender": (2, 6, 9), "location": "File > Import > OpenStreetMap (.osm)", "description": "Import a file in the OpenStreetMap format (.osm)", "warning": "", "wiki_url": "https://github.com/vvoovv/blender-geo/wiki/Import-OpenStreetMap-(.osm)", "tracker_url": "https://github.com/vvoovv/blender-geo/issues", "support": "COMMUNITY", "category": "Import-Export", } import bpy, bmesh # ImportHelper is a helper class, defines filename and invoke() function which calls the file selector from bpy_extras.io_utils import ImportHelper import sys, os import math # see conversion formulas at # http://en.wikipedia.org/wiki/Transverse_Mercator_projection # and # http://mathworld.wolfram.com/MercatorProjection.html class TransverseMercator: radius = 6378137 def __init__(self, **kwargs): # setting default values self.lat = 0 # in degrees self.lon = 0 # in degrees self.k = 1 # scale factor for attr in kwargs: setattr(self, attr, kwargs[attr]) self.latInRadians = math.radians(self.lat) def fromGeographic(self, lat, lon): lat = math.radians(lat) lon = math.radians(lon-self.lon) B = math.sin(lon) * math.cos(lat) x = 0.5 * self.k * self.radius * math.log((1+B)/(1-B)) y = self.k * self.radius * ( math.atan(math.tan(lat)/math.cos(lon)) - self.latInRadians ) return (x,y) def toGeographic(self, x, y): x = x/(self.k * self.radius) y = y/(self.k * self.radius) D = y + self.latInRadians lon = math.atan(math.sinh(x)/math.cos(D)) lat = math.asin(math.sin(D)/math.cosh(x)) lon = self.lon + math.degrees(lon) lat = math.degrees(lat) return (lat, lon) import xml.etree.cElementTree as etree import inspect, importlib def prepareHandlers(kwArgs): nodeHandlers = [] wayHandlers = [] # getting a dictionary with local variables _locals = locals() for handlers in ("nodeHandlers", "wayHandlers"): if handlers in kwArgs: for handler in kwArgs[handlers]: if isinstance(handler, str): # we've got a module name handler = importlib.import_module(handler) if inspect.ismodule(handler): # iterate through all module functions for f in inspect.getmembers(handler, inspect.isclass): _locals[handlers].append(f[1]) elif inspect.isclass(handler): _locals[handlers].append(handler) if len(_locals[handlers])==0: _locals[handlers] = None return (nodeHandlers if len(nodeHandlers) else None, wayHandlers if len(wayHandlers) else None) class OsmParser: def __init__(self, filename, **kwargs): self.nodes = {} self.ways = {} self.relations = {} self.minLat = 90 self.maxLat = -90 self.minLon = 180 self.maxLon = -180 # self.bounds contains the attributes of the bounds tag of the .osm file if available self.bounds = None (self.nodeHandlers, self.wayHandlers) = prepareHandlers(kwargs) self.doc = etree.parse(filename) self.osm = self.doc.getroot() self.prepare() def prepare(self): allowedTags = set(("node", "way", "bounds")) for e in self.osm: # e stands for element attrs = e.attrib if e.tag not in allowedTags : continue if "action" in attrs and attrs["action"] == "delete": continue if e.tag == "node": _id = attrs["id"] tags = None for c in e: if c.tag == "tag": if not tags: tags = {} tags[c.get("k")] = c.get("v") lat = float(attrs["lat"]) lon = float(attrs["lon"]) # calculating minLat, maxLat, minLon, maxLon # commented out: only imported objects take part in the extent calculation #if lat<self.minLat: self.minLat = lat #elif lat>self.maxLat: self.maxLat = lat #if lon<self.minLon: self.minLon = lon #elif lon>self.maxLon: self.maxLon = lon # creating entry entry = dict( id=_id, e=e, lat=lat, lon=lon ) if tags: entry["tags"] = tags self.nodes[_id] = entry elif e.tag == "way": _id = attrs["id"] nodes = [] tags = None for c in e: if c.tag == "nd": nodes.append(c.get("ref")) elif c.tag == "tag": if not tags: tags = {} tags[c.get("k")] = c.get("v") # ignore ways without tags if tags: self.ways[_id] = dict( id=_id, e=e, nodes=nodes, tags=tags ) elif e.tag == "bounds": self.bounds = { "minLat": float(attrs["minlat"]), "minLon": float(attrs["minlon"]), "maxLat": float(attrs["maxlat"]), "maxLon": float(attrs["maxlon"]) } self.calculateExtent() def iterate(self, wayFunction, nodeFunction): nodeHandlers = self.nodeHandlers wayHandlers = self.wayHandlers if wayHandlers: for _id in self.ways: way = self.ways[_id] if "tags" in way: for handler in wayHandlers: if handler.condition(way["tags"], way): wayFunction(way, handler) continue if nodeHandlers: for _id in self.nodes: node = self.nodes[_id] if "tags" in node: for handler in nodeHandlers: if handler.condition(node["tags"], node): nodeFunction(node, handler) continue def parse(self, **kwargs): def wayFunction(way, handler): handler.handler(way, self, kwargs) def nodeFunction(node, handler): handler.handler(node, self, kwargs) self.iterate(wayFunction, nodeFunction) def calculateExtent(self): def wayFunction(way, handler): wayNodes = way["nodes"] for node in range(len(wayNodes)-1): # skip the last node which is the same as the first ones nodeFunction(self.nodes[wayNodes[node]]) def nodeFunction(node, handler=None): lon = node["lon"] lat = node["lat"] if lat<self.minLat: self.minLat = lat elif lat>self.maxLat: self.maxLat = lat if lon<self.minLon: self.minLon = lon elif lon>self.maxLon: self.maxLon = lon self.iterate(wayFunction, nodeFunction) import os, math import bpy, bmesh import bmesh def extrudeMesh(bm, thickness): """ Extrude bmesh """ geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces) verts_extruded = [v for v in geom["geom"] if isinstance(v, bmesh.types.BMVert)] bmesh.ops.translate(bm, verts=verts_extruded, vec=(0, 0, thickness)) def assignTags(obj, tags): for key in tags: obj[key] = tags[key] class buildings: @staticmethod def condition(tags, way): return "building" in tags @staticmethod def handler(way, parser, kwargs): wayNodes = way["nodes"] numNodes = len(wayNodes)-1 # we need to skip the last node which is the same as the first ones # a polygon must have at least 3 vertices if numNodes<3: return if not kwargs["bm"]: # not a single mesh tags = way["tags"] thickness = kwargs["thickness"] if ("thickness" in kwargs) else 0 osmId = way["id"] # compose object name name = osmId if "addr:housenumber" in tags and "addr:street" in tags: name = tags["addr:street"] + ", " + tags["addr:housenumber"] elif "name" in tags: name = tags["name"] bm = kwargs["bm"] if kwargs["bm"] else bmesh.new() verts = [] for node in range(numNodes): node = parser.nodes[wayNodes[node]] v = kwargs["projection"].fromGeographic(node["lat"], node["lon"]) verts.append( bm.verts.new((v[0], v[1], 0)) ) bm.faces.new(verts) if not kwargs["bm"]: thickness = kwargs["thickness"] if ("thickness" in kwargs) else 0 # extrude if thickness>0: extrudeMesh(bm, thickness) bm.normal_update() mesh = bpy.data.meshes.new(osmId) bm.to_mesh(mesh) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.update() # final adjustments obj.select = True # assign OSM tags to the blender object assignTags(obj, tags) class highways: @staticmethod def condition(tags, way): return "highway" in tags @staticmethod def handler(way, parser, kwargs): wayNodes = way["nodes"] numNodes = len(wayNodes) # we need to skip the last node which is the same as the first ones # a way must have at least 2 vertices if numNodes<2: return if not kwargs["bm"]: # not a single mesh tags = way["tags"] osmId = way["id"] # compose object name name = tags["name"] if "name" in tags else osmId bm = kwargs["bm"] if kwargs["bm"] else bmesh.new() verts = [] prevVertex = None for node in range(numNodes): node = parser.nodes[wayNodes[node]] v = kwargs["projection"].fromGeographic(node["lat"], node["lon"]) v = bm.verts.new((v[0], v[1], 0)) if prevVertex: bm.edges.new([prevVertex, v]) prevVertex = v if not kwargs["bm"]: mesh = bpy.data.meshes.new(osmId) bm.to_mesh(mesh) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.update() # final adjustments obj.select = True # assign OSM tags to the blender object assignTags(obj, tags) import os, math import bpy, bmesh import bmesh def extrudeMesh(bm, thickness): """ Extrude bmesh """ geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces) verts_extruded = [v for v in geom["geom"] if isinstance(v, bmesh.types.BMVert)] bmesh.ops.translate(bm, verts=verts_extruded, vec=(0, 0, thickness)) def assignTags(obj, tags): for key in tags: obj[key] = tags[key] class buildings: @staticmethod def condition(tags, way): return "building" in tags @staticmethod def handler(way, parser, kwargs): wayNodes = way["nodes"] numNodes = len(wayNodes)-1 # we need to skip the last node which is the same as the first ones # a polygon must have at least 3 vertices if numNodes<3: return if not kwargs["bm"]: # not a single mesh tags = way["tags"] thickness = kwargs["thickness"] if ("thickness" in kwargs) else 0 osmId = way["id"] # compose object name name = osmId if "addr:housenumber" in tags and "addr:street" in tags: name = tags["addr:street"] + ", " + tags["addr:housenumber"] elif "name" in tags: name = tags["name"] bm = kwargs["bm"] if kwargs["bm"] else bmesh.new() verts = [] for node in range(numNodes): node = parser.nodes[wayNodes[node]] v = kwargs["projection"].fromGeographic(node["lat"], node["lon"]) verts.append( bm.verts.new((v[0], v[1], 0)) ) bm.faces.new(verts) if not kwargs["bm"]: thickness = kwargs["thickness"] if ("thickness" in kwargs) else 0 # extrude if thickness>0: extrudeMesh(bm, thickness) bm.normal_update() mesh = bpy.data.meshes.new(osmId) bm.to_mesh(mesh) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.update() # final adjustments obj.select = True # assign OSM tags to the blender object assignTags(obj, tags) class highways: @staticmethod def condition(tags, way): return "highway" in tags @staticmethod def handler(way, parser, kwargs): wayNodes = way["nodes"] numNodes = len(wayNodes) # we need to skip the last node which is the same as the first ones # a way must have at least 2 vertices if numNodes<2: return if not kwargs["bm"]: # not a single mesh tags = way["tags"] osmId = way["id"] # compose object name name = tags["name"] if "name" in tags else osmId bm = kwargs["bm"] if kwargs["bm"] else bmesh.new() verts = [] prevVertex = None for node in range(numNodes): node = parser.nodes[wayNodes[node]] v = kwargs["projection"].fromGeographic(node["lat"], node["lon"]) v = bm.verts.new((v[0], v[1], 0)) if prevVertex: bm.edges.new([prevVertex, v]) prevVertex = v if not kwargs["bm"]: mesh = bpy.data.meshes.new(osmId) bm.to_mesh(mesh) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) bpy.context.scene.update() # final adjustments obj.select = True # assign OSM tags to the blender object assignTags(obj, tags) import bmesh def extrudeMesh(bm, thickness): """ Extrude bmesh """ geom = bmesh.ops.extrude_face_region(bm, geom=bm.faces) verts_extruded = [v for v in geom["geom"] if isinstance(v, bmesh.types.BMVert)] bmesh.ops.translate(bm, verts=verts_extruded, vec=(0, 0, thickness)) class ImportOsm(bpy.types.Operator, ImportHelper): """Import a file in the OpenStreetMap format (.osm)""" bl_idname = "import_scene.osm" # important since its how bpy.ops.import_scene.osm is constructed bl_label = "Import OpenStreetMap" bl_options = {"UNDO"} # ImportHelper mixin class uses this filename_ext = ".osm" filter_glob = bpy.props.StringProperty( default="*.osm", options={"HIDDEN"}, ) ignoreGeoreferencing = bpy.props.BoolProperty( name="Ignore existing georeferencing", description="Ignore existing georeferencing and make a new one", default=False, ) singleMesh = bpy.props.BoolProperty( name="Import as a single mesh", description="Import OSM objects as a single mesh instead of separate Blender objects", default=False, ) importBuildings = bpy.props.BoolProperty( name="Import buildings", description="Import building outlines", default=True, ) importHighways = bpy.props.BoolProperty( name="Import roads and paths", description="Import roads and paths", default=False, ) thickness = bpy.props.FloatProperty( name="Thickness", description="Set thickness to make OSM building outlines extruded", default=0, ) def execute(self, context): # setting active object if there is no active object if context.mode != "OBJECT": # if there is no object in the scene, only "OBJECT" mode is provided if not context.scene.objects.active: context.scene.objects.active = context.scene.objects[0] bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.select_all(action="DESELECT") name = os.path.basename(self.filepath) if self.singleMesh: self.bm = bmesh.new() else: self.bm = None # create an empty object to parent all imported OSM objects bpy.ops.object.empty_add(type="PLAIN_AXES", location=(0, 0, 0)) parentObject = context.active_object self.parentObject = parentObject parentObject.name = name self.read_osm_file(context) if self.singleMesh: bm = self.bm # extrude if self.thickness>0: extrudeMesh(bm, self.thickness) bm.normal_update() mesh = bpy.data.meshes.new(name) bm.to_mesh(mesh) obj = bpy.data.objects.new(name, mesh) bpy.context.scene.objects.link(obj) # remove double vertices context.scene.objects.active = obj bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.remove_doubles() bpy.ops.mesh.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="OBJECT") bpy.context.scene.update() else: # perform parenting context.scene.objects.active = parentObject bpy.ops.object.parent_set() bpy.ops.object.select_all(action="DESELECT") return {"FINISHED"} def read_osm_file(self, context): scene = context.scene wayHandlers = [] if self.importBuildings: wayHandlers.append(buildings) if self.importHighways: wayHandlers.append(highways) osm = OsmParser(self.filepath, # possible values for wayHandlers and nodeHandlers list elements: # 1) a string name for the module containing classes (all classes from the modules will be used as handlers) # 2) a python variable representing the module containing classes (all classes from the modules will be used as handlers) # 3) a python variable representing the class # Examples: # wayHandlers = [buildings, highways] # wayHandlers = [handlers.buildings] # wayHandlers = [handlers] # wayHandlers = ["handlers"] wayHandlers = wayHandlers ) if "latitude" in scene and "longitude" in scene and not self.ignoreGeoreferencing: lat = scene["latitude"] lon = scene["longitude"] else: if osm.bounds and self.importHighways: # If the .osm file contains the bounds tag, # use its values as the extent of the imported area. # Highways may go far beyond the values of the bounds tag. # A user might get confused if higways are used in the calculation of the extent of the imported area. bounds = osm.bounds lat = (bounds["minLat"] + bounds["maxLat"])/2 lon = (bounds["minLon"] + bounds["maxLon"])/2 else: lat = (osm.minLat + osm.maxLat)/2 lon = (osm.minLon + osm.maxLon)/2 scene["latitude"] = lat scene["longitude"] = lon osm.parse( projection = TransverseMercator(lat=lat, lon=lon), thickness = self.thickness, bm = self.bm # if present, indicates the we need to create as single mesh ) # Only needed if you want to add into a dynamic menu def menu_func_import(self, context): self.layout.operator(ImportOsm.bl_idname, text="OpenStreetMap (.osm)") def register(): bpy.utils.register_class(ImportOsm) bpy.types.INFO_MT_file_import.append(menu_func_import) def unregister(): bpy.utils.unregister_class(ImportOsm) bpy.types.INFO_MT_file_import.remove(menu_func_import)