Blender Hoogtedata Addon

Voor wat het waard is, geef ik hier vast een ‘werkende’ op_get_bag.py code. Leg hem gerust naast de oude code, het is een stuk kleiner omdat er minder wordt gebruikt, ik heb wel zoveel mogelijk er in laten staan.

Probleem met deze code is dus dat de huisjes een heeeel eind vanaf de AHN map in Blender worden geladen,… je zult ze er dus zelf heen moeten slepen en dat lukt mij niet (ik ben erg onhandig in Blender)
Dit heeft mogelijk te maken met de diverse Spatial Systems. Wellicht dat de auteur @thomaskole van deze plugin er iets over kan zeggen. Het meeste fix werk is al gedaan, mocht ik nog iets moeten doen, dan hoor ik het graag…

import bpy
import urllib.request
import json
import zipfile
import os
import bmesh
from bpy.types import Operator

from . import utils

from bpy.props import (
        StringProperty,
        BoolProperty,
        IntProperty,
        FloatProperty,
        FloatVectorProperty,
        EnumProperty,
        )

tileset = {}

# Functie voor ophalen tiles binnen de basemap-bbox
def get_tiles_in_basemap(error_margin):
    get_tileset()
    global tileset

    bboxes = list()
    basemap = utils.get_basemap_obj()
    shiftLon = basemap.location.x
    shiftLat = basemap.location.y
    lon = bpy.context.scene["longitude"]
    lat = bpy.context.scene["latitude"]

    rd = utils.Rijksdriehoek()
    rd.from_wgs(lat, lon)
    lon = rd.rd_x + shiftLon
    lat = rd.rd_y + shiftLat

    sscale = utils.get_scene_scale_m()
    error = error_margin
    worldRect = [lon - sscale[0]/2 - error, lat - sscale[1]/2 - error, lon + sscale[0]/2 + error, lat + sscale[1]/2 + error]

    # Pas de nieuwe structuur aan
    for feature in tileset.get('features', []):
        bbox = feature["bbox"]
        uri = feature["properties"].get("obj_download", "")
        if isRectangleOverlap(bbox, worldRect):
            bboxes.append([bbox, uri])
    return bboxes

# Nieuwe get_tileset functie om de bounding box API aan te roepen
def get_tileset():
    global tileset
    if tileset:
        print("Tileset already cached")
    else:
        print("Fetching tileset with new bbox API")
        
        # Bounding box ophalen en API-request met bbox
        basemap = utils.get_basemap_obj()
        lon, lat = bpy.context.scene["longitude"], bpy.context.scene["latitude"]
        rd = utils.Rijksdriehoek()
        rd.from_wgs(lat, lon)
        lon, lat = rd.rd_x + basemap.location.x, rd.rd_y + basemap.location.y
        sscale = utils.get_scene_scale_m()
        bbox = f"{lon - sscale[0]/2},{lat - sscale[1]/2},{lon + sscale[0]/2},{lat + sscale[1]/2}"

        # URL samenstellen voor nieuwe API-oproep
        url = f"https://data.3dbag.nl/api/BAG3D/wfs?version=1.1.0&request=GetFeature&typename=BAG3D:Tiles&outputFormat=application/json&srsname=EPSG:28992&bbox={bbox},EPSG:28992"
        
        try:
            req = urllib.request.Request(url, None, utils.get_request_header())
            handle = urllib.request.urlopen(req, timeout=10)
            data = handle.read()
            handle.close()
            tileset = json.loads(data)
        except Exception as e:
            print(f"Error fetching tileset: {e}")
            tileset = {}

# Functie om overlapping te controleren
def isRectangleOverlap(R1, R2):
    return not (R1[0] >= R2[2] or R1[2] <= R2[0] or R1[3] <= R2[1] or R1[1] >= R2[3])

class op_get_bag(Operator):
    bl_idname = "gis.get_bag"
    bl_label = "Get 3D BAG"
    bl_description = "Download 3D buildings to match your existing basemap"
    bl_options = {'REGISTER', 'UNDO'}
    
    bag_lod: EnumProperty(
        name="Detail",
        description="3D BAG in verschillende levels.",
        items=(
            ('lod12', "LOD 1.2 (laag detail)", ""),
            ('lod13', "LOD 1.3 (medium detail)", ""),
            ('lod22', "LOD 2.2 (hoog detail)", "")
        ),
        default='lod22',
    )
    
    excess_cleanup: EnumProperty(
        name="Excess",
        description="Hoe te verwerken buiten de basemap-grenzen?",
        items=(
            ('CUT', "Cut away", "Verwijdert buiten basemap-grenzen"),
            ('INCLUDE', "Inclusief overhang", "Houdt gebouwen die grenzen aan basemap"),
            ('EXCLUDE', "Exclusief overhang", "Verwijdert gebouwen niet geheel binnen de basemap"),
            ('NOTHING', "Niets doen", "Behoudt alle tiles")
        ),
        default='CUT',
    )
    
    error_margin: FloatProperty(
        name="Excess",
        description="Gebruik dit om grenzen nauwkeurig aan te passen.",
        default=500,
    )

    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self, width=250)

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        if(utils.is_scene_georef()):
            row = layout.row()
            row.scale_y = 0.6
            row.label(text="Get Dutch 3D buildings to", icon='INFO')
            row = layout.row()
            row.scale_y = 0.6
            row.label(text="match your existing basemap")
            layout.separator()
            layout.row()
            layout.row()
            
            row = layout.row()
            row.label(text="Download:", icon='URL')
            row = layout.row()
            row.prop(self, "bag_lod")
            row = layout.row()
            row.prop(self, "error_margin")
            numtiles = len(get_tiles_in_basemap(self.error_margin))
            row = layout.row()             
            row.scale_y = 0.6
            row.label(text="Tiles in basemap: " + str(numtiles))
            
            layout.row()
            layout.row()
                  
            row = layout.row()
            row.label(text="Mesh:", icon='MOD_DISPLACE')
            row = layout.row()
            row.prop(self, "excess_cleanup")
            
        else:
            row = layout.row()
            row.scale_y = 0.6
            row.label(text="Scene has no basemap!", icon='ERROR')
            row = layout.row()
            row.scale_y = 0.6
            row.label(text="use (GIS > Web geodata > Basemap)")

    def execute(self, context):
        print("\n\n########## BLENDER HOOGTEDATA ADDON ##########\n\n")
        utils.ensure_basemap_scale()
        bboxes = get_tiles_in_basemap(self.error_margin)
        
        print("Number of tiles in search: " + str(len(bboxes)))

        for b in bboxes:
            # Gebruik alleen het laatste deel van de URI als id                         
            id = os.path.basename(b[1]).replace("-obj.zip", "")
            print(f"\n\nGetting 3D BAG tile: {b[1]}")
            try:
                req = urllib.request.Request(b[1], None, utils.get_request_header())
                handle = urllib.request.urlopen(req, timeout=10)
                data = handle.read()
                handle.close()
                
                file_path = os.path.join(utils.get_tmp_path(), f"bag3d_{id}.zip")
                print(f"Debugging file_path path: {file_path}")
                tmpzipfolder = os.path.join(utils.get_tmp_path(), f"bag3d_{id}/")
                print(f"Debugging tmpzipfolder path: {tmpzipfolder}")
                print(f"utils.get_tmp_path(): {utils.get_tmp_path()}")                

                # Zorg ervoor dat het tijdelijke pad bestaat
                os.makedirs(tmpzipfolder, exist_ok=True)
                print(f"Created temporary folder if it didn't exist: {tmpzipfolder}")
                                                    
                with open(file_path, 'wb') as zip_file:
                    zip_file.write(data)

                print(f"Selected LOD: {self.bag_lod}")  # Debug de geselecteerde LOD                
                with zipfile.ZipFile(file_path, 'r') as zip_ref:
                    zip_ref.extractall(tmpzipfolder)
                    for obj_file in os.listdir(tmpzipfolder):
                        print(f"Checking file: {obj_file}")  # Debug de bestandsnaam
                        if str(self.bag_lod.lower()) in obj_file.lower() and obj_file.lower().endswith('obj'):
                            # Oude Blender 2.x en 3.x methode::
                            #bpy.ops.import_scene.obj(filepath=os.path.join(tmpzipfolder, obj_file), use_split_objects=False, use_split_groups=False, axis_forward='Y', axis_up='Z')
                            # Nieuwe Blender 4.x methode:
                            bpy.ops.wm.obj_import(filepath=os.path.join(tmpzipfolder, obj_file))
            except Exception as e:
                print(f"Error with tile {id}: {e}")

        print("\n\n########## # ##########\n\n")
        return {'FINISHED'}

Ik heb overigens geprobeerd de AHN hoogte data uit Blender, als object (.obj) en ook als .stl te exporteren. Dat lukt. Echter als ik die dan inlees in Sketchup, dan wordt Sketchup zoooo traag (logisch, want het zijn allemaal vectors/punten/etc…).

Weet iemand of er niet een manier is dat ik het als het ware plat kan slaan in Blender (niet letterlijk plat, maar als 1 object/ding) en het dan als .obj of .stl exporteer zodat Sketchup er niet zo’n moeite mee heeft?

Momenteel ziet het er zo uit in Sketchup en dat is echt een beetje te zwaar…

Met de functie Remesh kun je het nabewerken. Niet te grote gebieden nemen. Beperk het tot een perceel.

Hoi!

Met dank aan de - onverwachte - maar zeer gewaardeerde hulp van @roeller lijkt de add-on weer klaar om getest te worden. Ik heb de afgelopen tijd geen tijd gehad om te kijken naar de API-veranderingen, maar roeller is zo vriendelijk geweest dit voor me te doen. Er waren nog wat kleine dingetjes maar die lijken nu opgelost.

Hopelijk veel plezier!

1 like

Tip: gebruik de decimate modifier.

https://docs.blender.org/manual/en/latest/modeling/modifiers/generate/decimate.html

In een gebied met veel water geeft AHN importeren niet altijd het gewenste mesh. Water wordt niet als ‘no data’ achterwege gelaten. ‘Jumpflood average, best for DTM’ levert wel mooi gesloten vlakken aan land maar dit zou het niet voor water moeten doen (voor 0.5m). Om dit iets beter te sturen kun je ervoor kiezen in CloudCompare de meshing te doen (in RD) en de overgebleven gaten te vullen in Blender. Dat is de aanpak die ik vaak doe.

Screenshot voor inzicht in de verschillende methoden:

Er zijn nog andere methodes:

Als geen van deze bevalt, kun je altijd hierna in edit mode gaan en handmatig iets met die data doen.
Als je edit mode in gaat dan zijn degene met no data geselecteerd.