Skip to content

Instantly share code, notes, and snippets.

@batFINGER
Created September 8, 2016 17:54
Show Gist options
  • Save batFINGER/87ad90900be589281eeeaa83697d541f to your computer and use it in GitHub Desktop.
Save batFINGER/87ad90900be589281eeeaa83697d541f to your computer and use it in GitHub Desktop.
Blender GEO OSM import tweak
# 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, execute:
# python plugin_builder.py io_import_scene_osm_dev.py
bl_info = {
"name": "Import OpenStreetMap (.osm)",
"author": "Vladimir Elistratov <vladimir.elistratov@gmail.com> and gtoonstra",
"version": (1, 1, 0),
"blender": (2, 7, 4),
"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 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 webbrowser
url = "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NNQBWQ6TH2N7N"
class Donate(bpy.types.Operator):
bl_idname = "blender_geo.donate"
bl_label = "Donate!"
bl_description = "If you like the add-on please donate"
bl_options = {"REGISTER"}
def execute(self, context):
webbrowser.open_new_tab(url)
return {'FINISHED'}
@classmethod
def gui(cls, layout, addonName):
box = layout.box()
box.label("If you like \'{}\' add-on".format(addonName))
box.label("please donate!")
box.operator(cls.bl_idname, icon='HELP')
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()
# A 'node' in osm: <node id="2599524395" visible="true" version="1" changeset="19695235" timestamp="2013-12-29T12:40:05Z" user="It's so funny_BAG" uid="1204291" lat="52.0096203" lon="4.3612318"/>
# A 'way' in osm:
# <way id="254138613" visible="true" version="1" changeset="19695235" timestamp="2013-12-29T12:58:34Z" user="It's so funny_BAG" uid="1204291">
# <nd ref="2599536906"/>
# <nd ref="2599537009"/>
# <nd ref="2599537013"/>
# <nd ref="2599537714"/>
# <nd ref="2599537988"/>
# <nd ref="2599537765"/>
# <nd ref="2599536906"/>
# <tag k="building" v="yes"/>
# <tag k="ref:bag" v="503100000022259"/>
# <tag k="source" v="BAG"/>
# <tag k="source:date" v="2013-11-26"/>
# <tag k="start_date" v="1850"/>
# </way>
#
# The parser creates two dictionaries: {}
# parser.nodes[ node_id ] = { "lat":lat, "lon":lon, "e":e, "_id":_id }
# parser.ways[ way_id ] = { "nodes":["12312312","1312313123","345345453",etc..], "tags":{"building":"yes", etc...}, "e":e, "_id":_id }
# So the way data is stored can seem a little complex
#
# The parser is then passed, along with the 'way' or 'node' object of the parser
# to the handler functions of buildings and highways, where they are used to convert
# them into blender objects.
#
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 bpy, bmesh
def extrudeMesh(bm, thickness, face=None):
"""
Extrude bmesh
"""
geom = bmesh.ops.extrude_face_region(bm, geom=(face,) if face else 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 assignMaterials(obj, materialname, color, faces):
# Get material
if bpy.data.materials.get(materialname) is not None:
mat = bpy.data.materials[materialname]
else:
# create material
mat = bpy.data.materials.new(name=materialname)
mat.diffuse_color = color
# Assign it to object
matidx = len(obj.data.materials)
obj.data.materials.append(mat)
for face in faces:
face.material_index = matidx
def assignTags(obj, tags):
for key in tags:
obj[key] = tags[key]
def parse_scalar_and_unit( htag ):
for i,c in enumerate(htag):
if not c.isdigit():
return int(htag[:i]), htag[i:].strip()
return int(htag), ""
class Buildings:
@staticmethod
def condition(tags, way):
return "building" in tags
@staticmethod
def handler(way, parser, kwargs):
singleMesh = kwargs["bm"]
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 singleMesh:
tags = way["tags"]
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 singleMesh 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)) )
face = bm.faces.new(verts)
tags = way["tags"]
if "height" in tags:
# There's a height tag. It's parsed as text and could look like: 25, 25m, 25 ft, etc.
thickness = parse_scalar_and_unit(tags["height"])[0]
else:
thickness = kwargs["thickness"] if ("thickness" in kwargs) else 0.
if "building:levels" in tags:
if "height" not in tags:
thickness = kwargs["level_height"] * int(tags["building:levels"])
# extrude
if thickness > 0.:
extrudeMesh(bm, thickness, face if singleMesh else None)
if not singleMesh:
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)
# EDIT not sure needed here.
#bpy.context.scene.update()
# final adjustments
obj.select = True
# assign OSM tags to the blender object
assignTags(obj, tags)
# EDIT ...
#assignMaterials( obj, "wall", (1,0.7,0.0), mesh.polygons[:] )
#assignMaterials( obj, "roof", (1.0,0.0,0.0), [mesh.polygons[-1]] )
class BuildingParts:
@staticmethod
def condition(tags, way):
return "building:part" in tags
@staticmethod
def handler(way, parser, kwargs):
singleMesh = kwargs["bm"]
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
tags = way["tags"]
if not singleMesh:
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"]
min_height = 0
height = 0
if "min_height" in tags:
# There's a height tag. It's parsed as text and could look like: 25, 25m, 25 ft, etc.
min_height = parse_scalar_and_unit(tags["min_height"])[0]
if "height" in tags:
# There's a height tag. It's parsed as text and could look like: 25, 25m, 25 ft, etc.
height = parse_scalar_and_unit(tags["height"])[0]
if "building:levels" in tags:
if "height" not in tags:
height = kwargs["level_height"] * int(tags["building:levels"])
if "building:min_level" in tags:
if "min_height" not in tags:
min_height = kwargs["level_height"] * int(tags["building:min_level"])
bm = kwargs["bm"] if singleMesh else bmesh.new()
verts = []
roofverts = []
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], min_height)) )
# EDIT add roof.
'''
if "roof:shape" in tags:
# EDIT quick hack to add a roof
roofverts.append(bm.verts.new((v[0], v[1], height + 10)))
'''
face = bm.faces.new(verts)
# extrude
if (height-min_height)>0:
extrudeMesh(bm, (height-min_height), face if singleMesh else None)
if len(roofverts):
bm.faces.new(roofverts)
if not singleMesh:
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)
# EDIT don't think it's needed
#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()
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)
class Naturals:
@staticmethod
def condition(tags, way):
return "natural" 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
if numNodes == 1:
# This is some point "natural".
# which we ignore for now (trees, etc.)
pass
numNodes = numNodes - 1
# a polygon must have at least 3 vertices
if numNodes<3: return
tags = way["tags"]
if not kwargs["bm"]: # not a single mesh
osmId = way["id"]
# compose object name
name = osmId
if "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"]:
tags = way["tags"]
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)
naturaltype = tags["natural"]
color = (0.5,0.5,0.5)
if naturaltype == "water":
color = (0,0,1)
assignMaterials( obj, naturaltype, color, [mesh.polygons[0]] )
import bpy, bmesh
def extrudeMesh(bm, thickness, face=None):
"""
Extrude bmesh
"""
geom = bmesh.ops.extrude_face_region(bm, geom=(face,) if face else 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 assignMaterials(obj, materialname, color, faces):
# Get material
if bpy.data.materials.get(materialname) is not None:
mat = bpy.data.materials[materialname]
else:
# create material
mat = bpy.data.materials.new(name=materialname)
mat.diffuse_color = color
# Assign it to object
matidx = len(obj.data.materials)
obj.data.materials.append(mat)
for face in faces:
face.material_index = matidx
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,
)
importNaturals = bpy.props.BoolProperty(
name="Import naturals",
description="Import natural outlines",
default=False,
)
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,
)
level_height = bpy.props.FloatProperty(
name="Level Height",
description="Set Height of building level",
default = 3.0
)
scale = bpy.props.FloatProperty(
name="Scale",
description="Scale the model",
default = 0.05,
)
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()
parentObject.scale *= self.scale
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)
wayHandlers.append(BuildingParts)
if self.importNaturals:
wayHandlers.append(Naturals)
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
level_height = self.level_height
)
def draw(self, context):
layout = self.layout
Donate.gui(
layout,
self.bl_label
)
layout.row().prop(self, "scale")
layout.row().prop(self, "ignoreGeoreferencing")
layout.row().prop(self, "singleMesh")
layout.row().prop(self, "importBuildings")
layout.row().prop(self, "importNaturals")
layout.row().prop(self, "importHighways")
layout.row().prop(self, "thickness")
layout.row().prop(self, "level_height")
# 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(Donate)
bpy.utils.register_class(ImportOsm)
bpy.types.INFO_MT_file_import.append(menu_func_import)
def unregister():
bpy.utils.unregister_class(Donate)
bpy.utils.unregister_class(ImportOsm)
bpy.types.INFO_MT_file_import.remove(menu_func_import)
# This allows you to run the script directly from blenders text editor
# to test the addon without having to install it.
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment