Last active
October 7, 2022 17:08
-
-
Save pramsey/7f12f0de2419a94a6c258f5daecf176f to your computer and use it in GitHub Desktop.
Moving Objects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Moving Objects</title> | |
<!-- CSS/JS for OpenLayers map --> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ol.js"></script> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/ol.css" type="text/css" /> | |
<!-- CSS for app --> | |
<link rel="stylesheet" href="moving-objects.css" type="text/css" /> | |
</head> | |
<div id="panel"> | |
<h1>Moving Objects</h1> | |
<div id="objectList"> | |
<!-- <div class="object"> | |
<div class="objectname">Object 1 <span id="obj1icon">⬤</span></div> | |
<div class="controls"> | |
<a href="#" onclick="objMove(1,'left')">⬅️</a> | |
<a href="#" onclick="objMove(1,'up')">⬆️</a> | |
<a href="#" onclick="objMove(1,'down')">⬇️</a> | |
<a href="#" onclick="objMove(1,'right')">➡️</a> | |
</div> | |
</div> --> | |
</div> | |
<div class="meta"> | |
<p>Click on the arrows to move an object!</p> | |
<p>A click updates the location in the database, and the object changes location on the map when the event propogates back out to all the clients.</p> | |
<p>Everyone using this map sees the same object locations and they all update in real time.</p> | |
<p><a href="https://github.com/pramsey/pg_eventserv/blob/main/examples/moving-objects/README.md">How it works...</a></p> | |
<hr/> | |
<pre id="wsStatus"></pre> | |
</div> | |
</div> | |
<div id="map"></div> | |
</div> | |
<script src="moving-objects.js" type="text/javascript"></script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html, body { | |
height: 100%; | |
width: 100%; | |
font-family: sans-serif; | |
} | |
body { | |
padding: 0; | |
margin: 0; | |
display: flex; | |
flex-direction: row; | |
} | |
header { | |
padding-left: 2em; | |
border-bottom: 2px solid lightgray; | |
} | |
h1 { | |
margin-left: 0.5em; | |
font-size: 150%; | |
} | |
#map { | |
background-color: rgb(171, 200, 229); | |
flex-grow: 8; | |
height: 100%; | |
} | |
#panel { | |
flex-grow: 0; | |
border-left: 1px solid black; | |
border-right: 1px solid black; | |
} | |
.meta { | |
padding: 0.5em; | |
margin: 0.5em; | |
text-align: center; | |
max-width: 15em; | |
} | |
div.objectname { | |
padding-right: 0.5em; | |
display: inline; | |
margin-right: 1em; | |
} | |
div.controls { | |
text-decoration: none; | |
display: inline-block; | |
text-align: right; | |
} | |
.objectList { | |
margin: 0; | |
padding: 0; | |
} | |
.object { | |
margin: 0.5em; | |
padding: 0.7em; | |
border-bottom: 1px solid black; | |
border: 1px solid black; | |
background-color: rgb(223, 247, 255) | |
} | |
#wsStatus { | |
padding-top: 0.5em; | |
margin: 0em; | |
text-align: left; | |
} | |
@media (max-width: 800px) { | |
body { | |
flex-direction: column; | |
} | |
#panel { | |
width: 100%; | |
} | |
li { | |
display: inline; | |
} | |
.meta { | |
display: none; | |
} | |
#wsStatus { | |
display: none; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Connection information for the pg_eventserv | |
// This sends us updates on object location | |
var wsChannel = "objects"; | |
var wsHost = "ws://ec2-34-222-178-120.us-west-2.compute.amazonaws.com:7700"; | |
var wsUrl = `${wsHost}/listen/${wsChannel}`; | |
// Connection information for the pg_featureserv | |
// This is where we send commands to move objects | |
// and where we draw the geofence and initial | |
// object locations from | |
var fsHost = "http://ec2-34-222-178-120.us-west-2.compute.amazonaws.com:9000"; | |
var fsObjsUrl = `${fsHost}/collections/moving.objects/items.json`; | |
var fsFencesUrl = `${fsHost}/collections/moving.geofences/items.json`; | |
// Objects are colored based on their | |
// 'color' property, so we need a dynamicly | |
// generated style to reflect that | |
var iconStyleCache = {}; | |
function getIconStyle(feature) { | |
var iconColor = feature.get('color'); | |
if (!iconStyleCache[iconColor]) { | |
iconStyleCache[iconColor] = new ol.style.Style({ | |
image: new ol.style.RegularShape({ | |
fill: new ol.style.Fill({ | |
color: iconColor | |
}), | |
stroke: new ol.style.Stroke({ | |
width: 1, | |
color: 'grey' | |
}), | |
points: 16, | |
radius: 6, | |
angle: Math.PI / 4 | |
}) | |
}); | |
} | |
return iconStyleCache[iconColor]; | |
}; | |
// Download the current set of moving objects from | |
// the pg_featureserv | |
var objLayer = new ol.layer.Vector({ | |
source: new ol.source.Vector({ | |
url: fsObjsUrl, | |
format: new ol.format.GeoJSON(), | |
}), | |
style: getIconStyle | |
}); | |
// We need a visual panel for each object so we can | |
// click on up/down/left/right controls, so we dynamically | |
// build the panels for each record in the set we | |
// downloaded from pg_featureserv | |
function objsAddToPage() { | |
objLayer.getSource().forEachFeature((feature) => { | |
// console.log(feature); | |
var id = feature.get('id'); | |
var objListElem = document.getElementById("objectList"); | |
var objElem = document.createElement("div"); | |
objElem.className = "object"; | |
objListElem.appendChild(objElem); | |
var objIconId = `obj${id}icon`; | |
var objHtml = `<div class="objectname"> | |
Object ${id} <span id="${objIconId}">⬤</span> | |
</div> | |
<div class="controls"> | |
<a href="#" onclick="objMove(${id},'left')">⬅️</a> | |
<a href="#" onclick="objMove(${id},'up')">⬆️</a> | |
<a href="#" onclick="objMove(${id},'down')">⬇️</a> | |
<a href="#" onclick="objMove(${id},'right')">➡️</a> | |
</div>`; | |
objElem.innerHTML = objHtml; | |
var iconElem = document.getElementById(objIconId); | |
iconElem.style.color = feature.get('color'); | |
return false; | |
} | |
); | |
} | |
// Cannot build the HTML panels until the features have been | |
// fully downloaded. | |
objLayer.getSource().on('featuresloadend', objsAddToPage); | |
// When a control is clicked, we just need to hit the | |
// pg_featureserv function end point with the direction | |
// and object id. So we do not have any actions to take | |
// in the onreadystatechange method, actually. | |
function objMove(objId, direction) { | |
//console.log(`move ${objId}! ${direction}`); | |
var xmlhttp = new XMLHttpRequest(); | |
xmlhttp.onreadystatechange = function() { | |
if (xmlhttp.readyState == XMLHttpRequest.DONE) { // XMLHttpRequest.DONE == 4 | |
if (xmlhttp.status == 200) { | |
// processed move | |
} | |
else { | |
// move failed | |
} | |
} | |
}; | |
var objMoveUrl = `${fsHost}/functions/postgisftw.object_move/items.json?direction=${direction}&move_id=${objId}`; | |
xmlhttp.open("GET", objMoveUrl, true); | |
xmlhttp.send(); | |
} | |
// Get current set of geofences from the | |
// pg_featureserv | |
var fenceLayer = new ol.layer.Vector({ | |
source: new ol.source.Vector({ | |
url: fsFencesUrl, | |
format: new ol.format.GeoJSON() | |
}), | |
style: new ol.style.Style({ | |
stroke: new ol.style.Stroke({ | |
color: 'blue', | |
width: 3 | |
}), | |
fill: new ol.style.Fill({ | |
color: 'rgba(0, 0, 255, 0.1)' | |
}) | |
}) | |
}); | |
// Basemap tile layer | |
var baseLayer = new ol.layer.Tile({ | |
source: new ol.source.XYZ({ | |
// url: "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png" | |
url: "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png" | |
}) | |
}); | |
// Compose map of our three layers | |
var map = new ol.Map({ | |
target: 'map', | |
view: new ol.View({ | |
center: [0, 0], | |
zoom: 2 | |
}), | |
layers: [baseLayer | |
,fenceLayer | |
,objLayer | |
] | |
}); | |
var outputStatus = document.getElementById("wsStatus"); | |
console.log("Preparing WebSocket..."); | |
var ws = new WebSocket(wsUrl); | |
console.log("WebSocket created."); | |
ws.onopen = function () { | |
outputStatus.innerHTML = "Connected to WebSocket!\n"; | |
}; | |
ws.onerror = function(error) { | |
console.log(`[error] ${error.message}`); | |
}; | |
// Got a message from the WebSocket! | |
ws.onmessage = function (e) { | |
// First, we can only handle JSON payloads, so quickly | |
// try and parse it as JSON. Catch failures and return. | |
try { | |
var payload = JSON.parse(e.data); | |
outputStatus.innerHTML = JSON.stringify(payload, null, 2) + "\n"; | |
} | |
catch (err) { | |
outputStatus.innerHTML = "Error: Unable to parse JSON payload\n\n"; | |
outputStatus.innerHTML += e.data; | |
return; | |
} | |
// We are not segmenting payloads by channel here, so we | |
// test the 'type' property to find out what kind of | |
// payload we are dealing with. | |
if ("type" in payload && payload.type == "objectchange") { | |
var oid = payload.object_id; | |
// The map sends us back coordinates in the map projection, | |
// which is web mercator (EPSG:3857) since we are using | |
// a web mercator back map. That means a little back projection | |
// before we start using the coordinates. | |
var lng = payload.location.longitude; | |
var lat = payload.location.latitude; | |
var coord = ol.proj.transform([lng, lat], 'EPSG:4326', 'EPSG:3857'); | |
const objGeom = new ol.geom.Point(coord); | |
const objProps = { | |
timeStamp: payload.ts, | |
props: payload.props, | |
color: payload.color, | |
}; | |
var objectSource = objLayer.getSource(); | |
// Make sure we already have this object in our | |
// local data source. If we do, we update the object, | |
// if we do not, we create a fresh local object and | |
// add it to our source. | |
const curFeature = objectSource.getFeatureById(oid); | |
if (curFeature) { | |
curFeature.setGeometry(objGeom); | |
curFeature.setProperties(objProps); | |
// console.log(curFeature); | |
} | |
else { | |
const newFeature = new ol.Feature(objGeom); | |
newFeature.setProperties(objProps); | |
newFeature.setId(oid); | |
objectSource.addFeature(newFeature); | |
// console.log(newFeature); | |
} | |
// Watch out for enter/leave events and change the color | |
// on the appropriate geofence to match the object | |
// doing the entering/leaving | |
if (payload.events) { | |
// Dumbed down to only handle on event at a time | |
var event = payload.events[0]; | |
var fenceId = event.geofence_id; | |
var feat = fenceLayer.getSource().getFeatureById(fenceId); | |
var style = feat.getStyle() ? feat.getStyle() : fenceLayer.getStyle().clone(); | |
style.getStroke().setColor(event.action == "entered" ? payload.color : "blue"); | |
feat.setStyle(style); | |
} | |
} | |
// Watch for a "layer changed" payload and fully reload the | |
// data for the appropriate layer when it comes by. Generally | |
// useful for all kinds of map synching needs. | |
if ( "type" in payload && payload.type == "layerchange") { | |
if ("geofences" == payload.layer) { | |
fenceLayer.getSource().refresh(); | |
} | |
} | |
}; | |
// This is that the payload object looks like, it's only | |
// one possibility among many. | |
// { | |
// 'object_id': 1, | |
// 'events': [ | |
// { | |
// 'action': 'left', | |
// 'geofence_id': 3, | |
// 'geofence_label': 'Ranch' | |
// } | |
// ], | |
// 'location': { | |
// 'longitude': -126.4, | |
// 'latitude': 45.3, | |
// } | |
// 'ts': '2001-01-01 12:34:45.1234', | |
// 'props': {'name':'Paul'}, | |
// 'color': 'red' | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment