1
0
mirror of https://github.com/ssb22/bits-and-bobs.git synced 2023-06-08 10:22:44 +00:00
bits-and-bobs/flatplan.py

198 lines
7.2 KiB
Python

#!/usr/bin/env python
# (should work in either Python 2 or Python 3)
# flatplan 1.3 (c) 2016, 2020 Silas S. Brown.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This is a program to draw an SVG diagram of a flat, using
# input that can be collected by a person with low vision and
# a centimetre tape measure. Area and wall length are noted
# and squared-paper backing is also added. You can also
# include the opening arcs of the internal doors.
# To measure the flat, start measuring at any wall, and trace
# your way around the entire flat until you come back to the
# same position. This in effect means every room's length and
# width measurements will be taken TWICE, for error checking
# (if the line doesn't come back to the same spot, you made a
# mistake, which can also be manifest in the two ends of some
# door frame not matching up with each other).
# For best results, include protrusions of door frames, depth
# of windows, etc. (Walls are much less likely to match up
# if door-frame protrusion is not taken into account.)
# The program assumes the flat is all on one level (hence "flat"),
# that it includes only right-angles, and that all walls are
# connected to the outside walls (no "islands"). This is
# probably valid for nearly all UK flats.
# As you go, imagine an ant (or similar) walking around the
# wall, and write the measurements as follows:
# number = walk that number of centimetres forward
# L = turn 90 degrees left
# R = turn 90 degrees right
# D<number> or E<number> = a door with specified width.
# This should be noted when the ant is at the door's hinge
# and is facing in the direction of the door's handle;
# D = door opens to the right, E = door opens to the lEft.
# The position of the ant is not changed. If followed by
# more walking in the same direction (e.g. to get through the
# doorway), add a separator character (e.g. a colon) between
# the two numbers.
# If you like, you can ''.join([...]) a list of strings, which
# allows you to put comments on some of the measurements.
# Place your string into s below:
s = ''.join([
"500",
"R500",
"R500",
"R500"])
margin_cm = 100
include_area_summary = True
include_doors = True
include_grid_lines = True
# Where to find history:
# on GitHub at https://github.com/ssb22/bits-and-bobs
# and on GitLab at https://gitlab.com/ssb22/bits-and-bobs
# and on BitBucket https://bitbucket.org/ssb22/bits-and-bobs
# and at https://gitlab.developers.cam.ac.uk/ssb22/bits-and-bobs
# and in China: https://gitee.com/ssb22/bits-and-bobs
# -------------------------------------------------------
xd=0;yd=-1 # start pointing up (SVG origin is top left)
curX = curY = oldX = oldY = 0
minX = minY = maxX = maxY = 0
lines = [] ; doors = []
def turnLeft(xd,yd):
if yd: return yd,0
else: return 0,-xd
def turnRight(xd,yd):
if yd: return -yd,0
else: return 0,xd
import re
for c in re.findall("[lr]|[de]?[0-9.]+",s.lower()):
if c=='l': xd,yd = turnLeft(xd,yd)
elif c=='r': xd,yd = turnRight(xd,yd)
elif c[0] in 'de':
radius = float(c[1:])
handleX = curX + radius*xd
handleY = curY + radius*yd
if c[0]=='d': xd2,yd2 = turnRight(xd,yd)
else: xd2,yd2 = turnLeft(xd,yd)
openX = curX + radius*xd2
openY = curY + radius*yd2
if c[0]=='d': sweep = 1
else: sweep = 0
doors.append((handleX,handleY,radius,sweep,openX,openY,curX,curY))
elif c:
oldX,oldY = curX,curY
curX += xd*float(c)
curY += yd*float(c)
minX = min(minX,curX)
minY = min(minY,curY)
maxX = max(maxX,curX)
maxY = max(maxY,curY)
lines.append((oldX,oldY,curX,curY))
minX-=margin_cm ; maxX+=margin_cm ; minY-=margin_cm ; maxY+=margin_cm
# help programs that display paper boundaries:
xOffset = -minX ; yOffset = -minY
minX += xOffset ; maxX += xOffset
minY += yOffset ; maxY += yOffset
try: xrange
except: xrange = range # Python 3
for i in xrange(len(lines)):
a,b,c,d = lines[i]
a += xOffset ; c += xOffset
b += yOffset ; d += yOffset
lines[i] = a,b,c,d
for i in xrange(len(doors)):
a,b,r,s,c,d,e,f = doors[i]
a += xOffset ; c += xOffset ; e += xOffset
b += yOffset ; d += yOffset ; f += yOffset
doors[i] = a,b,r,s,c,d,e,f
# area calculation (by flood-filling the outside and counting the rest) :
assert minX==minY==0
inside = [] ; totalLineLen = 0
for x in xrange(int(maxX)+1): inside.append([1]*(int(maxY)+1)) # don't say *int(maxX): it creates aliases of the same list
errorLine = lines[-1][-2:]+lines[0][:2] # completes the path in case of error
for a,b,c,d in lines+[errorLine]:
if a>c: c,a = a,c
if b>d: b,d = d,b
for x in xrange(int(a),int(c)+1):
for y in xrange(int(b),int(d)+1):
inside[x][y] = 0
totalLineLen += c-a + d-b
fillQ = [(0,0)]
while fillQ:
x,y = fillQ[0] ; del fillQ[0]
if inside[x][y]:
inside[x][y] = 0
if x>0: fillQ.append((x-1,y))
if x<len(inside)-1: fillQ.append((x+1,y))
if y>0: fillQ.append((x,y-1))
if y<len(inside[0])-1: fillQ.append((x,y+1))
area = sum(sum(x) for x in inside) / 10000.0
totalLineLen /= 100.0
maxDP = 1 # max 1 decimal place
area = (u"%%.%dg m\u00b2" % (len(str(int(area)))+maxDP)) % area
if not type(u"")==type(""): area = area.encode('utf-8') # Python 2
area += (("; walls = %%.%dg m" % (len(str(int(totalLineLen)))+maxDP)) % totalLineLen)
a,b,c,d = errorLine
import math ; err = math.sqrt((c-a)*(c-a) + (d-b)*(d-b))
if err: area += (("; error = %%.%dg cm" % (len(str(int(err)))+maxDP)) % err)
# SVG:
print ("""<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" width="%g" height="%g" viewbox="%g %g %g %g">""" % (maxX-minX,maxY-minY,minX,minY,maxX,maxY))
# grid lines:
def doGrid(colour,step):
print ("""<g stroke="%s" stroke-dasharray="1,2">""" % colour)
for x in xrange(int(minX),int(maxX),step):
print ("""<line x1="%g" y1="%g" x2="%g" y2="%g" stroke-width="1" />""" % (x,minY,x,maxY))
for y in xrange(int(minY),int(maxY),step):
print ("""<line x1="%g" y1="%g" x2="%g" y2="%g" stroke-width="1" />""" % (minX,y,maxX,y))
print ("</g>")
if include_grid_lines:
doGrid("cyan",10) ; doGrid("red",100)
# the wall itself:
print ("""<g stroke="black">""")
for l in lines: print ("""<line x1="%g" y1="%g" x2="%g" y2="%g" stroke-width="1" />""" % l)
print ("</g>")
if include_doors:
for x1,y1,r,sweep,x2,y2,cx,cy in doors: print ("""<path d="M%g,%g A%g,%g 0 0,%d %g,%g L%g,%g L%g,%g" fill="none" stroke="green" />""") % (x1,y1,r,r,sweep,x2,y2,cx,cy,x1,y1)
if include_area_summary:
print ("""<text x="25" y="75" font-family="Verdana" font-size="30">Area = %s</text>""" % area)
print ("</svg>")