Skip to content

Instantly share code, notes, and snippets.

@pramsey
Last active October 7, 2022 17:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pramsey/7f12f0de2419a94a6c258f5daecf176f to your computer and use it in GitHub Desktop.
Save pramsey/7f12f0de2419a94a6c258f5daecf176f to your computer and use it in GitHub Desktop.
Moving Objects
<!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/ol@v7.1.0/dist/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@v7.1.0/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">&#11044;</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>
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;
}
}
// 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}">&#11044;</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