Carbon Goggles

// Carbon Goggles 0.7
//
// An Augmented Virtual Reality HUD for Second Life 
// by Babbage Linden
//
// Available under the Creative Commons Attribution-ShareAlike 2.5 license
// http://creativecommons.org/licenses/by-sa/2.5/
float gSelectionTimeout = 120.0;
float gScanDelay = 5.0;
float gRateDelay = 0.0;
integer gAnnotateDialogChannel = 8888;
integer gAnnotateDialogChannelHandle = 0;
integer gCommandChannel = 5;
integer gCommandChannelHandle = 0;
integer gConfigDialogChannel = 9999;
integer gConfigDialogChannelHandle = 0;
float gSensorRange = 10.0;
integer gMaxOverlays = 8;
float gRefreshPeriod = 4.0;
float gFOV;
list gSkipNames = ["Object", "Tree", ""];
list gDetectedNames;
list gDetectedKeys;
list gDetectedPositions;
list gDetectedEmissions;
integer gAnnotate = FALSE;
string gAnnotationType;
string gAnnotationUrl = "http://wiki.amee.com/andrew/secondlife/get.php?path=/data";
string gAnnotationPath;
string gAnnotationParams;
key gAnnotationRequest;
key gEmissionsRequest;
integer gEmissionsRequestIndex;
// Emission parameters
integer gDistanceKmPerMonth = 1207; // cars, motorcycles
integer gJourneysPerYear = 2; // flights
integer gHoursPerMonth = 90; // tv
integer gCyclesPerMonth = 12; // washing machine, dryer, dishwasher
integer gNumberOwned = 1; // entertainment devices, e.g. computers, dvd, freeview box, game console etc. 
key gUpdateId;
string gRegionName;
vector gCameraPos;
rotation gCameraRot;
integer gHidingOverlays;
integer gSelectedIndex;
key gNotecardId;
integer gNotecardLine;
string gURL = "http://carbongoggles.org/";
float gLastRateTime;
float gLastScanTime;
float gLastSelectedTime;
integer DISPLAY_STRING = 204000;
integer SET_COLOR = 204007;
integer SET_POSITION = 204008;
integer SET_SCALE = 204009;
integer SET_TEXT_COLOUR = 204010;
integer SET_ALPHA = 204011;
integer OVERLAY_LINK_OFFSET = 2;
integer OVERLAY_COUNT = 8;
key httpRequest(string url, list parameters, string body)
{
 //llOwnerSay("llHTTPRequest " + url + " " + body);
 return llHTTPRequest(url, parameters, body);
}
requestEmissions()
{
 if(gEmissionsRequestIndex < llGetListLength(gDetectedEmissions) && llList2Float(gDetectedEmissions, gEmissionsRequestIndex) < 0.0f)
 {
 string url = gURL + "object/" + (string)llList2Key(gDetectedKeys, gEmissionsRequestIndex) + "/emissions?";
 url += "distanceKmPerMonth=" + (string) gDistanceKmPerMonth;
 url += "%26journeysPerYear=" + (string) gJourneysPerYear;
 url += "%26hoursPerMonth=" + (string) gHoursPerMonth;
 url += "%26cyclesPerMonth=" + (string) gCyclesPerMonth;
 url += "%26numberOwned=" + (string) gNumberOwned;
 //llOwnerSay(url);
 gEmissionsRequest = httpRequest(url, [HTTP_METHOD, "GET"], ""); 
 }
}
rescan()
{
 float time = llGetTime();
 if(time > gLastScanTime + gScanDelay && gSelectedIndex == -1)
 {
 llSensor("", NULL_KEY, ACTIVE | PASSIVE, gSensorRange, PI / 2.0);
 gLastScanTime = time;
 }
}
hideOverlay(integer index)
{
 float offset = 0.5 + ((2 / 16.0) * (float)index);
 //string s = (string)index;
 //float offset = 1.1;
 string s = "";
 llMessageLinked(index + OVERLAY_LINK_OFFSET, SET_ALPHA, (string)0.0f, NULL_KEY);
 llMessageLinked(index + OVERLAY_LINK_OFFSET, DISPLAY_STRING, s, NULL_KEY);
 llMessageLinked(index + OVERLAY_LINK_OFFSET, SET_SCALE, (string)<0.1,0.1,0.1>, NULL_KEY);
 llMessageLinked(index + OVERLAY_LINK_OFFSET, SET_POSITION, (string)<0,0,offset>, NULL_KEY);
}
hideOverlays()
{
 integer i = 0;
 while(i < OVERLAY_COUNT)
 {
 hideOverlay(i);
 ++i;
 }
 gHidingOverlays = TRUE;
}
setOverlayColour(vector colour)
{
 integer i;
 for(i = 0; i < OVERLAY_COUNT; ++i)
 {
 llMessageLinked(i + OVERLAY_LINK_OFFSET, SET_COLOR, (string)<0.8,0.8,0.8>, NULL_KEY);
 }
}
init(key id)
{
 gFOV = 60.0 * DEG_TO_RAD;
 gLastRateTime = 0.0;
 gLastScanTime = 0.0;
 llResetTime();
 llRequestPermissions(llGetOwner(), PERMISSION_TRACK_CAMERA);
 gCommandChannelHandle = llListen(gCommandChannel, "", id, "");
 gAnnotateDialogChannelHandle = llListen(gAnnotateDialogChannel, "", id, "");
 gConfigDialogChannelHandle = llListen(gConfigDialogChannel, "", id, "");
 llOwnerSay("Loading settings");
 gNotecardLine = 0;
 gNotecardId = llGetNotecardLine("Settings", gNotecardLine);
 hideOverlays();
 gSelectedIndex = -1;
 setOverlayColour(<0,1,0>);
}
integer overlay(integer index)
{
 vector cameraPos = llGetCameraPos();
 rotation cameraRot = llGetCameraRot();
 integer overlayLinkNum = index + OVERLAY_LINK_OFFSET;
 float emissions = llList2Float(gDetectedEmissions, index);
 vector objectPos = llList2Vector(gDetectedPositions, index);
 
 if(emissions >= 0.0f || gAnnotate)
 {
 // Translate object in to camera space.
 objectPos = objectPos - cameraPos;
 
 // Rotate object in camera space.
 rotation invCameraRot = <0.0,0.0,0.0,1.0> / cameraRot;
 objectPos = objectPos * invCameraRot;
 
 // Switch axes from Z = up to RHS (X = left, Y = up, Z = forward)
 objectPos = <-objectPos.y, objectPos.z, objectPos.x>;
 
 // Apply perspective distortion.
 float xHUD = (objectPos.x * (1.0 / llTan(gFOV / 2.0))) / objectPos.z;
 float yHUD = (objectPos.y * (1.0 / llTan(gFOV / 2.0))) / objectPos.z;
 
 // Set front clipping plane to 1m and back clipping plane to infinity.
 float zHUD = (objectPos.z - 2) / objectPos.z;
 
 // Clip object to HUD.
 if( xHUD > -1.0 && xHUD < 1.0 && 
 yHUD > -1.0 && yHUD < 1.0 &&
 zHUD > -1.0 && zHUD < 1.0)
 {
 string label = "Click to annotate";
 if(llList2Float(gDetectedEmissions, index) >= 0.0f)
 {
 label = (string)llList2String(gDetectedNames, index) + "\n";
 label += (string)llList2Float(gDetectedEmissions, index) + " kg co2/month";
 }
 else
 {
 emissions = 1.0f;
 }
 
 vector posHUD = <0, -xHUD / 2, yHUD / 2> - llGetLocalPos();
 llMessageLinked(overlayLinkNum, SET_POSITION, (string)posHUD, NULL_KEY);
 float radius = llPow((3.0f * emissions) / (4.0f * PI), (1.0 / 3.0));
 float scale = (float)radius / 10.0f;
 if(scale > 1.0)
 {
 scale /= 10.0;
 llMessageLinked(overlayLinkNum, SET_COLOR, (string)<0.5,0.05,0.05>, NULL_KEY);
 }
 else
 {
 llMessageLinked(overlayLinkNum, SET_COLOR, (string)<0.8,0.8,0.8>, NULL_KEY);
 }
 llMessageLinked(overlayLinkNum, SET_SCALE, (string)<scale,scale,scale>, NULL_KEY);
 llMessageLinked(overlayLinkNum, DISPLAY_STRING, label, NULL_KEY);
 llMessageLinked(overlayLinkNum, SET_ALPHA, (string)1.0f, NULL_KEY);
 return TRUE;
 }
 }
 hideOverlay(index);
 return FALSE;
}
integer setOption(string option)
{
 list pair = llParseString2List(option, [":", " "], []);
 string name = llList2String(pair, 0);
 name = llToLower(name);
 if(name == "sensorrange")
 {
 gSensorRange = llList2Float(pair, 1);
 } 
 else if(name == "refreshperiod")
 {
 gRefreshPeriod = llList2Float(pair, 1);
 llSetTimerEvent(gRefreshPeriod);
 }
 else if(name == "selectiontimeout")
 {
 gSelectionTimeout = llList2Float(pair, 1);
 }
 else if(name == "annotate")
 {
 gAnnotate = llList2Integer(pair, 1);
 }
 else if(name == "fieldofview")
 {
 gFOV = llList2Float(pair, 1) * DEG_TO_RAD;
 }
 else if(name == "skipnames")
 {
 gSkipNames = llCSV2List(llList2String(pair, 1));
 }
 else if(name == "overlayrgb")
 {
 vector colour;
 list rgb = llCSV2List(llList2String(pair, 1));
 colour.x = llList2Float(rgb, 0);
 colour.y = llList2Float(rgb, 1);
 colour.z = llList2Float(rgb, 2);
 setOverlayColour(colour);
 }
 else if(name == "distancekmpermonth")
 {
 gDistanceKmPerMonth = llList2Integer(pair, 1);
 }
 else if(name == "journeysperyear")
 {
 gJourneysPerYear = llList2Integer(pair, 1);
 } 
 else if(name == "hourspermonth")
 {
 gHoursPerMonth = llList2Integer(pair, 1);
 }
 else if(name == "cyclespermonth")
 {
 gCyclesPerMonth = llList2Integer(pair, 1);
 }
 else if(name == "numberowned")
 {
 gNumberOwned = llList2Integer(pair, 1);
 } 
 else
 {
 return FALSE;
 }
 //llOwnerSay(llList2String(pair, 0) + ":" + llList2String(pair, 1));
 return TRUE;
}
annotateObject(integer index, string ameeurl)
{
 key id = llList2Key(gDetectedKeys, index);
 vector position = llList2Vector(gDetectedPositions, index);
 string body = (string)((integer)position.x);
 body += "," + (string)((integer)position.y);
 body += "," + (string)((integer)position.z);
 body += "," + llEscapeURL(ameeurl); 
 body += "," + llList2String(gDetectedNames, index);
 string url = gURL + "object/" + (string)id;
 gUpdateId = httpRequest(url, [HTTP_METHOD, "POST"], body);
 llOwnerSay("Annotated " + llList2String(gDetectedNames, gSelectedIndex));
 gSelectedIndex = -1;
}
saySettings()
{
 llOwnerSay("To change settings, chat \"/5 Setting:Value\" or change values in Settings notecard then re-attach HUD\nSensorRange:N (scan objects up to N meters away)\nRefreshTime:N (update overlay positions every N seconds)\nShowUnknown:N (augment objects with unknown emissions if N is 1)\nFieldOfView:N (field of view in degrees, must match viewer)\nSkipNames:X,Y,...,Z (ignore objects with names in list X,Y,...,Z)\nOverlayRGB:R,G,B (set overlay colour to <R,G,B>)\nSelectionTimeout:N (time out object selection after N seconds)\n");
}
default
{
 state_entry()
 {
 init(llGetOwner());
 }
 
 attach(key id)
 {
 if(id != NULL_KEY)
 {
 init(id);
 }
 }
 
 listen(integer channel, string name, key id, string message)
 {
 if(channel == gCommandChannel)
 {
 list pair = llParseString2List(message, ["", " "], []);
 string command = llList2String(pair, 0);
 command = llToLower(command);
 if(setOption(message) == TRUE)
 {
 return;
 }
 else if(command == "annotate")
 {
 annotateObject(gSelectedIndex, llList2String(pair, 1));
 }
 else
 {
 llOwnerSay("ERROR: Unrecognised command \"" + message + "\" chat \"/5 Help\" for help");
 }
 }
 else if(channel == gAnnotateDialogChannel)
 {
 if(gAnnotationType == "path")
 {
 gAnnotationPath += "/" + llEscapeURL(message);
 }
 else
 {
 if(gAnnotationParams == "")
 {
 gAnnotationParams = "/drill%3f";
 }
 else
 {
 gAnnotationParams += "%26";
 }
 gAnnotationParams += gAnnotationType;
 gAnnotationParams += "=";
 gAnnotationParams += llEscapeURL(message);
 }
 string url = gAnnotationUrl + gAnnotationPath + gAnnotationParams;
 gAnnotationRequest = httpRequest(url, [], "");
 //llOwnerSay(url);
 }
 else if(channel == gConfigDialogChannel)
 {
 if(message == "Visualise")
 {
 llOwnerSay("Hiding annotation interface...");
 gAnnotate = FALSE;
 }
 else if(message == "Annotate")
 {
 llOwnerSay("Showing annotation interface...");
 gAnnotate = TRUE;
 }
 else if(message == "Web")
 {
 llOwnerSay("Loading web interface...");
 llLoadURL(llGetOwner(), "", gURL);
 }
 else if(message == "Settings")
 {
 saySettings();
 }
 else if(message == "Off")
 {
 llOwnerSay("Turning off...");
 state off;
 }
 }
 }
 
 timer()
 { 
 if((gSelectedIndex != -1) && (gLastSelectedTime + gSelectionTimeout < llGetTime()))
 {
 gSelectedIndex = -1;
 llOwnerSay("Selection timeout");
 }
 
 vector cameraPos = llGetCameraPos();
 rotation cameraRot = llGetCameraRot();
 string regionName = llGetRegionName();
 integer hide = (regionName != gRegionName)
 || (llVecDist(cameraPos, gCameraPos) / gRefreshPeriod > 0.1)
 || (llAngleBetween(cameraRot, gCameraRot) * RAD_TO_DEG > 10);
 gRegionName = regionName;
 gCameraPos = cameraPos;
 gCameraRot = cameraRot;
 if(hide == TRUE)
 {
 hideOverlays();
 return;
 }
 gHidingOverlays = FALSE;
 
 integer count = llGetListLength(gDetectedNames);
 integer i = 0;
 while(i < count)
 {
 overlay(i);
 ++i;
 }
 while(i < OVERLAY_COUNT)
 {
 hideOverlay(i);
 ++i;
 }
 rescan();
 }
 
 sensor(integer num)
 {
 //llOwnerSay("sensor");
 list oldKeys = gDetectedKeys;
 list oldEmissions = gDetectedEmissions; 
 gDetectedNames = [];
 gDetectedKeys = [];
 gDetectedPositions = [];
 gDetectedEmissions = [];
 integer i;
 for(i = 0; i < num && i < gMaxOverlays; ++i)
 {
 string name = llDetectedName(i);
 //llOwnerSay("scanned " + name);
 integer skip = FALSE;
 integer skipCount = llGetListLength(gSkipNames);
 integer skipIndex;
 for(skipIndex = 0; (skipIndex < skipCount) && (skip == FALSE); ++skipIndex)
 {
 if(name == llList2String(gSkipNames, skipIndex))
 {
 skip = TRUE;
 }
 }
 
 if(skip == FALSE)
 {
 key newKey = llDetectedKey(i);
 integer oldCount = llGetListLength(oldKeys);
 integer oldIndex;
 integer found = FALSE;
 for(oldIndex = 0; (oldIndex < oldCount) && (found == FALSE); ++oldIndex)
 {
 key oldKey = llList2Key(oldKeys, oldIndex);
 if(oldKey == newKey)
 {
 gDetectedEmissions += llList2Float(oldEmissions, oldIndex);
 found = TRUE; 
 }
 }
 if(found == FALSE)
 {
 gDetectedEmissions += -1.0f;
 }
 gDetectedNames += name;
 gDetectedKeys += newKey;
 gDetectedPositions += llDetectedPos(i);
 }
 }
 gRegionName = llGetRegionName();
 gCameraPos = llGetCameraPos();
 gEmissionsRequest = NULL_KEY;
 gEmissionsRequestIndex = 0;
 gSelectedIndex = -1;
 requestEmissions();
 }
 
 http_response(key id, integer status, list meta, string body) 
 {
 if(id == gEmissionsRequest) 
 { 
 //llOwnerSay("request http_response " + (string)status + " length " + (string)llStringLength(body) + " " + body);
 if(status == 200)
 {
 list result = llParseString2List(body, [","], []);
 //llOwnerSay(llList2CSV(result));
 float emissions = llList2Float(result, 1);
 //llOwnerSay((string) emissions + " " + (string) gEmissionsRequestIndex + " " + llList2CSV(gDetectedEmissions));
 gDetectedEmissions = llListReplaceList(gDetectedEmissions, [emissions], gEmissionsRequestIndex, gEmissionsRequestIndex);
 }
 overlay(gEmissionsRequestIndex);
 gEmissionsRequest = NULL_KEY;
 ++gEmissionsRequestIndex;
 requestEmissions();
 }
 else if(id == gAnnotationRequest)
 {
 //llOwnerSay((string)status);
 //llOwnerSay(body);
 //list result = llParseString2List(body, [" ",",","\n"], []);
 //llOwnerSay(llList2CSV(result));
 //string type = llList2String(result, 0);
 string type = llGetSubString(body, 0, llSubStringIndex(body, ",") - 1);
 list result = llParseString2List(body, [type + ",","\n"], []);
 //llOwnerSay(body);
 //llOwnerSay(llList2CSV(result));
 if(type == "uid")
 {
 // AMEE data item UID found, build and store AMEE URL.
 string uid = llList2String(result, 0);
 string url = gAnnotationUrl + gAnnotationPath + "/" + uid;
 annotateObject(gSelectedIndex, url);
 llOwnerSay("(To annotate another object with the same data, select it, then say \"/5 annotate " + url + "\"");
 }
 else
 {
 gAnnotationType = type;
 list options = [];
 integer length = llGetListLength(result);
 integer i;
 for(i = 0; i < length; i += 1)
 {
 options += llList2String(result, i);
 }
 if(type == "path")
 {
 type = "catagory";
 }
 options = llList2List(options, 0, 11); // TODO: Paginate instead of truncating options.
 //llOwnerSay(llList2CSV(options)); 
 llDialog(llGetOwner(), "Select " + type, options, gAnnotateDialogChannel);
 }
 }
 if(status != 200 && status != 404)
 {
 llOwnerSay("ERROR:" + (string)status + ":" + body);
 }
 //llOwnerSay("http_response " + (string)status + " length " + (string)llStringLength(body) + " " + body);
 }
 
 dataserver(key id, string data)
 {
 if(id == gNotecardId)
 {
 if(data != EOF)
 {
 if(setOption(data) == FALSE)
 {
 llOwnerSay("ERROR: Unknown setting " + data);
 }
 ++gNotecardLine;
 gNotecardId = llGetNotecardLine("Settings", gNotecardLine);
 }
 else
 {
 llOwnerSay("Settings loaded\nEmissions data will be overlayed on objects when avatar is stationary\nClick logo for settings");
 llSetTimerEvent(gRefreshPeriod);
 llSensor("", NULL_KEY, ACTIVE | PASSIVE, gSensorRange, PI / 2.0);
 }
 }
 }
 
 touch_start(integer num)
 {
 integer linkNum = llDetectedLinkNumber(0);
 //llOwnerSay((string)linkNum); 
 linkNum -= OVERLAY_LINK_OFFSET;
 if(linkNum >= 0)
 {
 gSelectedIndex = linkNum;
 gLastSelectedTime = llGetTime();
 llOwnerSay("Selected " + llList2String(gDetectedNames, gSelectedIndex) + "(" + llList2String(gDetectedKeys, gSelectedIndex) + ")");
 gAnnotationPath = "";
 gAnnotationParams = "";
 gAnnotationRequest = httpRequest(gAnnotationUrl, [], "");
 }
 else
 {
 llDialog(llGetOwner(), "Select Interface", ["Web", "Settings", "Off", "Visualise", "Annotate"], gConfigDialogChannel);
 }
 }
}
state off
{
 state_entry()
 {
 hideOverlays();
 llSetTimerEvent(0);
 }
 
 touch_start(integer num)
 {
 llOwnerSay("Turning on...");
 state default;
 }
}