house map
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

342 lines
10 KiB

let haStateURL = "http://beacon:1880/ha_state"
let metrics = {
roomDefaultOpacity : 0.15,
roomHighlightOpacity : 0.6,
roomHighlightDuration : 0.3,
roomUnhighlightDuration: 0.3,
animationCurve : TWEEN.Easing.Cubic.InOut
}
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 )
let homeContainer = new THREE.Group()
scene.add(homeContainer)
let home = new THREE.Group()
homeContainer.add(home)
// key: area name
// value: info
var areaData = { }
var areasWithEntitiesToUpdate = { }
const renderer = new THREE.WebGLRenderer({
logarithmicDepthBuffer : true
})
renderer.setSize( window.innerWidth, window.innerHeight )
document.body.appendChild( renderer.domElement )
const light = new THREE.AmbientLight(0xFFAAAA, 1)
light.position.z = 100
scene.add(light)
scene.background = new THREE.Color("white")
// back the camera up
camera.position.z = 25
// flip home on x axis
home.rotation.x = Math.PI
// Rotate container a bit for comfort
homeContainer.rotation.x = 0.6 * (-Math.PI / 2)
// Returns the first child (or container) whose name matches
function childWithName(container, name) {
if (container.userData.name == name) {
return container;
}
for (var child of container.children) {
let childMatches = childWithName(child, name)
if (childMatches != null) {
return childMatches
}
}
return null
}
function animate() {
requestAnimationFrame( animate )
TWEEN.update()
var areasToUpdate = new Set()
for (var area of Object.keys(areasWithEntitiesToUpdate)) {
let updatedEntities = areasWithEntitiesToUpdate[area]
if (updatedEntities.length > 0) {
areasToUpdate.add(area)
}
}
for (var area of areasToUpdate) {
let areaContainer = childWithName(scene, area)
let box = childWithName(areaContainer, "box")
let fadeIn = new TWEEN.Tween(box.material)
.easing(metrics.animationCurve)
.to( { opacity: metrics.roomHighlightOpacity }, metrics.roomHighlightDuration * 1000)
let fadeOut = new TWEEN.Tween(box.material)
.easing(metrics.animationCurve)
.to( { opacity: metrics.roomDefaultOpacity }, metrics.roomUnhighlightDuration * 1000)
// This will rotate to the area that has a change
let rotate = new TWEEN.Tween(homeContainer.rotation)
.easing(metrics.animationCurve)
.to( { z: areaData[area].angle }, 2000 )
rotate.chain(fadeIn)
fadeIn.chain(fadeOut)
rotate.start()
}
// homeContainer.rotation.z += 0.003
renderer.render( scene, camera )
}
animate()
function createTextMesh(text, colorName) {
let canvas = document.createElement('canvas')
let context = canvas.getContext('2d')
context.textAlign = "center"
context.textBaseline = "middle"
context.fillStyle = colorName
context.font = '144pt sans-serif'
let size = context.measureText(text)
let height = context.measureText('M').width
size.height = height
let canvasScale = 0.1
canvas.width = size.width * canvasScale * 0.6
canvas.height = size.height * canvasScale
console.log([canvas.width, canvas.height])
context.fillStyle = 'red'
context.fillRect(0, 0, canvas.width, canvas.height)
context.fillStyle = 'blue'
context.fillText(text, 5, 10)
let texture = new THREE.CanvasTexture(canvas)
texture.needsUpdate = true
let material = new THREE.MeshBasicMaterial({
map: texture
})
material.transparent = true
let scaleFactor = 0.05
let plane = new THREE.PlaneGeometry(canvas.width * scaleFactor, canvas.height * scaleFactor)
let mesh = new THREE.Mesh(plane, material)
return mesh
}
var lastUpdateTime = new Date()
function updateWithHAData(data) {
// `data` is an array of entities. loop through and group by area
var areasToEntities = { }
var areasToUpdatedEntities = { }
var areaNames = new Array()
for (var name of Object.keys(areaData)) {
areaNames.push(name)
areasToEntities[name] = new Array()
areasToUpdatedEntities[name] = new Array()
}
let sortedAreaNames = areaNames
sortedAreaNames.sort(function(a, b) {
return b.length - a.length;
})
for (var entity of data) {
let id = entity.entity_id
let friendlyName = entity.attributes.friendly_name || entity.entity_id
for (var area of sortedAreaNames) {
let matchesUnderscored = id.includes(area.toLowerCase().replace(' ','_'))
let matchesNoSpaces = id.includes(area.toLowerCase().replace(' ',''))
let matchesFriendlyName = friendlyName.includes(area)
let matches = matchesUnderscored || matchesNoSpaces || matchesFriendlyName
if (matches) {
let areaEntities = areasToEntities[area]
areaEntities.push(entity)
areasToEntities[area] = areaEntities
let lastUpdate = new Date(entity.last_changed)
if (lastUpdate > lastUpdateTime) {
let areaUpdatedEntities = areasToUpdatedEntities[area]
areaUpdatedEntities.push(entity)
areasToUpdatedEntities[area] = areaUpdatedEntities
}
}
}
}
lastUpdateTime = new Date()
areasWithEntitiesToUpdate = areasToUpdatedEntities
}
async function loadHAData() {
let request = new Request(haStateURL)
fetch(request)
.then(response => {
return response.json()
})
.then(json => {
updateWithHAData(json)
new Promise(resolve => setTimeout(resolve, 250))
.then(_ => {
loadHAData()
})
})
}
function setPosition(mesh, x, y, z, w, h, d) {
// position sets the *center* position
// our coordinates are top/left/bottom
let meshX = x + w/2
let meshY = y + h/2
let meshZ = -z - d/2
mesh.position.x = meshX
mesh.position.y = meshY
mesh.position.z = meshZ
}
function configureScene(data) {
var minX = 1000
var maxX = -1000
var minY = minX
var maxY = maxX
var minZ = minX
var maxZ = maxX
// Add geometry for the rooms
for (var room of data.rooms) {
let roomContainer = new THREE.Group()
roomContainer.userData.name = room.name
areaData[room.name] = room
let roomGeo = new THREE.BoxGeometry(room.w, room.h, room.d)
let roomColor = new THREE.Color(room.color)
let roomBoxMaterial = new THREE.MeshPhysicalMaterial({ color: roomColor })
roomBoxMaterial.transparent = true
roomBoxMaterial.opacity = metrics.roomDefaultOpacity
let roomMesh = new THREE.Mesh(roomGeo, roomBoxMaterial)
roomMesh.userData.name = "box"
roomContainer.add(roomMesh)
let roomEdgesGeo = new THREE.EdgesGeometry(roomGeo)
let roomLinesMaterial = new THREE.LineBasicMaterial({ color: roomColor })
roomLinesMaterial.linewidth = 3
let roomLines = new THREE.LineSegments(roomEdgesGeo, roomLinesMaterial)
roomLines.userData.name = "lines"
roomContainer.add(roomLines)
let roomZ = room.z || 0
setPosition(roomContainer, room.x, room.y, roomZ, room.w, room.h, room.d)
home.add(roomContainer)
let roomLabel = createTextMesh(room.name, room.color)
roomLabel.position.x = roomContainer.position.x
roomLabel.position.y = roomContainer.position.y
roomLabel.position.z = roomZ
roomLabel.renderOrder = 5000
// not yet
// homeContainer.add(roomLabel)
minX = Math.min(minX, room.x)
minY = Math.min(minY, room.y)
minZ = Math.min(minZ, roomZ)
maxX = Math.max(maxX, room.x + room.w)
maxY = Math.max(maxY, room.y + room.h)
maxZ = Math.max(maxZ, roomZ + room.d)
}
let xExtent = maxX - minX
let yExtent = maxY - minY
let zExtent = maxZ - minZ
home.position.x = -1 * (xExtent / 2)
home.position.y = +1 * (yExtent / 2)
home.position.z = -1 * (zExtent / 2)
let homeCenterX = minX + (xExtent / 2)
let homeCenterY = minY + (yExtent / 2)
let homeCenterZ = minZ + (zExtent / 2)
for (var room of data.rooms) {
let name = room.name
let centerX = room.x + room.w / 2
let centerY = room.y + room.h / 2
let centerZ = (room.z || 0) + room.d / 2
let unitX = (centerX - homeCenterX) / (xExtent / 2)
let unitY = (centerY - homeCenterY) / (yExtent / 2)
let unitZ = (centerZ - homeCenterZ) / (zExtent / 2)
let radiusXY = Math.sqrt(unitX * unitX + unitY * unitY)
unitX *= (1.0 / radiusXY)
unitY *= (1.0 / radiusXY)
// Compute rotations
let angleX = Math.acos(unitX)
let angleY = Math.asin(unitY)
let angle = 0
if (unitX > 0 && unitY > 0) {
// First quadrant
if (angleX < Math.PI/2) {
angle = angleX
} else {
angle = angleY
}
} else if (unitX < 0 && unitY > 0) {
// Second quadrant
if (angleX > Math.PI/2 && angleX < Math.PI) {
angle = angleX
} else {
angle = angleY
}
} else if (unitX < 0 && unitY < 0) {
// Third quadrant
if (angleX > Math.PI && angleX < Math.PI * 1.5) {
angle = angleX
} else {
angle = angleY
}
} else {
// Fourth quadrant
if (angleX > Math.PI * 1.5 && angleX < Math.PI * 2) {
angle = angleX
} else {
angle = angleY
}
}
// update in areaData
areaData[room.name].angle = angle - Math.PI/2
}
}
async function loadHomeData() {
let request = new Request('data.json')
fetch(request)
.then(response => {
return response.json()
})
.then(json => {
configureScene(json)
// Kick off first HA load
loadHAData()
})
}
loadHomeData()