I'm interested in calculating the shadow of building footprints, considering the height of buildings, in a certain time and date of the year. I have an attribute including building footprint and height of each of them (attached). I've tried 'sun shadow' tool. The results are not convincing. The shadow volume falls below the surface terrain which is obviously strange! Have a look:
Exploring the web I came across a script posted on Geographic Information Systems Stack Exchange. It almost looks like what I would be interested to do. There is a script where one could calculate the 2D shadow of buildings considering their height, however, not specified for a certain time or date of the year.
here is the link to the web and the script:
import arcpy
import os
import math
import multiprocessing
import time
############################ Configuration: ##############################
# Specify scratch workspace
scratchws = r"c:\temp\bldgshadowtest" # MUST be a folder, not a geodatabase!
# Specify output field name for the original FID
origfidfield = "ORIG_FID"
# Specify the number of processors (CPU cores) to use (0 to use all available)
cores = 0
# Specify per-process feature count limit, tune for optimal
# performance/memory utilization (0 for input row count divided by cores)
procfeaturelimit = 0
# TIP: Set 'cores' to 1 and 'procfeaturelimit' to 0 to avoid partitioning and
# multiprocessing completely
################################################################################
def message(msg, severity=0):
print msg
try:
for string in msg.split('\n'):
if severity == 0:
arcpy.AddMessage(string)
elif severity == 1:
arcpy.AddWarning(string)
elif severity == 2:
arcpy.AddError(string)
except:
pass
def getOidRanges(inputFC, oidfield, count):
oidranges = []
if procfeaturelimit > 0:
message("Partitioning row ID ranges ...")
rows = arcpy.SearchCursor(inputFC, "", "", oidfield, "%s A" % oidfield)
minoid = -1
maxoid = -1
for r, row in enumerate(rows):
interval = r % procfeaturelimit
if minoid < 0 and (interval == 0 or r == count - 1):
minoid = row.getValue(oidfield)
if maxoid < 0 and (interval == procfeaturelimit - 1 or r == count - 1):
maxoid = row.getValue(oidfield)
if minoid >= 0 and maxoid >= 0:
oidranges.append([minoid, maxoid])
minoid = -1
maxoid = -1
del row, rows
return oidranges
def computeShadows(inputFC, outputFC, oidfield, shapefield, heightfield, azimuth, altitude, outputSR="", whereclause=""):
# Set outputs to be overwritten just in case; each subprocess gets its own environment settings
arcpy.env.overwriteOutput=True
# Create in-memory feature class for holding the shadow polygons
tempshadows = r"in_memory\tempshadows"
arcpy.CreateFeatureclass_management(
"in_memory",
"tempshadows",
"POLYGON", "", "", "",
outputSR)
arcpy.AddField_management(tempshadows, origfidfield, "LONG")
# Open a cursor on the input feature class
searchfields = ",".join([heightfield, oidfield, shapefield])
rows = arcpy.SearchCursor(inputFC, whereclause, "", searchfields)
# Open an insert cursor on the in-memory feature class
tempinscur = arcpy.InsertCursor(tempshadows)
# Create array for holding shadow polygon vertices
arr = arcpy.Array()
# Compute the shadow offsets.
spread = 1/math.tan(altitude)
# Read the input features
for row in rows:
oid = int(row.getValue(oidfield))
shape = row.getValue(shapefield)
height = float(row.getValue(heightfield))
# Compute the shadow offsets.
x = -height * spread * math.sin(azimuth)
y = -height * spread * math.cos(azimuth)
# Copy the original shape as a new feature
tempnewrow = tempinscur.newRow()
tempnewrow.setValue(origfidfield, oid) # Copy the original FID value to the new feature
tempnewrow.shape = shape
tempinscur.insertRow(tempnewrow)
# Compute the wall shadow polygons and insert them into the in-memory feature class
for part in shape:
for i,j in enumerate(range(1,part.count)):
pnt0 = part
pnt1 = part
if pnt0 is None or pnt1 is None:
continue # skip null points so that inner wall shadows can also be computed
# Compute the shadow offset points
pnt0offset = arcpy.Point(pnt0.X+x,pnt0.Y+y)
pnt1offset = arcpy.Point(pnt1.X+x,pnt1.Y+y)
# Construct the shadow polygon and insert it to the in-memory feature class
[arr.add(pnt) for pnt in [pnt0,pnt1,pnt1offset,pnt0offset,pnt0]]
tempnewrow.shape = arr
tempnewrow.setValue(origfidfield, oid) # Copy the original FID value to the new feature
tempinscur.insertRow(tempnewrow)
arr.removeAll() # Clear the array so it can be reused
# Clean up the insert cursor
del tempnewrow, tempinscur
# Dissolve the shadow polygons to the output feature class
dissolved = arcpy.Dissolve_management(tempshadows, outputFC, origfidfield).getOutput(0)
# Clean up the in-memory workspace
arcpy.Delete_management("in_memory")
return dissolved
if __name__ == "__main__":
arcpy.env.overwriteOutput=True
# Read in parameters
inputFC = arcpy.GetParameterAsText(0)
outputFC = arcpy.GetParameterAsText(1)
heightfield = arcpy.GetParameterAsText(2) #Must be in the same units as the coordinate system!
azimuth = math.radians(float(arcpy.GetParameterAsText(3))) #Must be in degrees
altitude = math.radians(float(arcpy.GetParameterAsText(4))) #Must be in degrees
# Get field names
desc = arcpy.Describe(inputFC)
shapefield = desc.shapeFieldName
oidfield = desc.oidFieldName
count = int(arcpy.GetCount_management(inputFC).getOutput(0))
message("Total features to process: %d" % count)
#Export output spatial reference to string so it can be pickled by multiprocessing
if arcpy.env.outputCoordinateSystem:
outputSR = arcpy.env.outputCoordinateSystem.exportToString()
elif desc.spatialReference:
outputSR = desc.spatialReference.exportToString()
else:
outputSR = ""
# Configure partitioning
if cores == 0:
cores = multiprocessing.cpu_count()
if cores > 1 and procfeaturelimit == 0:
procfeaturelimit = int(math.ceil(float(count)/float(cores)))
# Start timing
start = time.clock()
# Partition row ID ranges by the per-process feature limit
oidranges = getOidRanges(inputFC, oidfield, count)
if len(oidranges) > 0: # Use multiprocessing
message("Computing shadow polygons; using multiprocessing (%d processes, %d jobs of %d maximum features each) ..." % (cores, len(oidranges), procfeaturelimit))
# Create a Pool of subprocesses
pool = multiprocessing.Pool(cores)
jobs = []
# Get the appropriately delmited field name for the OID field
oidfielddelimited = arcpy.AddFieldDelimiters(inputFC, oidfield)
# Ensure the scratch workspace folder exists
if not os.path.exists(scratchws):
os.mkdir(scratchws)
for o, oidrange in enumerate(oidranges):
# Build path to temporary output feature class (dissolved shadow polygons)
# Named e.g. <scratchws>\dissolvedshadows0000.shp
tmpoutput = os.path.join(scratchws, "%s%04d.shp" % ("dissolvedshadows", o))
# Build a where clause for the given OID range
whereclause = "%s >= %d AND %s <= %d" % (oidfielddelimited, oidrange[0], oidfielddelimited, oidrange[1])
# Add the job to the multiprocessing pool asynchronously
jobs.append(pool.apply_async(computeShadows, (inputFC, tmpoutput, oidfield, shapefield, heightfield, azimuth, altitude, outputSR, whereclause)))
# Clean up worker pool; waits for all jobs to finish
pool.close()
pool.join()
# Get the resulting outputs (paths to successfully computed dissolved shadow polygons)
results = [job.get() for job in jobs]
try:
# Merge the temporary outputs
message("Merging temporary outputs into output feature class %s ..." % outputFC)
arcpy.Merge_management(results, outputFC)
finally:
# Clean up temporary data
message("Deleting temporary data ...")
for result in results:
message("Deleting %s" % result)
try:
arcpy.Delete_management(result)
except:
pass
else: # Use a single process
message("Computing shadow polygons; using single processing ...")
computeShadows(inputFC, outputFC, oidfield, shapefield, heightfield, azimuth, altitude, outputSR)
# Stop timing and report duration
end = time.clock()
duration = end - start
hours, remainder = divmod(duration, 3600)
minutes, seconds = divmod(remainder, 60)
message("Completed in %d:%d:%f" % (hours, minutes, seconds))
Is there a way to convert this into a ArcToolbox? Unfortunately I have no knowledge on scripting
I appreciate any feedback and tip. I have attached
Cheers
it stays at the end of my Question:
Okay, did have a look at this.
And, yes, those shadow multipatches produced by the shadow volume tool do look rather odd. (my version 10.4.1).
Bit of a mystery why they should protrude into the ground like that.
Did load the original shapefile into a fgdb.
Then added Z, set to 0.0.
Checked the geometry as well.
These shadows from a single time at 12 noon today.
Is this a bug? Been a long time since I used any of this shadow stuff. I can't recall it generating shadows which are +200m under the Z values.
Is there someone in the 3d analyst team we could tag here?
Well, thats what I was dealing with. It looks strange, doesn't it...!
I found an answer to it on GIS.exchange. I understood his point, coulnd't follow the step he mentioned. Pls take a look at it and see i you get the steps...
The methodology is pretty easy.
As you can see i did construct a simple polygon to cover the area of the data with its Z also at 0.
In ArcScene, extrude it to a ht bigger than your building, I think I used 20m.
Then save this as a layer file. Then in tool box, use the 3d Analyst tools / Conversion / Layer 3d to Feature class to make this a multipatch.
Then use the 3d analyst tools / 3d features / Intersect 3d tool to intersect the big box with the shadows.
You are left with only the parts of the shadows which are above ground.
It's been my experience that multiprocessing and tool box don't play well with one another. You're be better off running as a stand alone script.
Has anyone been able to get this script/tool to work? I can't seem to get the tool to perform as expected.
Thanks