Google Map

Hi all,

an update to the google map example, featuring cleaned up code & daily history:

Items:

String ItemMQTT { mqtt="<[broker:owntracks/jkmtnngq/android:state:default]" }
String ItemPatrikLocationHistory
String PatrikLocationString "Patrik [%s]" <place>

Location ItemPatrikLocation "Patrik" <location>
Location ItemLocationHome

Number ItemPatrikLocationAccuracy
Number ItemPatrikNatelBattery "Natel Patrik [%d %%]" <battery> (GroupPersistrrd4jMinute,GroupBattery)

DateTime ItemPatrikLastPositionUpdate "Patrik - letztes Update [%1$tH:%1$tM, %1$td.%1$tm.%1$ty]" <location>

Switch ItemPatrikAtHome "Patrik" <home>
String ItemGoogleMapAPIKey

Rules:

rule InitializeSystem
    when
        System started
    then
        ItemLocationHome.postUpdate(new PointType("47.501104543961,8.3445922192264"))
        ItemGoogleMapAPIKey.postUpdate("Your API Key")
    end

rule PatrikLocationHistoryReset
    when
        Time is midnight
    then
        ItemPatrikLocationHistory.postUpdate("")
    end

rule "LocationPatrikChanged"
    when
        Item ItemPatrikLocation changed    
    then
        val _apiKey = ItemGoogleMapAPIKey.state
        val _position = ItemPatrikLocation.state as PointType
        val _url =  "https://maps.googleapis.com/maps/api/geocode/json?latlng=" 
                    + _position.latitude  + "," 
                    + _position.longitude 
                    + "&sensor=true&key=" + _apiKey

        val _geocodeJSON = sendHttpGetRequest(_url)
        val _address = transform("JSONPATH", "$.results[0].formatted_address", _geocodeJSON)

        PatrikLocationString.postUpdate(_address)
    end

rule MQTT
    when
        Item ItemMQTT changed
    then
        val _mqtt = (ItemMQTT.state as StringType).toString
        
        println("MQTT: " + _mqtt)
        
        val _messageType = transform("JSONPATH", "$._type", _mqtt)
        val _id = transform("JSONPATH", "$.tid", _mqtt)
        val _battery = transform("JSONPATH", "$.batt", _mqtt)
        
        if (_messageType == "location") {
           val _latitude  = transform("JSONPATH", "$.lat", _mqtt)
           val _longitude = transform("JSONPATH", "$.lon", _mqtt)
           val _accuracy  = transform("JSONPATH", "$.acc", _mqtt)
           if (_id == "PG") {
               val _positionString = _latitude + "," + _longitude
               val _position = new PointType(_positionString)
               val _distance = -1
               val _distanceFromHome = -1

               println(_positionString)
               println(_position)
               println(ItemLocationHome)
               println(ItemPatrikLocation)
               println(ItemPatrikLocation.state)

               if (ItemPatrikLocation.state != NULL) {
                   println("Calculating distance ...")
                    _distance = _position.distanceFrom(ItemPatrikLocation.state).intValue
                    _distanceFromHome = _position.distanceFrom(ItemLocationHome.state).intValue
                    
                    if (_distanceFromHome < 100) {
                        ItemPatrikAtHome.postUpdate(ON)
                    } else {
                        ItemPatrikAtHome.postUpdate(OFF)
                    }
               } 

               println(_distance)
               println(_distanceFromHome)
               
               if ((_distance > 100) || (_distance < 0)) {
                   println("Updating location ...")
                   val _locationHistory = ""
                   val _location = new PointType(_positionString)
                   
                   if (ItemPatrikLocationHistory.state != NULL) {
                       _locationHistory = ItemPatrikLocationHistory.state + _positionString + ";"
                   }

                   ItemPatrikLocation.postUpdate(_location)
                   ItemPatrikLocationAccuracy.postUpdate(new DecimalType(_accuracy).intValue)
                   ItemPatrikLocationHistory.postUpdate(_locationHistory)
               }
               
               ItemPatrikLastPositionUpdate.postUpdate(new DateTimeType())
               ItemPatrikNatelBattery.postUpdate(_battery)
           } 
        }
    end

Javascript (openHAB.js):

// ***
// * Required definitions:
// *  - var baseURL = "../";
// *
// ***
// * Required API
// * - Google jQuery: https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// ***

function _GetData(itemUrl) {
    var itemValue = null;
    $.ajax({
        contentType: 'text/plain',
        url: itemUrl,
        data: {},
        async: false,
        success: function(data) {
            if (data != "NULL") {
                itemValue = data;
            }
        }
    });

    return itemValue;
}

// JSON to CSV Converter
function _ConvertToCSV(objArray) {
    var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
    var str = '';

    for (var i = 0; i < array.data.length; i++) {
        var line = '';
        for (var index in array.data[i]) {
            if (line !== '') line += ',';

            line += array.data[i][index];
        }

        str += line + '\r\n';
    }

    return str;
}

function GetOpenHABItem(item) {
    var itemUrl = baseURL.concat("rest/items/").concat(item).concat("/state/");
    var itemValue = null;

    return _GetData(itemUrl);
}

function GetOpenHABItemIntValue(item) {
    return Math.round(GetOpenHabItem(item));
}

function GetOpenHABItemHistoryJSON(item, days = 1, service = "rrd4j") {
    var jsonData = null;
    // var numberOfMilliseconds = 86400 * 1000 * days;

    var startTimeObject = (new Date()).addDays(days * -1);
    // var endTimeObject = new Date();
    var _month = startTimeObject.getMonth() + 1;
    var _starttime = startTimeObject.getFullYear() + "-" + _month + "-" + startTimeObject.getDate() + "T" + startTimeObject.getHours() + ":" + startTimeObject.getMinutes() + ":" + startTimeObject.getSeconds();
    // var _endtime = endTimeObject.getFullYear() + "-" + endTimeObject.getMonth() + "-" + endTimeObject.getDate() + "T" + endTimeObject.getHours() + ":" + endTimeObject.getMinutes() + ":" + endTimeObject.getSeconds();

    var itemUrl = baseURL.concat("rest/persistence/items/").concat(item);
    itemUrl = itemUrl.concat("?serviceId=" + service);

    if (days != 1) { itemUrl = itemUrl.concat("&starttime=" + _starttime); }
    // itemUrl = itemUrl.concat("&endtime="   + _endtime);

    jsonData = _GetData(encodeURI(itemUrl));
    return jsonData;
}

function GetOpenItemHABHistoryCSV(item, days = 1, service = "rrd4j") {
    var jsonData = GetOpenHABItemHistoryJSON(item, days, service);
    return _ConvertToCSV(jsonData);
}

function GetParameter(parameterName) {
    var result = null,
        tmp = [];
    location.search.substr(1).split("&")
        .forEach(function(item) {
            tmp = item.split("=");
            if (tmp[0] === parameterName) {
                result = decodeURIComponent(tmp[1]);
            }
        });
    return result;
}

function DOMSetElementHeight(elementId, height) {
    var pageElement = document.getElementById(elementId);
    pageElement.style.height = height + "px";
}

HTML:

<!DOCTYPE html>
<html>

<head>
    <title>Map</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <style>
        /* Always set the map height explicitly to define the size of the div
         * element that contains the map. 
         */
        
        #map {
            height: 100%;
        }
        
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }
    </style>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    <script type="text/javascript" src="../javascript/openHAB.js"></script>
</head>

<body>
    <div id="map"></div>
    <script>        
        var baseURL = "../../";
        var iconHome = "https://maps.google.com/mapfiles/kml/pal2/icon10.png";
        var iconKarin = "https://maps.google.com/mapfiles/ms/icons/blue-dot.png";
        var iconPatrik = "https://maps.google.com/mapfiles/ms/icons/green-dot.png";
        var iconKarinHistory = "https://maps.google.com/mapfiles/kml/paddle/blu-circle-lv.png";
        var iconPatrikHistory = "https://maps.google.com/mapfiles/kml/paddle/grn-circle-lv.png";
        var map;
        
        function setMarker(location, markerIcon, zIndex = null, label = null) {
            var marker;
            if (location != null) {
                var lat = parseFloat(location.split(',')[0]);
                var lng = parseFloat(location.split(',')[1]);
                if (!isNaN(lat) && !isNaN(lng)) {
                    var markerLocation = new google.maps.LatLng(lat, lng);

                    marker = new google.maps.Marker({
                        position: markerLocation,
                        optimized: false, // Needs to be set to false, else zIndex does not work.
                        zIndex: zIndex,
                        icon: {
                            labelOrigin: new google.maps.Point(14, 42),
                            url: markerIcon,
                        },
                        label: label,
                        map: map
                    });

                    bounds.extend(markerLocation);
                }
            }
            return marker;
        }

        function showHistory(history, markerIcon) {
            var marker;
            if (history != null) {
                history.split(";").forEach(function(element) {
                    if (element != "") {
                        marker = setMarker(element, markerIcon, 1);
                    }
                });
            }
            return marker;
        }

        function initializeMap() {
            var location = GetOpenHABItem("ItemLocationHome");
            var locationHome = new google.maps.LatLng(
                parseFloat(location.split(',')[0]),
                parseFloat(location.split(',')[1]));

            map = new google.maps.Map(document.getElementById('map'), {
                center: locationHome,
                mapTypeId: google.maps.MapTypeId.TERRAIN,
                zoom: 10
            });

            bounds = new google.maps.LatLngBounds(locationHome, locationHome);

            // enable traffic layer
            var trafficLayer = new google.maps.TrafficLayer();
            trafficLayer.setMap(map);

            showHistory(GetOpenHABItem("ItemPatrikLocationHistory"), iconPatrikHistory);
            showHistory(GetOpenHABItem("ItemKarinLocationHistory"), iconKarinHistory);

            setMarker(GetOpenHABItem("ItemPatrikLocation"), iconPatrik, 100, "Patrik");
            setMarker(GetOpenHABItem("ItemKarinLocation"), iconKarin, 100, "Karin");
            setMarker(location, iconHome, 50);

            map.fitBounds(bounds);

        }
    </script>
    <script src="https://maps.googleapis.com/maps/api/js?key=[GoogleAPIKey]&callback=initializeMap" async defer></script>

</body>

</html>

Sitemap:

Webview url="/static/Map/GoogleMap.html" height=11

Hope is it of use as example/inspiration :-).

Notes:

  • You need to add your API key to .html
  • The .html is for two people - but easy to modify for just one or more ā€¦
  • openHAB.js needs to be placed subfolder ā€œjavascriptā€, the .html in ā€œMapā€.

with kind regards,
Patrik

15 Likes

Thatā€™s great! Iā€™ll definitely include this one! Thanks :beers:
The title is a bit unclear, maybe you want to rephrase it?

Thank you for the feedback - do you have an idea for a fitting title?
with kind regards,
Patrik

Integrating a Google Map webview?

Which google maps api should be used? There are quite a few optionsā€¦

Regardless, iā€™ll be updating my setup with this once I get a chance. Thanks for sharing.

Looks great, but I am stil a bit confused regarding the placement of the files:
with ā€˜subfolder javascriptā€™ I presume you mean something like: usr/share/javascript ?
Where is the folder ā€œMapā€?

The html goes into /etc/openhab/html/Map/GoogleMap.html. The javascript goes into /etc/openhab/html/javascript/openHAB.js.

Where the /etc/openhab folder is the openHAB configuration folder where you also find your folders containing your items, rules, transformations, and so on. Depending on your installation (e.g. Windows), this folder may be located at a different location.

great, thank you

Hi, It looks great but it is a little unclear for me where/how to define ā€œItemLocationHomeā€?
Jesper

Iā€™ve an item defined:

Location ItemLocationHome

This I initialize during system start-up to my home location:

rule InitializeSystem
    when
        System started
    then
        ItemLocationHome.postUpdate(new PointType("47.501104543961,8.3445922192264"))
    end

kind regards,
Patrik

Thanks, I am a bit of a slow learner, what about ItemGoogleMapAPIKey, is it also in a rule as ItemLocationHome?
Best regards
Jesper

np ā€¦ yep - the same principle here; but of course the item type is different:

String ItemGoogleMapAPIKey

Initialized again during system start-up (you need your own API key here).

@patrik_gfeller thanks for the update and cool way of using google maps.

However, like so many others. Iā€™m a slow learner.
I got an android API key, is this correct for maps or should I get some other API key?
The string for the APi key, how is that rule looking?
Something like this?

rule InitializeSystem
    when
        System started
    then
        ItemGoogleMapAPIKey=APIKEYHERE
    end

In the first post it looks very easy to implement, but it seems there are some files you need to have to get going.
Could you please update the first post with some more help in what is needed? (Also, it seems RR4J is needed to be configured?)
Does MQTT owntracks binding need to be installed/configured as well in openhab apart from owntracks app in android?
Cheers

1 Like

Been trying to get this up and running but facing some problems. I can see that my mqtt message gets in and the ā€œMQTTā€ item updates which triggers the ā€œMQTTā€ rule but it seems like the other items do not get updated or the transformation is not working for me. Any ideas what Iā€™m doing wrong?

Can you talk a bit more on how the location history works? How often does it update? Why is it a String and not Location type? I donā€™t use MQTT, but iCloud location service. So I have Location items for my wife and I as well as for all our vehicles with the mojio binding. I have your example working for real time, but I am looking for a way to take that Location for each item and build a history so I can get daily history working.

It looks like your storing all history in one big string separated with ; How large can this be? Would it not be better to use persistence and Location type item?

You are right, there are preconditions that are not explicitly mentioned; kind of an advanced example :slight_smile: . Unfortunately I do not have the time to explain every step in every detail - but thought the example might be helpful as inspiration, or starting point anyhow - thus I posted it.

The api key needs to be set similar to the home location; if the one you have does work you can try (I donĀ“t know) - if not generate one for the maps API.

rule InitializeSystem
    when
        System started
    then
        ItemLocationHome.postUpdate(new PointType("47.501104543961,8.3445922192264"))
        ItemGoogleMapAPIKey.postUpdate("Your API Key")
    end

Yep: You need to be able to get the locations from somewhere - I use mqtt; thus that would be the 1st step. with some modifications you can also use a different location source.

For the history I do not use rrd4j - but a string where I store all the coordinates for one day delimated with special characters. Not the most sophisticated way - but works :slight_smile: .

Go step by step ā€¦ once you have the location let me know - IĀ“ll try to help from there; but the MQTT setup youĀ“ll need to figure out with the help of the docu.

with kind regards,
Patrik

As I reset the history after one day the string works for me; but of course you can also modify the example to use persistence and a location item. The openHAB.js already has some utility functions to get item history (note, youĀ“ll need jquery csv from https://github.com/evanplaice/jquery-csv ).

It updates on every location change. I use a string as I did not want to deal with persistence in the javascript yet (but did some preparations) & for one day a string works fine. So canĀ“t help you with the persistence approach yet - but IĀ“m sure it can be done w/o to many modifications. For charts I read history items via javascript, if desired I can post some example that might help you to start. As usual, those are not expert/reference snippets - just the way I could get it to work.

Managed to get it working.
For reference for anyone else wanting to do this; use an editor of choice.

Create all files necessary according to above and find/replace all Patrik (or Karin) with your choice of name in all applicable files.

I used MAPS EMBED API on google (https://developers.google.com/maps/documentation/embed/).
Change it in the files applicable (rules, html).

In owntracks android application under preferences and connection, you edit the identification to your choice.
Example:
Username: Nicklas
Device ID: Android
Tracker ID: NS (this is arbitrary but must be 2 digits or letters)
Now edit in items file and replace according to what you chose:
String ItemMQTT { mqtt="<[broker:owntracks/Nicklas/Android:state:default]" }

Important, in rule MQTT you must edit this line to whatever the tracker ID you gave in owntracks application configuration: if (_id == ā€œNSā€) {
In Patriks example, he has PG.

In the rule initializeSystem, edit with your home coordinates (and googlemapAPI).

This was pretty easy to set up when you know what to do, but I had to troubleshoot the ā€œPGā€ to make it work.
Thanks @patrik_gfeller for the code!

If one looks more closely, they will see that you get some information from the phone as well, like battery.
Add to sitemap:
Text item=ItemNicklasNatelBattery
And for instance a switch that shows if you are home or not in sitemap:
Switch item=ItemNicklasAtHome

@patrik_gfeller, I have some problem displaying it in HABDroid. Itā€™s just a white box.
In chrome it works but does not refresh.
Also the zoom seems off when I open it and I manually have to center the map.
Any pointers?

1 Like

Unfortunately I do not know why it does not work in HABDroid - as I use web UI only. The center of the map should be set in function ā€œinitializeMapā€:

        function initializeMap() {
            // Get home location from openHAB ...
            var location = GetOpenHABItemState("ItemLocationHome");
            // Convert to google data type ...
            var locationHome = new google.maps.LatLng(
                parseFloat(location.split(',')[0]),
                parseFloat(location.split(',')[1]));

            // create map (centered @ home) ...
            map = new google.maps.Map(document.getElementById('map'), {
                center: locationHome,
                mapTypeId: google.maps.MapTypeId.TERRAIN,
                zoom: 10
            });

For the auto-zoom the bounds are important; check that they are set and updated when you add markers (as in the example). The last line of the init map then should set the zoom:

map.fitBounds(bounds);

At the moment the map does not yet auto refresh; you can consider to add a meta to the html to automatically refresh:

<meta http-equiv=ā€refreshā€ content=ā€60" />

with kind regards,
Patrik

1 Like