/* Minification failed. Returning unminified contents.
(14,23): run-time error CSS1031: Expected selector, found '='
(14,23): run-time error CSS1025: Expected comma or open brace, found '='
(15,13): run-time error CSS1031: Expected selector, found '='
(15,13): run-time error CSS1025: Expected comma or open brace, found '='
(16,12): run-time error CSS1031: Expected selector, found '='
(16,12): run-time error CSS1025: Expected comma or open brace, found '='
(17,20): run-time error CSS1031: Expected selector, found '='
(17,20): run-time error CSS1025: Expected comma or open brace, found '='
(35,23): run-time error CSS1031: Expected selector, found '='
(35,23): run-time error CSS1025: Expected comma or open brace, found '='
(43,23): run-time error CSS1031: Expected selector, found '='
(43,23): run-time error CSS1025: Expected comma or open brace, found '='
(55,10): run-time error CSS1031: Expected selector, found 'parseJsonDate('
(55,10): run-time error CSS1025: Expected comma or open brace, found 'parseJsonDate('
(80,10): run-time error CSS1031: Expected selector, found 'FormatDateHR('
(80,10): run-time error CSS1025: Expected comma or open brace, found 'FormatDateHR('
(109,10): run-time error CSS1031: Expected selector, found 'FormatMoneyHR('
(109,10): run-time error CSS1025: Expected comma or open brace, found 'FormatMoneyHR('
(123,10): run-time error CSS1031: Expected selector, found 'NullStr('
(123,10): run-time error CSS1025: Expected comma or open brace, found 'NullStr('
(128,10): run-time error CSS1031: Expected selector, found 'IsNull('
(128,10): run-time error CSS1025: Expected comma or open brace, found 'IsNull('
(143,10): run-time error CSS1031: Expected selector, found 'ToAppPath('
(143,10): run-time error CSS1025: Expected comma or open brace, found 'ToAppPath('
(156,10): run-time error CSS1031: Expected selector, found 'StrToInt('
(156,10): run-time error CSS1025: Expected comma or open brace, found 'StrToInt('
(169,16): run-time error CSS1031: Expected selector, found '='
(169,16): run-time error CSS1025: Expected comma or open brace, found '='
(248,10): run-time error CSS1031: Expected selector, found 'ArrayUnique('
(248,10): run-time error CSS1025: Expected comma or open brace, found 'ArrayUnique('
(259,10): run-time error CSS1031: Expected selector, found 'getParameterByName('
(259,10): run-time error CSS1025: Expected comma or open brace, found 'getParameterByName('
(310,19): run-time error CSS1031: Expected selector, found '='
(310,19): run-time error CSS1025: Expected comma or open brace, found '='
(712,30): run-time error CSS1031: Expected selector, found '='
(712,30): run-time error CSS1025: Expected comma or open brace, found '='
(898,3): run-time error CSS1019: Unexpected token, found '('
(898,4): run-time error CSS1019: Unexpected token, found ')'
(903,26): run-time error CSS1031: Expected selector, found '='
(903,26): run-time error CSS1025: Expected comma or open brace, found '='
(1579,2): run-time error CSS1019: Unexpected token, found '('
(1579,3): run-time error CSS1019: Unexpected token, found ')'
 */
/**
* Axiom WebGIS library: objekti i priručne funkcije.
* Dependency o vanjskim library-jima: OpenLayers, OData 
*    karta ->  na temelju OpenLayers-a
*    podaci -> OData
*/

/*******************************************
*
* Konstante
*
*******************************************/

var GoogleMercatorStr = "EPSG:900913";
var WGSPROJ = new OpenLayers.Projection("EPSG:4326"); // WGS 1984 projection
var SMPROJ = new OpenLayers.Projection("EPSG:900913"); // Spherical Mercator Projection
var GEOJSON_FORMAT = new OpenLayers.Format.GeoJSON();

/*******************************************
*
* Globalne varijable
*
*******************************************/

// var map; // OpenLayers karta - potrebno inicijalizirati (new OpenLayers.Map...)

/*******************************************
*
* Priručne funkcije 
*
*******************************************/
// lpad i rpad funkcije
// izvor: http://sajjadhossain.com/2008/10/31/javascript-string-trimming-and-padding/
//pads left
String.prototype.lpad = function (padString, length) {
    var str = this;
    while (str.length < length)
        str = padString + str;
    return str;
}

//pads right
String.prototype.rpad = function (padString, length) {
    var str = this;
    while (str.length < length)
        str = str + padString;
    return str;
}        

/**
* Vraća datum iz stringa u json datumskom formatu
*
* @returns Date datum 
*/
function parseJsonDate(jsonDate) {
    // izvor: http://sudarshanbhalerao.wordpress.com/2011/08/14/convert-json-date-into-javascript-date/
    
    // var offset = new Date().getTimezoneOffset() * 60000;
    var parts = /\/Date\((-?\d+)([+-]\d{2})?(\d{2})?.*/.exec(jsonDate);

    if (parts[2] == undefined)
        parts[2] = 0;

    if (parts[3] == undefined)
        parts[3] = 0;

    // bez offset-a (time zona & zimsko/ljetno vrijeme)
    var d1 = new Date(+parts[1] + parts[2] * 3600000 + parts[3] * 60000);
    return new Date(d1.getTime() + d1.getTimezoneOffset() * 60000);
}

/**
* Formatiranje json datuma u HR formatu dd.mm.yyyy
*
* @param string jDate - datum u json formatu
* @param boolean yearonly - vratiti samo godinu
*
* @returns string datum u formatu dd.mm.yyyy
*/
function FormatDateHR(jDate, yearOnly) {
    yearOnly = typeof yearOnly !== 'undefined' ? yearOnly : false;
    try {
        var ddate;
        if(typeof(jDate) == 'string') {
            if (jDate.substring(0,5) == '/Date') // if we got json date, eg. '/Date(1224043200000)/'
                ddate = parseJsonDate(jDate);
            else { // JSON "2004-08-17T00:00:00"
                ddate = new Date(jDate);
                ddate = new Date(ddate.getTime() - ddate.getTimezoneOffset() * 60000);
            }
        }
        else {
            ddate = jDate;
        }
        // HR shortdate format, bilo bi ljepše internacionalno (svakom prema lokalnom)
        var dan = String(ddate.getUTCDate()).lpad("0", 2); // ovdje UTCDate jer inače dodaje offset timezone
        var mjesec = String(ddate.getUTCMonth() + 1).lpad("0", 2);
        if (!yearOnly) 
            return dan + "." + mjesec + "." + ddate.getUTCFullYear();
        else
            return ddate.getUTCFullYear().toString();                
    }
    catch (e) {
        return "";
    }
}

// from: http://www.josscrowcroft.com/2011/code/format-unformat-money-currency-javascript/
function FormatMoneyHR(number, places, symbol, thousand, decimal) {
    places = !isNaN(places = Math.abs(places)) ? places : 2;
    symbol = symbol !== undefined ? symbol : ""; // "$";
    thousand = thousand || ".";
    decimal = decimal || ",";
    var negative = number < 0 ? "-" : "",
        i = parseInt(number = Math.abs(+number || 0).toFixed(places), 10) + "",
        j = (j = i.length) > 3 ? j % 3 : 0;
    return symbol + negative + (j ? i.substr(0, j) + thousand : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thousand) + (places ? decimal + Math.abs(number - i).toFixed(places).slice(2) : "");
}

/**
* Returns empty string("") if @val is null
*/
function NullStr(val)
{
	return val === null ? "" : val;
}

function IsNull(val, retval)
{
    return val === null ? retval : val;
}

/**
* Na root direktorij aplikacije dodaje zadani path
* - za root se uzima:
*  1) ukoliko je definirana varijabla AppRoot, uzima se ta
*  2) inače prvi direktorij u path-u aktivnog URL-a
*
* @param string path - path relativan od root-a aplikacije
*
* @returns string apsolutni path u odnosu na root aplikacije
*/
function ToAppPath(path) {
    if (typeof AppRoot != 'undefined')
        return AppRoot + path;
    else {
        var firstdir = window.location.pathname.replace(/^\/([^\/]*).*$/, '$1')
        if(firstdir.indexOf('.') >= 0)  // if it has extension it is a file in root folder -> return root
            return '/' + path;
        else 
            return '/' + firstdir + '/' + path;
    }
}

// Returns int or isnullval if str is not a valid integer. isnullval = undefined if parameter is not provided.
function StrToInt(str, isnullval) {
    if (str == "" || isNaN(str))
        return isnullval;
    return parseInt(str);
}

// locale semi-agnostic string to float conversion [JavaScript], https://gist.github.com/GerHobbelt/2037124
// parseFloat() but now locale aware and trying to be locale agnostic
//
// put in closure to cache intel which you don't want to calc each time
// you convert a string --> float
//

var StrToFloat = (function () {
    var decimal_point = (1.1).toLocaleString().substr(1, 1);
    var K_separator = (5000).toLocaleString().substr(1, 1);
    if (K_separator === '0')
        K_separator = (decimal_point === '.' ? ',' : '.');

    return function (str) {
        switch (typeof (str)) {
            case 'float':
            case 'integer':
                return str;

            default:
                str = '' + str; // force str to be string type
                str = str.match(/[0-9.,eE-]+/);
                if (str)
                    str = str[0];
                else
                    return Number.NaN; // VERY illegal number format

                var kp = str.indexOf(',');
                var dp = str.indexOf('.');
                var kpl = str.lastIndexOf(',');
                var dpl = str.lastIndexOf('.');
                // can we be 'locale agnostic'? We can if both markers are in the input:
                if (kp > 0 && dp > 0) {
                    if (kp < dp) {
                        // e.g.: 1,000.00
                        if (kpl > dpl || dpl > dp)
                            return Number.NaN; // VERY illegal number format
                        str = str.replace(/,/g, '');
                    } else {
                        // e.g.: 1.000,00
                        if (kpl < dpl || kpl > kp)
                            return Number.NaN; // VERY illegal number format
                        str = str.replace(/\./g, '').replace(',', '.');
                    }
                } else {
                    // only one of 'em in there: must we use the detected 'current' locale
                    // or can we 'heuristically determine' what's right here?
                    // We can do the latter if we have either:
                    // - only up to 2 digits following the only separator
                    // - more than 3 digits following the only separator
                    // - one separator only and the number starts with it, e.g. '.45'
                    //
                    // When we have only 3 digits following the last and only sep, we assume the separator
                    // to be a 'thousands marker'.
                    // We COULD fall back to the current locale, but than the ambiguous items receive
                    // different treatment on a possibly incorrect locale setting (e.g. my machines are all
                    // US/metric configured while I live in the Netherlands, but I /despise/ Dutch MS Excel,
                    // for example. Riding the 'locale aware' ticket would definitely screw up my [ambiguous]
                    // numbers. I believe it's better to be consistently wrong than semi-randomly correct.
                    //
                    // examples: 1.000 : 5,00 : 2,999.95 : 2,998 : 7.50 : 0,1791634 : 0.1791634 : .45 : ,32
                    if (dp >= 0 && kp < 0) {
                        // .45, ....
                        if (dp !== 0 && (dpl > dp || str.substr(dp + 1).match(/^[0-9]{3}\b/)))
                            str = str.replace(/\./g, '');
                    } else if (kp >= 0 && dp < 0) {
                        // ,32; ....
                        if (kp !== 0 && (kpl > kp || str.substr(kp + 1).match(/^[0-9]{3}\b/)))
                            str = str.replace(/,/g, '');
                        else
                            str = str.replace(',', '.');
                    } else if (kp < 0 && dp < 0) {
                        // integer value
                    } else {
                        // VERY illegal format, such as '.456,678'
                        return Number.NaN;
                    }
                }
                // now str has parseFloat() compliant format with US decimal point
                // only (iff it has a decimal fraction at all).
                return parseFloat(str);
        }
    };
})();

// jedinstvene vrijednosti iz polja
function ArrayUnique(arr) {
    var hash = {}, result = [];
    for (var i = 0, l = arr.length; i < l; ++i) {
        if (!hash.hasOwnProperty(arr[i])) { //it works with objects! in FF, at least
            hash[arr[i]] = true;
            result.push(arr[i]);
        }
    }
    return result;
}

function getParameterByName(name) {
    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
        results = regex.exec(location.search);
    return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}

/*******************************************
*
* "Wrapper objekti": karta, odata
*
*******************************************/

/*

"Class" used to create objects that know how to handle Entity Framework service query and populate jquery mobile table.
"Row clicked" handler is attached to populated table rows. Additional "info div" will then be used to display data on details page, when customer selects desired table row.
Required parameters:

 tableid  - ID of table tag, eg. <table id="TablicaPokojnika"..
 rowcfg   - Table columns configuration array is essential commponent to map service columns to html table columns. 
            HTML table will be constructed accordingly (no "design time" <th>, <tr>, <td> is needed). Eg.:

                [
                     {name:"PREZIME_OSOB",  visible:true,  priority:0, width:150, caption:"Prezime", filterfunc: NullStr}
                    ,{name:"IME_OSOB",      visible:true,  priority:0, width:150, caption:"Ime",     filterfunc: NullStr}
                    ..
                    ,{name:"DATUM_SMRTI",   visible:false, priority:2, width:80, caption:"Godina smrti",filterfunc: function(d){return FormatDateHR(d, true);}}
                    ,{name:"DATUM_UKOPA",   visible:true,  priority:2, width:80, caption:"Datum ukopa", filterfunc: function(d){return FormatDateHR(d, true).replace('01.01.', '');}}
                    ..
                    ,{name:"GPS_N",         visible:false, priority:0, width:0, caption:"GPS N",   filterfunc: NullStr}
                    ,{name:"GPS_E",         visible:false, priority:0, width:0, caption:"GPS E",   filterfunc: NullStr}
                    ,{name:"HANDLE_KARTA",  visible:false, priority:0, width:0, caption:"Handle",  filterfunc: NullStr}
                ];
                
            - "priority" is jquery mobile table attribute by which jquery mobile can decide which columns to hide on lower screen resolutions. Column with lower number have higher "display" priority.
            - "width" - column display "width" attribute is used for the desktop version (jquery.datatables).

 infodiv  - ID of div tag in which clicked row data replacement will be executed on row clicked event. Span tags for target column data must be attributed with "data-columnname" attribute. Eg.:
             <div id="ownerinfo" style="padding-left:  10px;">
               Šifra: <span data-columnname="SIFRA_POSP"></span>
               Naziv: <span data-columnname="NAZIV_POSP"></span>
               ..
             </div>					
 rowselected_handler - event to attach custom row selected function, eg. function myRowHandler(row) {.. which will be called after the row is selected.
 beforedata_handler  - event is called after the first data chunk arives from server.
 
 After the object is created "init" method must be called to initialize table.
 To populate data execute "executeQuery(svcuri, params)", eg. executeQuery("Trazilica.svc/PvWebUkopani?", {ime:'marko%', prezime:'markovic%', groblje='%}) 
 
*/
var web_viewModel = function (tableid, rowconfig, infodiv, rowselected_handler, beforedata_handler, datatable_options) {
    // object variables
    var tableId = tableid;
    var infoDiv = infodiv
    var rowcfg = rowconfig;
    var dtOptions = typeof datatable_options == 'undefined' ? {} : datatable_options; // override za defaultne opcije, putem $.extend(

    // event handlers
    var OnRowSelected = rowselected_handler;
    var OnBeforeData  = beforedata_handler;
    
    // Mobile version uses jquery mobile tables and desktop version, jquery.datatable. IsMobile is set in Init().
    var IsMobile = false; 
    // nonmobile version uses jquery.dataTable pluggin, mobile - jQuery mobile
    var oTable; // datatable object	
    var tab_counter = 1; // incremented for each queryNext
    var currentRow = undefined;
    var updateRow = false; // do not clear table, just update current row
    var LastHANDLE_KARTA = "";  // keep reference of last retrieved shape handle
    var LastExecutedQuery = "";
    var IsDT110 = false; // is jquery datatables version > 1.10
    var LastResultSet = null;
    
    /**
    * Initialize table #tableId using rowcfg
    * @param boolean ismobile - when true uses jquery moble tables, else jquery.datatables
    */   
    function init(ismobile) {
    
        IsMobile = typeof ismobile !== 'undefined' ? ismobile : false;        
        
        if(IsMobile) { // set table column headers
            // table header row from rowcfg
            var row = '<tr class="ui-bar-c">';
            for (var column in rowcfg) {
                row += (rowcfg[column].visible ? '<th class="ui-table-priority-' + rowcfg[column].priority + '">' : '<th style="display:none;">') + rowcfg[column].caption + '</th>';
                // dozvola za editiranje na info stranici
                if(typeof(userAuthorized) == "boolean" && userAuthorized && rowcfg[column].editable)
                    $("#" + infoDiv + " [data-columnname='" + rowcfg[column].name + "']").editable(rowcfg[column].editable);
            }
            row += '</tr>';
            $("table#" + tableId + " thead tr").remove();
            $("table#" + tableId + " thead").append(row);

            // row selected handler
            $(document).on("click", "table#" + tableId + " tbody td", rowclick);
        }
        else { // initialize jquery.datatable
            var dtva = $.fn.dataTable.version.split('.');
            IsDT110 = parseInt(dtva[0]) >= 1 && parseInt(dtva[1]) >= 10;

            var aoColumnsDef = [];
            var row = '<tr>';
            for (var column in rowcfg) {
                var thTag = '<th>';
                if (rowcfg[column].ColumnClass)
                    thTag = '<th class="' + rowcfg[column].ColumnClass + '">';
                else
                    thTag = '<th class="tekstLijevo">'; // IE, Firefox po defaultu centriraju
                row += (rowcfg[column].visible ? thTag : '<th style="display:none;">') + rowcfg[column].caption + '</th>';
                if (IsDT110)
                    aoColumnsDef.push({
                        className: typeof (rowcfg[column].ColumnClass) == "string" ? rowcfg[column].ColumnClass : "tekstLijevo",
                        width: rowcfg[column].width.toString() + 'px',
                        visible: rowcfg[column].visible
                        }
                    );
                else
                    aoColumnsDef.push({
                        sClass: typeof (rowcfg[column].ColumnClass) == "string" ? rowcfg[column].ColumnClass : "tekstLijevo",
                        sWidth:   rowcfg[column].width.toString() + 'px', 
                        bVisible: rowcfg[column].visible
                      }
                    );
            }
            row += '</tr>';            
            $("table#" + tableId + " thead tr").remove();
            // set table column headers
            $("table#" + tableId + " thead").append(row);            

           
            oTable = $('#' + tableId).dataTable(
                $.extend(
                   { "iDisplayLength": 10,
                       "bFilter": false,
                       "bSort": false,
                       "bJQueryUI": true,
                       "oLanguage": {
                           "sProcessing": "Procesiram...",
                           "sLengthMenu": "Prikaži _MENU_ redova po stranici",
                           "sZeroRecords": "Ništa nije pronađeno",
                           "sInfo": "Prikazano _START_ do _END_ od _TOTAL_ slogova. Za detalje kliknite na željeni red!",
                           "sInfoEmpty": "Prikazano 0 do 0 od 0 slogova",
                           "sInfoFiltered": "(filtrirano iz ukupnih _MAX_ redova)",
                           "sInfoPostFix": "",
                           "sSearch": "Filter",
                           "sUrl": "",
                           "oPaginate": {
                               "sFirst": "Prva",
                               "sPrevious": "Nazad",
                               "sNext": "Naprijed",
                               "sLast": "Zadnja"
                           }
                       },
                       "aoColumns": aoColumnsDef            
                   }
               ,dtOptions)
            );
			
			// rajko, za sad skrivam nepotrebni header (kako preko opcije?)
			$('.fg-toolbar.ui-corner-tl').hide();
            // row selected handler
            $("#" + tableId + " tbody td").live('click', rowclick);            
        }       
    };

    function execQuery(svcuri, params, replacerow) {
        updateRow = replacerow;
        if(!replacerow)
            currentRow = undefined;
        var upit = svcuri;
        var andadd = '';
        for (var param in params) {
            if (typeof (params[param]) == "string")
                upit = upit + andadd + param + "=" + "'" + params[param] + "'";
            else if (typeof (params[param]) == "undefined")
                continue;
            else if (typeof (params[param]) == "object" && params[param]._isAMomentObject) // momentjs dates are formated as json text
                upit = upit + andadd + param + "=" + "'" + params[param].zone(params[param].zone()).format('YYYY-MM-DD HH:mm') + "'"; // toJSON() + "'"; // forget zone conversion
            else if (typeof (params[param]) == "object") { // for arrays add paramname=array[0] or paramname=array[1].... For now, array values are treated as strings.
                var oradd = ""
                upit = upit + andadd;
                for (var i = 0; i < params[param].length; i++) {
                    upit = upit + oradd + param + " eq " + "'" + params[param][i] + "'";
                    oradd = " or ";
                }
            }
            else
                upit = upit + andadd + param + "=" + params[param];
            andadd = "&";
        }
        // clear table rows
        if (IsMobile) {
            if (!replacerow)
                $("#" + tableId + " tr.class-tablica-pokojnika").remove();
        }
        else {
            if (IsDT110) {
                oTable.clear().draw();
            }
            else {
                oTable.fnClearTable();
                oTable.fnPageChange('first');
            }
        }
        tab_counter = 1;
        MakeRequest(upit);
    }    
    
    function displayResults(results, filter) {

        var entities;
        if (results[0] == undefined) {
            queryNext = results.__next;
            entities = results.results;
        }
        else {
            queryNext = "";
            entities = results;
        }
		
		LastExecutedQuery = ""; // query completed, allow the same query if wanted..
		queryNext = ""; // legacy ??
		LastResultSet = results;

        var newRows = "";
        var odatarow = [];
        var okrow;
        for (var post in entities) {
            if (IsMobile) {
                if (filter && (typeof filter == "object")) {
                    okrow = true;
                    for (var property in filter) {
                        if (filter.hasOwnProperty(property)) {
                            for (var column in rowcfg) {
                                if (property == rowcfg[column].name && !(rowcfg[column].filterfunc(entities[post][rowcfg[column].name]) == filter[property])) {
                                    okrow = false;
                                    break;
                                }
                            }
                        }
                        if (!okrow)
                            break;
                    }
                }
                else
                    okrow = true;
                if (!okrow)
                    continue;
                var row = '<tr class="class-tablica-pokojnika">';
                /* create <td>'s, examples:                     
                <td class="ui-table-priority-3" data-columnname="KRATICA_PGRMJ">VRANJ</td>                    
                <td style="display:none;" data-columnname="GPS_E">16.494068</td>
                */
                for (var column in rowcfg) {
                    row += (rowcfg[column].visible ? '<td class="ui-table-priority-' + rowcfg[column].priority + '" data-columnname="' + rowcfg[column].name + '">'
                        : '<td style="display:none;" data-columnname="' + rowcfg[column].name + '">') + rowcfg[column].filterfunc(entities[post][rowcfg[column].name]) + '</td>';
                }                
                row += '</tr>';
                newRows += row;
                if(entities[post]["HANDLE_KARTA"] != "")
                    LastHANDLE_KARTA = entities[post]["HANDLE_KARTA"];                
            }
            else {
                if (tableId == "TablicaGMZaProdaju") { // HANDLE_KARTA u id <TR>
                    odatarow = {}; // dict: {0: 'val1', 1: 'val2' .. 'DT_RowId': HANDLE_KARTA]
                    for (var column in rowcfg) {
                        odatarow[column] = rowcfg[column].filterfunc(entities[post][rowcfg[column].name]);
                        if (rowcfg[column].name == "HANDLE_KARTA")
                            odatarow["DT_RowId"] = entities[post][rowcfg[column].name];
                    }
                }
                else {
                    odatarow = [];
                    for (var column in rowcfg)
                        odatarow.push(rowcfg[column].filterfunc(entities[post][rowcfg[column].name]));
                }
                
                if (IsDT110) {
                    oTable.row.add(odatarow);
                    if (post == entities.length - 1)
                        oTable.draw();                    
                }
                else
                    oTable.fnAddData(
                        odatarow
                        ,post == entities.length - 1 // setting redraw to false ...don't redraw after EVERY row..overwhelms client
                    );            
            }
        }        
        if (IsMobile)
        {
            if(updateRow && currentRow) {
                var cidx = currentRow.index() + 1;
                currentRow.replaceWith(newRows);
                currentRow = $('#' + tableId + ' tr').eq(cidx);
                currentRow.addClass('row_selected');
            }
            else
                $("table#" + tableId + " tbody").append(newRows);
        }

        if (tab_counter == 1 && typeof (OnBeforeData) == 'function')
            OnBeforeData(entities.length);
            
        if (queryNext > "") {
            MakeRequest(results.__next);
            tab_counter += 1;
        }
        else {
            tab_counter = 1;
        }
    }

    /*
    * Fills infoDiv "span-s", eg. <span data-columnname="IME_OSOB"> with rowdata 
    * + calls OnRowSelected providing tableId, infodict = {COLUMN1 : "xx"..}
    */    
    function rowclick() {
        currentRow = $(this).parent();

        if (currentRow.hasClass('row_selected'))
            currentRow.removeClass('row_selected');
        else {
            currentRow.parent().find('tr.row_selected').removeClass('row_selected');
            currentRow.addClass('row_selected');
        }
        
        // Array to Dict: aData = [VAL1, VAL2..]   =>   infodict = {COLUMN_NAME1 : "...", COLUMN_NAME2 : ..}
        var infodict = {};        
        if(IsMobile) 
        {
            var tds = currentRow.children('td');
            // details to <span>'s in detail <div> #infodiv, link by same data-columnname                     
            for (var i = 0; i < tds.length; i++) {
                $("#" + infoDiv + " [data-columnname='" + tds.eq(i).attr('data-columnname') + "']").text(tds.eq(i).text());
                infodict[tds.eq(i).attr('data-columnname')] = tds.eq(i).text();
                if (typeof(userAuthorized) == "boolean" && userAuthorized && typeof(rowcfg[i].editable) != "undefined")
                    $("#" + infoDiv + " [data-columnname='" + tds.eq(i).attr('data-columnname') + "']").editable('setValue', tds.eq(i).text(), typeof(rowcfg[i].editable.format) == 'undefined' ? undefined : rowcfg[i].editable.format);
                if (tds.eq(i).attr('data-columnname') == 'HANDLE_KARTA' && tds.eq(i).text() != "")
                    LastHANDLE_KARTA = tds.eq(i).text();                   
            }        
        }
        else
        {
            var aPos = $('#' + tableId).dataTable().fnGetPosition(this);
            var aData = $('#' + tableId).dataTable().fnGetData(aPos[0]);
            $.map(aData, function (el, i) {
                if (rowcfg[i]) { // undefined za DT_RowId (ukoliko se red kreira iz dict-a a ne array-a)
                        $("#" + infoDiv + " [data-columnname='" + rowcfg[i].name + "']").html(el);
                        infodict[rowcfg[i].name] = el;
                    }
                }
            );  
        }
        
        if (typeof (OnRowSelected) == 'function')
            OnRowSelected(tableId, infodict);
    }

    function errorHandler(err) {
		LastExecutedQuery = ""; 
        var errmessage = err.message == "HTTP request failed" ? "Neuspjeh pri pozivu web servisa.." : err.message;
        try {
            if (err.response.body.substring(0, 5) == "<?xml") {
                var xmlParser = new DOMParser();
                errmessage = xmlParser.parseFromString(err.response.body, "text/xml").getElementsByTagName('message')[0].childNodes[0].nodeValue;
            }
            else
                errmessage = JSON.parse(err.response.body)["odata.error"].message.value;
        }
        catch(e) {
        }
        if (IsMobile)
            $.mobile.loading('hide');
        else
            $("#loading").html("").fadeOut(1400);
        if (typeof(toastr) != 'undefined') {
            toastr.options.positionClass = 'toast-top-left';
            toastr.error(errmessage);
        }
        else if (typeof(alertify) != 'undefined')
            alertify.error(errmessage);        
        else
            alert(errmessage);
        
    }

    function MakeRequest(url) {
		// asynchronous requests -> while waiting on response, disallow running the same query twice (for example due to doubleclick)
		if (url == LastExecutedQuery)
			return;
		LastExecutedQuery = url;
        OData.read(url, displayResults, errorHandler);
    };    
    
    // new record (mobile, editable)
    function appendRow() {
        if (!(IsMobile && typeof(userAuthorized) == "boolean" && userAuthorized))
            return;
        var row = '<tr class="class-tablica-pokojnika">';        
        for (var column in rowcfg) {
            row += (rowcfg[column].visible ? '<td class="ui-table-priority-' + rowcfg[column].priority + '" data-columnname="' + rowcfg[column].name + '">'
                : '<td style="display:none;" data-columnname="' + rowcfg[column].name + '">')
                   + ((rowcfg[column].name == "HANDLE_KARTA") ? LastHANDLE_KARTA : "") 
                   + '</td>';
        }
        row += '</tr>';    
        currentRow = $("table#" + tableId + " tbody").append(row).children("tr:last");
        $(currentRow.children()[0]).trigger("click");
    }
    
    function LastShapeHandle(newval) {
        if(typeof(newval) == "string")
            LastHANDLE_KARTA = newval;
        return LastHANDLE_KARTA;
    }

    function applyFilter(filter) {
        if (!IsMobile || !LastResultSet)
            return;
        $("#" + tableId + " tr.class-tablica-pokojnika").remove();
        displayResults(LastResultSet, filter);
    }

    function columDistincts(colname) {
        if ( !LastResultSet || !LastResultSet.results || LastResultSet.results.length == 0 )
            return [];
        var entities;
        if (LastResultSet[0] == undefined)
            entities = LastResultSet.results;
        else
            entities = LastResultSet;		
        result = {}; // unique
        for (var post in entities) {
            for (var column in rowcfg) {
                if (colname == rowcfg[column].name)
                    result[entities[post][colname]] = 1;
            }
        }
        return Object.keys(result);
    }

    // reveal all things private by assigning public pointers
    return {
        init: init, execQuery: execQuery, appendRow: appendRow, LastShapeHandle: LastShapeHandle, applyFilter: applyFilter, columDistincts: columDistincts
    }
};

/**
 * Minimap wrapper ("singleton" objekt). Prije uporabe potrebna je inicijalizacija: AxWebGIS_CemeteryMinimap.init();
*/        
var AxWebGIS_CemeteryMinimap = function () {
    var MINIMAP_DIV; // <div id="..">  
    var MiniMap;     // OpenLayers.Map
    var selectcontrol; // select shape-a u vektorskom sloju
    var Groblja; // referenca na dictionary sa svim relevantnim podacima o grobljima, postavlja se u init-u
    var MinZoom = 0;
    var RestrictedExtent;
    
    /**
     * Inicijalizacija minimap karte.
     *   
     * @param object groblja - dictionary s grobljima i potrebnim atributima. Jedinstven po korisniku (definicija u CemeteryData.js)
     * @param string minimapdiv - id <div> elementa za minimap, "minimap" ako je nedefiniran.
     */
    function init(groblja, minimapdiv) {        
        Groblja = groblja;
        MINIMAP_DIV = typeof minimapdiv !== 'undefined' ? minimapdiv : 'minimap';

        var zoom_panel = new OpenLayers.Control.Panel();
        zoom_panel.addControls([new OpenLayers.Control.ZoomIn(), new OpenLayers.Control.ZoomOut()]);

        MiniMap = new OpenLayers.Map(MINIMAP_DIV, { controls: [zoom_panel, new OpenLayers.Control.Navigation()] });

        var slojevi_polja = [];
        for (var groblje in Groblja) {
            if (Groblja[groblje].hasOwnProperty("polja") && Groblja[groblje].polja.features.length > 0) {
                MiniMap.addLayer(Groblja[groblje].polja);
                slojevi_polja.push(Groblja[groblje].polja);
            }
            if (Groblja[groblje].hasOwnProperty("ulazi") && Groblja[groblje].ulazi.features.length > 0)
                MiniMap.addLayer(Groblja[groblje].ulazi);
        }
        var mapnik = new OpenLayers.Layer.OSM();
        if (window.location.protocol === "https:") {
            for (var urli = 0; urli < mapnik.url.length; urli++)
                mapnik.url[urli] = mapnik.url[urli].replace("http:", "https:");
        }
        mapnik.addOptions({ opacity: 0.4 });
        MiniMap.addLayer(mapnik);        
        selectcontrol = new OpenLayers.Control.SelectFeature(slojevi_polja, { clickout: false, toggle: false, multiple: false, hover: false, box: false });
        MiniMap.addControl(selectcontrol);
        // restrikcija min. zoom levela
        MiniMap.events.register('zoomend', this, function (event) {
            var x = MiniMap.getZoom();
            if (x < MinZoom && RestrictedExtent) {
				MiniMap.zoomToExtent(RestrictedExtent);
                // MiniMap.zoomTo(MinZoom);		
            }
        });
        
        // minimap-u na prvo groblje u hash-u
        for (var groblje in Groblja) {
            if (Groblja[groblje].hasOwnProperty("lon")) {
                MiniMap.setCenter(new OpenLayers.LonLat(Groblja[groblje].lon, Groblja[groblje].lat).transform(WGSPROJ, SMPROJ), Groblja[groblje].overview_zoom);
                break;
            }
        }
        restrictToCurrentExtent();
    }
    
    function clearPosition() {
        var poslayer = MiniMap.getLayersByName("OViewPos");

        if (poslayer.length > 0) {
            poslayer = poslayer[0];
            poslayer.removeAllFeatures();
        }
    }
    
    function markPosition(center) {
        var poslayer = MiniMap.getLayersByName("OViewPos");

        if (poslayer.length < 1) {
            poslayer = new OpenLayers.Layer.Vector("OViewPos");
            MiniMap.addLayer(poslayer);
        }
        else
            poslayer = poslayer[0];

        var mapbounds = MiniMap.getExtent();

        var style;

        if (window.minimap_markpos_style) // custom markopos styling
            style = window.minimap_markpos_style;
        else
            style = { strokeColor: "blue", strokeOpacity: 1, strokeWidth: 1 };
        var smallcross = [new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(
                  [new OpenLayers.Geometry.Point(center.lon, mapbounds.top),
                  ,new OpenLayers.Geometry.Point(center.lon, mapbounds.bottom)
                  ]), null, style)
                  ,new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(
                  [new OpenLayers.Geometry.Point(mapbounds.left, center.lat),
                  ,new OpenLayers.Geometry.Point(mapbounds.right, center.lat)
                  ]), null, style)
        ];

        // za linije puno veće od vidljivog extenta, događa se da postanu nevidljive -> načini dvije oznake, jedna za razinu karte, druga za manji zoom
        var bigcross = [new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(
                  [new OpenLayers.Geometry.Point(center.lon, center.lat - 10 * (mapbounds.top - mapbounds.bottom)),
                  ,new OpenLayers.Geometry.Point(center.lon, center.lat + 10 * (mapbounds.top - mapbounds.bottom))
                  ]), null, style)
                  ,new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(
                  [new OpenLayers.Geometry.Point(center.lon - 10 * (mapbounds.right - mapbounds.left), center.lat),
                  , new OpenLayers.Geometry.Point(center.lon + 10 * (mapbounds.right - mapbounds.left), center.lat)
                  ]), null, style)
        ];

        poslayer.removeAllFeatures();
        poslayer.addFeatures(smallcross);
        poslayer.addFeatures(bigcross);
    }
    
    /**
     * Označi polje na minimapi. 
     * Ova defaultna implementacija uspoređuje samo identifikator polja sa atributom POLJE_WEB u sloju polja.
     * Eventualno dodati funkcije za implementacije gdje ovo ne zadovoljava (ukoliko je npr. uz polje potreban i odjel)
     * 
     * @param string groblje
     * @param string odjelid
     * @param string poljeid
     * 
     * @returns feature openlayersfeature - ako je pronačeno polje po zadanim parametrima
     */
    function highlightPolje(groblje, odjelid, poljeid) {
        var fid, feature;
        var found = false;
        var retval = null;
            
        if(odjelid == null)
            odjelid = "";
        if(poljeid == null)
            poljeid = "";                    
        selectcontrol.unselectAll();
        for (fid in Groblja[groblje].polja.features) {
            feature = Groblja[groblje].polja.features[fid];
            if (feature.data.POLJE_WEB != undefined && feature.data.POLJE_WEB.toUpperCase() == poljeid) {
                selectcontrol.select(feature);
                retval = feature;
            }
        }
        return retval; // može se upotrijebiti za centriranje karte ako grobno mjesto nije spojeno ali ipak pronađemo polje
    }

    /**
    * Centriranje karte na OpenLayers.LonLat poziciju i zoom level.
    * 
    * @param OpenLayers.LonLat centerpoint
    * @param int zoomlevel
    */
    function setCenter(centerpoint, zoomlevel) {
        MiniMap.setCenter(centerpoint, zoomlevel);
    }

    /**
    * Restrikcija extenta karte na trenutno prikazan ekstent.
    */
    function restrictToCurrentExtent() {
        // Rajko, 22.4.2014, pri restrikciji nezgodno svojstvo da kod prvog zoomiranja karta "skače"
        // MiniMap.restrictedExtent = MiniMap.getExtent();
        RestrictedExtent = MiniMap.getExtent();
        MinZoom = MiniMap.getZoom();
    }
    
    /**
    * Makni restrikciju ekstenta minimape.
    */
    function unrestrictedExtent() {
        MiniMap.restrictedExtent = null;
        MinZoom = 0;
    }

    // reveal all things private by assigning public pointers
    return {
        init:                    init,
        markPosition:            markPosition,
        clearPosition:           clearPosition,
        setCenter:               setCenter,
        restrictToCurrentExtent: restrictToCurrentExtent,
        highlightPolje:          highlightPolje,
        unrestrictedExtent:      unrestrictedExtent,
        SelectControl:           function () {
            return selectcontrol;
        } // ukoliko se izvana želi izabirati polja
    }

} ();

/**
* Map wrapper ("singleton" objekt). Prije uporabe potrebna je inicijalizacija: AxWebGIS_CemeteryMap.init();
*/        
var AxWebGIS_CemeteryMap = function () {
    //globalne od objekta
    var MAP_DIV; // <div id="..">  
    var Map;     // OpenLayers.Map
    var MapCenter;        // centar početno postavljenog extenta
    var Groblja;          // referenca na dictionary sa svim relevantnim podacima o grobljima, postavlja se u init-u
    // slojevi koji se isključuju na zoom level
    var LayerCemeteryPlacemarks;
    var LayerOSM;         // Open Street Map
    var LayerNumeracija;
    var LayerPhotoTrack;  // označi pozicije trenutne "foto sesije" 
    var HighlightLayer;   // vektorski sloj sa shape-ovima identificiranim na klik (WFS servis)
    var GeoLocationLayer; // oznaka trenutne pozicije
    var GeoLocationControl;
    var popupCemeteries = [];
    var IsMobile = false;
    var PoiGUIDSearchHandler;
    var movestart; // detektiranje click-a na touch device-u
    var firstGeolocation;
    var prevLat, prevLon, minAccuracy = 25, distPosCount = 0;
    var geoloc_reloadhttps = false;
    var karta;
    
    /**
     * Inicijalizacija karte.
     *   
     * @param object groblja - dictionary s grobljima i potrebnim atributima. Jedinstven po korisniku (definicija u CemeteryData.js)
     * @param string mapdiv - id <div> elementa za mapu, "map" ako je nedefiniran.
     * @param boolean ismobile - mobilna / desktop verzija
     * @param function poiguidsearch_handler - handler pri pozitivnom WFS upitu
     * @param cemetery_placemarks_title - naslov sloja s oznakama groblja
     */
    function init(groblja, mapdiv, ismobile, poiguidsearch_handler, cemetery_placemarks_title) {
        Groblja = groblja;
        MAP_DIV = typeof mapdiv !== 'undefined' ? mapdiv : 'map';
        IsMobile = typeof ismobile !== 'undefined' ? ismobile : false;
        PoiGUIDSearchHandler = typeof poiguidsearch_handler !== 'undefined' ? poiguidsearch_handler : function () {};        
        
        if(IsMobile) {
            // Geo lokacija
            GeoLocationLayer = new OpenLayers.Layer.Vector('GeoLocationLayer', {displayInLayerSwitcher: false, isBaseLayer: false});
            GeoLocationControl = new OpenLayers.Control.Geolocate({
                bind: false,
                geolocationOptions: {
                    enableHighAccuracy: true,
                    maximumAge: 0, 
                    timeout: 20000
                }
            });
            GeoLocationControl.events.register("locationupdated", this, locationUpdated);
            GeoLocationControl.events.register("locationfailed", this, function (evt) {
                if ($.mobile && (($.mobile.activePage.attr("id") == "searchpage") || ($.mobile.activePage.attr("id") == "overviewtablepage") || ($.mobile.activePage.attr("id") == "infopage")))
                    $.mobile.changePage("#mappage", "flip", true, false);
                var poruka = "Neuspješno određivanje pozicije. Omogućite korištenje lokacije u postavkama uređaja te koristite sigurnu/https adresu tražilice.";
                if (geoloc_reloadhttps)
                    poruka = "Neuspješno određivanje pozicije. Omogućite korištenje lokacije u postavkama uređaja te eventualno ponovo učitajte stranicu sa sigurne/https adrese.";
                typeof(alertify) == "object" ? alertify.error(poruka) : alert(poruka);
            });           
            this.Map = new OpenLayers.Map(mapdiv, { controls: [
                    new OpenLayers.Control.TouchNavigation({
                        dragPanOptions: {
                            enableKinetic: true
                        },
                        clickHandlerOptions: {
                            handleSingle: touchSingleClick
                        }
                    })
                ,GeoLocationControl
            ], 
                units: 'm', projection: GoogleMercatorStr, maxResolution: 'auto', allOverlays: true, displayProjection: WGSPROJ
            });
        }
        else {
            this.Map = new OpenLayers.Map('map', { controls: [
                new OpenLayers.Control.Navigation(),
                new OpenLayers.Control.PanZoomBar(),
                new OpenLayers.Control.LayerSwitcher({ roundedCornerColor: 'black'  }),
                new OpenLayers.Control.ScaleLine(),
                new OpenLayers.Control.MousePosition(),
                new OpenLayers.Control.ZoomBox()
            ], units: 'm', projection: GoogleMercatorStr, maxResolution: 'auto', allOverlays: true, displayProjection: WGSPROJ
            });
        }
        karta = this.Map;
        
        // vidljivi ekstent se postavlja u CemeteryData
        var lb = new OpenLayers.LonLat(mapconstraints.extent_left, mapconstraints.extent_bottom).transform(WGSPROJ, SMPROJ);
        var rt = new OpenLayers.LonLat(mapconstraints.extent_right, mapconstraints.extent_top).transform(WGSPROJ, SMPROJ);
        this.Map.maxExtent = new OpenLayers.Bounds(lb.lon, lb.lat, rt.lon, rt.lat);
        
        var layerPoljaBase =      groblja_getLayerList("mappolja");    
        var layerGrobnaMjesta =   groblja_getLayerList("mapgrmj");
        var layerInfrastruktura = groblja_getLayerList("mapobjekti");
        var layerOznakePolja =    groblja_getLayerList("mapoznakepolja"); 
        var layerOznakeUlaza =    groblja_getLayerList("mapulazi");
        var layerNumeracijaGrmj = groblja_getLayerList("mapnumeracija");
        var layerZelenilo       = groblja_getLayerList("mapzelenilo");
        
        LayerOSM = new OpenLayers.Layer.OSM("Openstreet karta", "", { sphericalMercator: true, isBaseLayer: false, zoomOffset: 11, resolutions: [76.437028271, 38.218514136, 19.109257068, 9.554628534, 4.777314267, 2.388657133, 1.194328567, 0.597164283, 0.2985821415, 0.14929107075, 0.074645535375, 0.0373227676875] });
        if (window.location.protocol === "https:") {
            for (var urli = 0; urli < LayerOSM.url.length; urli++)
                LayerOSM.url[urli] = LayerOSM.url[urli].replace("http:", "https:");
        }
        this.Map.addLayer(LayerOSM);

        // "Tile layers"
        var tileopts = { zoomOffset: 11, numZoomLevels: 24, sphericalMecator: true, maxResolution: 156543.0339, maxExtent: new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34) };
        if (layerPoljaBase != "")
            this.Map.addLayer(new OpenLayers.Layer.XYZ("Polja", "/ats.ashx?MAPSID=" + mapconstraints.MAPSID + "&Z=${z}&X=${x}&Y=${y}" + "&LAYERS=" + layerPoljaBase  + "&FMT=png", tileopts));
        if (layerZelenilo != "")
            this.Map.addLayer(new OpenLayers.Layer.XYZ("Zelenilo", "/ats.ashx?MAPSID=" + mapconstraints.MAPSID + "&Z=${z}&X=${x}&Y=${y}" + "&LAYERS=" + layerZelenilo + "&FMT=png", tileopts));                        
        if (layerInfrastruktura != "")
            this.Map.addLayer(new OpenLayers.Layer.XYZ("Infrastruktura", "/ats.ashx?MAPSID=" + mapconstraints.MAPSID + "&Z=${z}&X=${x}&Y=${y}" + "&LAYERS=" + layerInfrastruktura + "&FMT=png", tileopts));
        if (layerGrobnaMjesta != "")
            this.Map.addLayer(new OpenLayers.Layer.XYZ("Grobna mjesta", "/ats.ashx?MAPSID=" + mapconstraints.MAPSID + "&Z=${z}&X=${x}&Y=${y}" + "&LAYERS=" + layerGrobnaMjesta + "&FMT=png", tileopts));

        // WMS layers (numeracija, oznake ulaza)
        var wmsopts = { singleTile: true, isBaseLayer: false, transitionEffect: 'resize'};
        if (layerOznakePolja != "")
            this.Map.addLayer(new OpenLayers.Layer.WMS("Oznake polja", "/wms.ashx?MAPSID=" + mapconstraints.MAPSID, { layers: layerOznakePolja, version: '1.3.0', format: 'image/png', transparent: true }, wmsopts));
        if (layerNumeracijaGrmj != "") {
            // kod mobilnog opterećenje na malom ekranu - može se koristiti IsMobile
            LayerNumeracija = new OpenLayers.Layer.WMS("Oznake grobnih mjesta", "/wms.ashx?MAPSID=" + mapconstraints.MAPSID, { layers: layerNumeracijaGrmj, version: '1.3.0', format: 'image/png', transparent: true }, wmsopts);
            this.Map.addLayer(LayerNumeracija);
        }
        if(layerOznakeUlaza != "")
            this.Map.addLayer(new OpenLayers.Layer.WMS("Oznake ulaza", "/wms.ashx?MAPSID=" + mapconstraints.MAPSID, { layers: layerOznakeUlaza, version: '1.3.0', format: 'image/png', transparent: true }, wmsopts));

        // HighlightLayer - WFS server
        HighlightLayer = new OpenLayers.Layer.Vector("Highlighted Features", { displayInLayerSwitcher: false, isBaseLayer: false });
        this.Map.addLayer(HighlightLayer);
        // pozicije groblja
        if (typeof cemetery_placemarks_title !== 'undefined' && cemetery_placemarks_title != "") {
            LayerCemeteryPlacemarks = createlayerCemeteryPlacemarks(cemetery_placemarks_title, this.Map);
            this.Map.addLayer(LayerCemeteryPlacemarks);
        }
        if(IsMobile) {
            this.Map.addLayer(GeoLocationLayer);
            LayerPhotoTrack = new OpenLayers.Layer.Markers("Foto");
            this.Map.addLayer(LayerPhotoTrack);
            populatePhotoTrack();
            LayerPhotoTrack.setVisibility(false);
        }

        OpenLayers.Util.onImageLoadError = function () { this.src = 'img/blank.gif'; }
        OpenLayers.Tile.Image.useBlankTile = false;

        this.Map.events.register("zoomend", this, zoomEnd);
        
        if (IsMobile) {
            // single click bude samo ako prethodno nije poslan movestart
            this.Map.events.register("movestart", this, function(evt) {movestart = true;});
			this.Map.events.register("moveend", this, function(evt) {movestart = false;});
		}
        else // desktop: click
            this.Map.events.register('click', this, getFeatures);
        this.Map.zoomToExtent(this.Map.maxExtent);
        MapCenter = this.Map.center;            
    }
    
    /**
    * URL za poziv WFS servisa
    * @param locationxy lat, lon točke za koju se traže
    * @returns string URL WFS servisa
    */
    function wfsURL(locationxy) {
        return "/ats.ashx?MAPSID=" + mapconstraints.MAPSID + "&LAYERS=" + groblja_getLayerList("mapgrmj") + "&GETFEATURE=TRUE&X=" + locationxy.lon + "&Y=" + locationxy.lat;
    }    
    
    /**
    * Pomoć pri razlikovanju single click-a, move i zoom-a na touch device-ima
    */
    function touchSingleClick(event){
        // WFS za touch device (bez ovog ne proslijeđuje single click)
        // samo ako su vidljiva grobna mjesta (još veći zoom zbog touch-a) 
        // (ovdje je this objekt touchnavigation)
        if (this.map.zoom > 9 && !movestart)
        {
            HighlightLayer.destroyFeatures();
            var loc = this.map.getLonLatFromPixel(event.xy);
            OpenLayers.Request.GET({url: wfsURL(loc), callback: showFeatureInfo}); 
        }
        movestart = false;        
    }
    
    function getFeatures(event) {
        // samo ako su vidljiva grobna mjesta
        // (ovdje je this ovaj objekt)
        if (this.Map.zoom >= 8) {
            HighlightLayer.destroyFeatures();
            var loc = this.Map.getLonLatFromPixel(event.xy);
            OpenLayers.Request.GET({url: wfsURL(loc), callback: showFeatureInfo});
        }
    }
    
    function showFeatureInfo(evt) {
        var geoJasonObjekti = GEOJSON_FORMAT.read(evt.responseText, "FeatureCollection");

        if (geoJasonObjekti.length > 0) {
            HighlightLayer.addFeatures(geoJasonObjekti);
            HighlightLayer.redraw();
            /* Rajko, 15.10.2012, ukoliko je više poligona jedan iznad drugog (greška u editiranju)..moguće da šaljemo nespojeni
            // u principu bi trebao vratiti samo jedan shape..uzimamo prvi
            web_ukopani_viewModel.poiguidSearch(geoJasonObjekti[0].attributes.POIGUID);
            */
            var handles = [];
            for (var i = 0, l = geoJasonObjekti.length; i < l; ++i)
                handles.push(geoJasonObjekti[i].attributes.POIGUID);
//           web_ukopani_viewModel.poiguidSearch(handles);
            if (IsMobile)
                setTimeout(function() {PoiGUIDSearchHandler(handles);}, 1000); // ostani na stranici da se vidi da je poligon označen
            else
                PoiGUIDSearchHandler(handles);
        }
    }    
    
    function zoomEnd(event) {
        LayerOSM.setVisibility(this.Map.zoom <= 7);
        if (typeof LayerCemeteryPlacemarks !== 'undefined')
            LayerCemeteryPlacemarks.setVisibility(this.Map.zoom < 6);
        if (IsMobile) {
            LayerNumeracija.setVisibility(this.Map.zoom > 9);
            LayerPhotoTrack.setVisibility(this.Map.zoom > 8)
        }
        movestart = false; // touch, identifikacija single click-a
    }
    
    /**
    * Iz hashmap-a groblja, vrati string sa comma separated vrijednostima željenog atributa. Pogodno za kreiranje popisa slojeva pri pozivu wms servisa.
    *  Pr. groblja_getLayerList("mappolja") vraća "PoljaDubovac,PoljaDubovac_VZP,PoljaJamadol,..."
    *
    * @param string atribut 
    * @returns string comma separated lista vrijednosti atributa 
    */
    function groblja_getLayerList(atribut)
    {
        retval = [];
        for (var groblje in Groblja) {
            if (Groblja[groblje].hasOwnProperty("grupa") && Groblja[Groblja[groblje].grupa].hasOwnProperty(atribut)) // moji slojevi su već dodani putem grupe
                continue;
            if (Groblja[groblje].hasOwnProperty(atribut)) 
                retval.push(Groblja[groblje][atribut]);
        }
        return retval.join(",");
    }    
    
    function addMarker(point) {
        var markerlayer = this.Map.getLayersByName("Markers");

        if (markerlayer.length < 1) {
            markerlayer = new OpenLayers.Layer.Markers("Markers", { displayInLayerSwitcher: false });
            this.Map.addLayer(markerlayer);
        }
        else
            markerlayer = markerlayer[0];

        markerlayer.clearMarkers();

        if (GeoLocationLayer)
            GeoLocationLayer.removeAllFeatures();

        var size = new OpenLayers.Size(75, 40);
        var offset = new OpenLayers.Pixel(-(size.w / 2), -size.h);
        markerlayer.addMarker(new OpenLayers.Marker(point, new OpenLayers.Icon(ToAppPath('Content/App/img/marker.png'), size, offset)));
        markerlayer.redraw();
    }

    /**
     * Postavi markere na GPS pozicije grobnih mjesta koja su fotografirana od strane trenutno prijavljenog korisnika (u zadnjih?)
     */  
    function populatePhotoTrack()
    {
        if (!(typeof(userAuthorized) == "boolean" && userAuthorized))
            return;
        OData.defaultHttpClient.enableJsonpCallback = true;
        var icon_marker = new OpenLayers.Icon(ToAppPath('Content/App/img/camera.png'), new OpenLayers.Size(24, 24), new OpenLayers.Pixel(-12, -12));

        OData.request({
                requestUri: ToAppPath("ObevBlobs.svc/MyPhotoTrack"),
                headers: { "Content-Type": "application/json" }
            },
            function (data, response) {
                LayerPhotoTrack.clearMarkers();
                $.each(data.results, function (key, value) {
                    if (value.GPS_E != null)
                        LayerPhotoTrack.addMarker(new OpenLayers.Marker(new OpenLayers.LonLat(value.GPS_E, value.GPS_N).transform(WGSPROJ, SMPROJ), icon_marker.clone()));
                });
                LayerPhotoTrack.redraw();
            },function (err) {
               // console.log("OData, PhotoTrack: " + err.message);
            }
            ,OData.jsonHandler
        );
    }

    function getMarker(){
        var markerlayer = this.Map.getLayersByName("Markers");
        if (markerlayer.length < 1) {
            return null;
        }
        if (markerlayer[0].markers.length < 1) {
            return null;
        } else {
            return markerlayer[0].markers[0].lonlat;
        }
    }    
    
    /**
     * Kreira sloj oznaka grobalja na pregledu. 
     *   
     * @param string layername - naziv sloja u kome će biti oznake (prijedlog: naziv korisnika, npr. "Zelenilo d.o.o. Karlovac" )
     */    
    function createlayerCemeteryPlacemarks(layername, map) {
        var popup;
        var marker;

        function markerPopup(evt) {
            var title = (typeof this.adresa_groblja == "function") ? this.adresa_groblja() : this.adresa_groblja;
            var popup = new OpenLayers.Popup.FramedCloud("Popup",
                        this.lonlat,
                        null,
                        '<div>' + title + '</div>',
                        null,
                        false);
            var mymap = map;
            mymap.addPopup(popup);            
            setInterval(function (){mymap.removePopup(popup)}, 3000);
            OpenLayers.Event.stop(evt);
        }
        
        LayerCemeteryPlacemarks = new OpenLayers.Layer.Markers(layername);
        var size = new OpenLayers.Size(24,24);
        var offset = new OpenLayers.Pixel(-(size.w/2), -size.h);
        var icon_marker = new OpenLayers.Icon(ToAppPath('Content/App/img/cemetery_placemark.png?v=20220304'), size, offset);        
        for (var groblje in Groblja) {
            var myicon = icon_marker.clone();
            myicon.imageDiv.className += " olCemeteryMarker";
            marker = new OpenLayers.Marker(new OpenLayers.LonLat(Groblja[groblje]["lon"], Groblja[groblje]["lat"]).transform(WGSPROJ, SMPROJ), myicon);
            marker.adresa_groblja = Groblja[groblje]["adresa"];
            marker.Map = map;
            marker.events.register('mousedown', marker, markerPopup);                
            LayerCemeteryPlacemarks.addMarker(marker);
        }         
        return LayerCemeteryPlacemarks;            
    }    
        
    /**
    * Prikaži popup oblačić za groblje izabrano u izborniku za lociranje groblja;
    */    
    function cemeteryInfoPopup(nazgroblja) {    
        var groblje, groblje_centar;
     
        function cemeteryInfoPopupDestroy() {
            if (popupCemeteries.length>0)
                 for (var i = 0; i < popupCemeteries.length; i++) {
                     popupCemeteries[i].destroy();
                 }
                 popupCemeteries = [];
        }         
        
        groblje = Groblja[nazgroblja];
        if (groblje) {
            cemeteryInfoPopupDestroy();
            groblje_centar = new OpenLayers.LonLat(groblje.lon, groblje.lat).transform(WGSPROJ, SMPROJ);
            var title = (typeof groblje.adresa == "function") ? groblje.adresa() : groblje.adresa;
            popupCemeteries.push(new OpenLayers.Popup.FramedCloud(nazgroblja,
                new OpenLayers.LonLat(groblje_centar.lon, groblje_centar.lat),
                new OpenLayers.Size(300, 50),
                title,
            null, true, null)
            );
            this.Map.addPopup(popupCemeteries[0]);
            this.Map.setCenter(groblje_centar, 2);            
        }        
    }
    
    function locationUpdated(evt) {        
        // console.log(evt.position.coords.accuracy);
        
        if (evt.position.coords.latitude != prevLat || evt.position.coords.longitude != prevLon) {
            distPosCount++;
            prevLat = evt.position.coords.latitude;
            prevLon = evt.position.coords.longitude;
        }

        //if (true)
        if (evt.position.coords.accuracy <= minAccuracy && distPosCount>1) //tražimo dobru preciznost i da nije prvo što nam GPS vrati (vrati iz cache-a i svašta)
         {
            GeoLocationControl.deactivate(); //samo jednom

            GeoLocationLayer.removeAllFeatures();
            // samo ako je u vidljivom ekstentu
            if (!this.Map.maxExtent.contains(evt.point.x, evt.point.y)) {
                alert('Vaša (GPS) lokacija je izvan maksimalnog dozvoljenog područja!');
                return;
            }
            //if (!this.Map.getExtent().contains(evt.point.x, evt.point.y)) {
            //alert('Vaša (GPS) lokacija je izvan područja prikazanog na ekranu. Povećajte područje (-) i pokušajte ponovo.');
            //return;
            var point;
            point = this.getMarker();
            if (point == null) {
                //nema markera, samo zoomiraj gdje sam sad
                this.Map.setCenter([evt.point.x, evt.point.y]);
            }
            else {
                var xmin, ymin, xmax, ymax;
                xmin = Math.min(evt.point.x, point.lon) - 7; // bar još 7m
                ymin = Math.min(evt.point.y, point.lat) - 7;
                xmax = Math.max(evt.point.x, point.lon) + 7;
                ymax = Math.max(evt.point.y, point.lat) + 7;

                this.Map.zoomToExtent(new OpenLayers.Bounds(xmin, ymin, xmax, ymax));

                // measure line
                var distpoints = new Array(new OpenLayers.Geometry.Point(evt.point.x, evt.point.y), new OpenLayers.Geometry.Point(point.lon, point.lat));
                var distline = new OpenLayers.Geometry.LineString(distpoints);
                var dist = Math.round(distline.getLength());

                var lineFeature = new OpenLayers.Feature.Vector(distline, null, style = {
                    strokeWidth: 3,
                    strokeOpacity: 1,
                    strokeColor: "#666666",
                    strokeDashstyle: "dash"
                });

                var textFeature = new OpenLayers.Feature.Vector(
                                new OpenLayers.Geometry.Point((distpoints[0].x + distpoints[1].x) / 2, (distpoints[0].y + distpoints[1].y) / 2), {}, {
                                    label: "  ~" + dist.toString() + " m",
                                    fontColor: "#FF0000",
                                    fontSize: "16px",
                                    fontFamily: "Arial",
                                    fontWeight: "bold",
                                    labelAlign: "lm"
                                });

                GeoLocationLayer.addFeatures([lineFeature, textFeature]);

            }

            var circle = new OpenLayers.Feature.Vector(
                OpenLayers.Geometry.Polygon.createRegularPolygon(
                    new OpenLayers.Geometry.Point(evt.point.x, evt.point.y),
                    evt.position.coords.accuracy / 2,
                    40,
                    0
                ),
                {},
                { fillColor: '#000', fillOpacity: 0.1, strokeWidth: 0 }
            );            

            GeoLocationLayer.addFeatures([
                new OpenLayers.Feature.Vector(
                    evt.point,
                    {},
                    {
                        graphicName: 'circle',
                        strokeColor: '#f00',
                        strokeWidth: 2,
                        fillOpacity: 0,
                        pointRadius: 10
                    }
                ),
                circle
            ]);
            if (firstGeolocation) {
                if ($.mobile && (($.mobile.activePage.attr("id") == "searchpage") || ($.mobile.activePage.attr("id") == "overviewtablepage") || ($.mobile.activePage.attr("id") == "infopage")))
                    $.mobile.changePage("#mappage", "flip", true, false);
                this.Map.setCenter(new OpenLayers.Geometry.Point(evt.point.x, evt.point.y));
                pulsateGeoLocation(circle);
                firstGeolocation = false;
                this.bind = true; //centrira, poslije uzimamo extent od grobnog mjesta i pozicije na kojoj se korisnik nalazi - upitno da li ovu liniju treba zakomentirati
            }
        }
    }
    
    function pulsateGeoLocation(feature) {
        var point = feature.geometry.getCentroid(),
            bounds = feature.geometry.getBounds(),
            radius = Math.abs((bounds.right - bounds.left)/2),
            count = 0,
            grow = 'up';

        var resize = function(){
            if (count>16) {
                clearInterval(window.resizeInterval);
            }
            var interval = radius * 0.03;
            var ratio = interval/radius;
            switch(count) {
                case 4:
                case 12:
                    grow = 'down'; break;
                case 8:
                    grow = 'up'; break;
            }
            if (grow!=='up') {
                ratio = - Math.abs(ratio);
            }
            feature.geometry.resize(1+ratio, point);
            GeoLocationLayer.drawFeature(feature);
            count++;
        };
        window.resizeInterval = window.setInterval(resize, 50, point, radius);
    }

    function startGeoLocation(highaccuracy)
    {
        if (typeof highaccuracy == "string" && highaccuracy == "geoloc_reloadhttps")
            geoloc_reloadhttps = true;
        else
            geoloc_reloadhttps = false;
        GeoLocationLayer.removeAllFeatures();
        GeoLocationControl.deactivate();
        //GeoLocationControl.watch = false;
        GeoLocationControl.watch = true;
        firstGeolocation = true;
        distPosCount = 0;
        GeoLocationControl.geolocationOptions.enableHighAccuracy = typeof(highaccuracy) == "boolean" ? highaccuracy : true;
        GeoLocationControl.activate();        
    }
    
    function mapCenter()
    {
        return MapCenter;
    }

    function unregisterGetFeatures() {
        this.Map.events.unregister('click', this, getFeatures);
    }

    function zoomToCemetery(cem, zoom) {
        var groblje = Groblja[cem];
        var groblje_centar = new OpenLayers.LonLat(groblje.lon, groblje.lat).transform(WGSPROJ, SMPROJ);
        this.Map.setCenter(groblje_centar, zoom);
    }

    /* Sloj za markiranje pozicije reda (posebno pretraživanje) */
    function GMRowMarkerLayer() {
        var layer = karta.getLayersByName("Pozicija reda");
        if (layer.length < 1) {
            layer = new OpenLayers.Layer.Vector("Pozicija reda", {
                displayInLayerSwitcher: false,
                isBaseLayer: false
            });
            karta.addLayer(layer);
        }
        else
            layer = layer[0];
        return layer;
    }

    /* Bounding Box & start - stop markers of a grave row
       Parameter: pos - object, row boundaries, eg. 
        { 
             "NAZIV_GROB":"Lovrinac"
            ,"POLJE_GRMJ":"1"
            ,"RED_GRMJ":"N"
            ,"ID_GROB":"101"
            ,"GPS_N_MIN":"43.514437"
            ,"GPS_E_MIN":"16.493363"
            ,"GPS_N_MAX":"43.514446"
            ,"GPS_E_MAX":"16.494119"
            ,"GPS_N_START":"43.514437"
            ,"GPS_E_START":"16.493363"
            ,"GPS_N_END":"43.514446"
            ,"GPS_E_END":"16.494119"
            ,"GM_CNT":"102"
        }
    */
    function createRowBBox( pos ) {
        var min_N, min_E, max_N, max_E, start_N, start_E, end_N, end_E;

        min_N = parseFloat(pos.GPS_N_MIN);
        min_E = parseFloat(pos.GPS_E_MIN);
        max_N = parseFloat(pos.GPS_N_MAX);
        max_E = parseFloat(pos.GPS_E_MAX);
        start_N = parseFloat(pos.GPS_N_START);
        start_E = parseFloat(pos.GPS_E_START);
        end_N = parseFloat(pos.GPS_N_END);
        end_E = parseFloat(pos.GPS_E_END);
        min_n = parseFloat(pos.GPS_N_MIN);

        var sw = new OpenLayers.LonLat(min_E, min_N).transform(WGSPROJ, SMPROJ);
        var ne = new OpenLayers.LonLat(max_E, max_N).transform(WGSPROJ, SMPROJ);
        var p1 = new OpenLayers.Geometry.Point(sw.lon, sw.lat);
        var p2 = new OpenLayers.Geometry.Point(sw.lon, ne.lat);
        var p3 = new OpenLayers.Geometry.Point(ne.lon, ne.lat);
        var p4 = new OpenLayers.Geometry.Point(ne.lon, sw.lat);
        var p5 = new OpenLayers.Geometry.Point(sw.lon, sw.lat);
        var pnt = [];
        pnt.push(p1, p2, p3, p4, p5);

        var ln = new OpenLayers.Geometry.LinearRing(pnt);
        var pf = new OpenLayers.Feature.Vector(ln, null, {
            strokeColor: "red",
            strokeOpacity: 1,
            strokeWidth: 3,
            fill: true,
            fillOpacity: 0.5,
            fillColor: "yellow"
        });

        var startP = new OpenLayers.LonLat(start_E, start_N).transform(WGSPROJ, SMPROJ);
        var endP   = new OpenLayers.LonLat(end_E, end_N).transform(WGSPROJ, SMPROJ);
        var startF = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(startP.lon, startP.lat), null, {
            fillColor: "green",
            fillOpacity: 0.5,
            pointRadius: 10
        });
        var endF = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(endP.lon, endP.lat), null, {
            fillColor: "red",
            fillOpacity: 0.5,
            pointRadius: 10
        });

        var layer = GMRowMarkerLayer();
        layer.destroyFeatures();
        layer.addFeatures(pf);

        var center = new OpenLayers.LonLat((sw.lon + ne.lon) / 2.0, (sw.lat + ne.lat) / 2.0);
        karta.setCenter(center, 6);
        setTimeout(function () {
            karta.setCenter(center, 7);
            setTimeout(function () {
                karta.setCenter(center, 8);
                layer.addFeatures(startF);
                layer.addFeatures(endF);
                setTimeout(function () {
                    layer.removeFeatures(pf);
                }, 1500);
            }, 1500);
        }, 2000);
    }

    function markRowGMFeature( GPS_E, GPS_N, label ) {
        var point = new OpenLayers.LonLat(GPS_E, GPS_N).transform(WGSPROJ, SMPROJ);
        var feature = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.Point(point.lon, point.lat), null, {
            fillColor: "yellow",
            fillOpacity: 0.7,
            pointRadius: 20,
            label: label,
            fontColor: "black",
            fontSize: "16px",
            fontWeight: "bold"
        });
        GMRowMarkerLayer().addFeatures(feature);
        return feature;
    }

    // clear single feature or whole layer
    function rowMarkerLayerClear( feature )
    {
        var layer = GMRowMarkerLayer();
        if (feature)
            layer.removeFeatures(feature);
        else
            layer.destroyFeatures();
    }

    // reveal all things private by assigning public pointers
    return {
        Map:                     this.Map,
        init:                    init,
        addMarker:               addMarker,
        getMarker:               getMarker,
        cemeteryInfoPopup:       cemeteryInfoPopup,
        mapCenter:               mapCenter,
        startGeoLocation:        startGeoLocation,
        populatePhotoTrack:      populatePhotoTrack,
        unregisterGetFeatures:   unregisterGetFeatures,
        zoomToCemetery:          zoomToCemetery,
        createRowBBox:           createRowBBox,
        markRowGMFeature:        markRowGMFeature,
        rowMarkerLayerClear:     rowMarkerLayerClear
    }
}();

