source : popupService.js

/*
 * Copyright (C) 2016 Atlas of Living Australia
 * All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 * 
 * Created by Temi on 6/09/2016.
 */
(function (angular) {
    'use strict';
    /**
     * @memberof spApp
     * @ngdoc service
     * @name PopupService
     * @description
     *   Map popup generation
     */
    angular.module('popup-service', ['leaflet-directive', 'map-service', 'biocache-service'])
        .factory("PopupService", ['$rootScope', '$compile', '$http', '$q', '$window', '$templateRequest', 'leafletData', 'MapService', 'BiocacheService', 'LayersService',
            function ($rootScope, $compile, $http, $q, $window, $templateRequest, leafletData, mapService, biocacheService, LayersService) {
                var addPopupFlag = true,
                    popup, loc, leafletMap;
                var templatePromise = $templateRequest('/spApp/intersectPopupContent.htm');
                var intersects = [],
                    layers = [],
                    ssLayers = [],
                    speciesLayers = [],
                    occurrences = [],
                    areaLayers = [],
                    occurrenceList;
                var occurrenceBBox = []; //bbox of a popup/occurence. Need to scope level, then won't change when zooming
                var processedLayers = [0];
                var _httpDescription = function (method, httpconfig) {
                    if (httpconfig === undefined) {
                        httpconfig = {};
                    }
                    httpconfig.service = 'PopupService';
                    httpconfig.method = method;
                    return httpconfig;
                };
                function addPopupToMap(latlng, map, templatePromise, intersects, occurrences) {
                    var leafletmap = $('.angular-leaflet-map');
                    var layerCount = layers.length + speciesLayers.length + areaLayers.length;
                    if (addPopupFlag) {
                        templatePromise.then(function (content) {
                            var popupScope = $rootScope.$new();
                            popupScope.processedLayers = processedLayers;
                            // TODO: build a better progress indicator
                            popupScope.intersects = intersects;
                            popupScope.olist = occurrences;
                            var html = $compile(content)(popupScope);
                            popup = L.popup({maxWidth: 500, maxHeight: 600, minWidth: 410, autoPanPadding: 10})
                                .setLatLng(latlng)
                                .setContent(html[0])
                                .openOn(map);
                            addPopupFlag = false
                        })
                    }
                }
                function OccurrenceList(speciesLayers, occurrenceBBox) {
                    var self = this;
                    this.occurrenceBBox = occurrenceBBox
                    this.isFirstOccurrence = true;
                    this.layersWithResults = [];
                    this.occurrences = [];
                    this.pageSize = 1;
                    this.total = 0;
                    this.index = 0;
                    this.speciesLayers = undefined;
                    this.config = $SH;
                    this.zoomedToRecord = false;
                    this.viewRecordUrl = '';
                    this.listRecordsUrl = '';
                    this.getFirstOccurrence = function (layer) {
                        if (self.isFirstOccurrence) {
                            self.getOccurrence(0, layer);
                            self.isFirstOccurrence = false
                        }
                    };
                    /**
                     * Get the fq terms for a layer.
                     *
                     * Optionally include fq terms to
                     * - Restrict results to the clicked bounding box.
                     * - Restrict results to occurrences matching the adhoc terms.
                     * - Restrict results to occurrences not in matching the adhoc terms.
                     *
                     * Adhoc terms are constructed from
                     * - layer.adhocGroup that lists occurrence ids that are included or excluded.
                     * - layer.adhocBBoxes that lists bounding boxes of occurrences that are included.
                     *
                     * @param layer
                     * @param includeClickedBox boolean
                     * @param includeAdhoc boolean
                     * @param excludeAdhoc boolean
                     * @returns {Array}
                     */
                    this.getFq = function (layer, includeClickedBox, includeAdhoc, excludeAdhoc) {
                        var facetFq = biocacheService.facetsToFq(layer.facets, true);
                        var fq = [];
                        if (facetFq.fq) {
                            if (layer.isSelectedFacetsOnly) {
                                fq = facetFq.fq;
                            } else if (layer.isWithoutSelectedFacetsOnly) {
                                var fqs = facetFq.fq.splice()
                                for (var i = 0; i < fqs.length; i++) {
                                    // Use (*:* AND -facet:*) instead of (-facet:*)
                                    if (fqs[i].match(/^-[^\s]*:\*$/) != null) {
                                        fqs[i] = '*:* AND ' + fqs[i]
                                    }
                                }
                                fq = ["-((" + fqs.join(") AND (") + "))"];
                            }
                        }
                        // include clicked bbox
                        if (includeClickedBox) {
                            fq.push("latitude:[" + this.occurrenceBBox[0][0] + " TO " + this.occurrenceBBox[1][0] + "]")
                            fq.push("longitude:[" + this.occurrenceBBox[0][1] + " TO " + this.occurrenceBBox[1][1] + "]");
                        }
                        var includedFqs = []
                        var excludedFqs = []
                        // include adhoc term
                        if (includeAdhoc || excludeAdhoc) {
                            for (var k in layer.adhocGroup) {
                                if (layer.adhocGroup[k]) {
                                    // include
                                    includedFqs.push("id:" + k)
                                } else {
                                    // exclude
                                    excludedFqs.push("id:" + k)
                                }
                            }
                            for (var i in layer.adhocBBoxes) {
                                var occurrenceBBox = layer.adhocBBoxes[i];
                                includedFqs.push("(latitude:[" + occurrenceBBox[0][0] + " TO " + occurrenceBBox[1][0] + "] AND longitude:[" + occurrenceBBox[0][1] + " TO " + occurrenceBBox[1][1] + "])");
                            }
                        }
                        if (includeAdhoc) {
                            if (includedFqs.length > 0) {
                                fq.push(includedFqs.join(" OR "))
                            }
                            if (excludedFqs.length > 0) {
                                fq.push("(*:* AND -" + excludedFqs.join(" AND -") + ")")
                            }
                        }
                        if (excludeAdhoc) {
                            var term = '';
                            if (includedFqs.length > 0) {
                                term += "(*:* AND -" + includedFqs.join(" AND -") + ")";
                                if (excludedFqs.length > 0) {
                                    term += " OR "
                                }
                            }
                            if (excludedFqs.length > 0) {
                                term += excludedFqs.join(" OR ")
                            }
                            if (term.length > 0) {
                                fq.push(term)
                            }
                        }
                        return fq;
                    }
                    this.getOccurrence = function (index, layer) {
                        var fq = this.getFq(layer, true, false, false)
                        self.layer = layer;
                        biocacheService.searchForOccurrences(layer, fq, 1, index).then(function (data) {
                            if (data.occurrences && data.occurrences.length) {
                                addPopupToMap(loc, leafletMap, templatePromise, intersects, occurrenceList);
                                // empty array
                                data.occurrences[0].layername = layer.name;
                                self.occurrences.splice(0, self.occurrences.length);
                                self.occurrences.push.apply(self.occurrences, data.occurrences);
                                //check if ticked or in bbox
                                self.tickOccurence();
                                self.viewRecord()
                            }
                        })
                    };
                    this.tickOccurence = function () {
                        this.isAdhocOrBBox()
                    }
                    this.isAdhocGroup = function () {
                        var layer = self.layer;
                        var id = self.occurrences[0].uuid;
                        if (layer.adhocGroup === undefined) {
                            layer.adhocGroup = {};
                            layer.adhocGroupSize = 0
                        }
                        return layer.adhocGroup[id] !== undefined && layer.adhocGroup[id]
                    };
                    this.isInBBox = function () {
                        var layer = self.layer;
                        var oc = self.occurrences[0];
                        var id = oc.uuid;
                        if (layer.adhocBBoxes && layer.adhocBBoxes.length > 0) {
                            for (var i in layer.adhocBBoxes) {
                                var occurrenceBBox = layer.adhocBBoxes[i];
                                if (oc.decimalLatitude <= occurrenceBBox[1][0] && oc.decimalLatitude >= occurrenceBBox[0][0] &&
                                    oc.decimalLongitude <= occurrenceBBox[1][1] && oc.decimalLongitude >= occurrenceBBox[0][1]) {
                                    return true;
                                }
                            }
                        }
                        return false;
                    }
                    this.isAdhocOrBBox = function () {
                        var oc = self.occurrences[0];
                        var id = oc.uuid;
                        if (self.isAdhocGroup()) {// it is MANUALLY checked
                            oc.adhocGroup = true;
                        } else if (self.layer.adhocGroup[id] == false) { // Manually unchecked,
                            oc.adhocGroup = false;
                        } else {
                            oc.adhocGroup = self.isInBBox();
                        }
                    }
                    this.toggleAdhocGroup = function () {
                        var layer = self.layer;
                        var oc = self.occurrences[0];
                        var id = self.occurrences[0].uuid;
                        if (layer.adhocGroup === undefined) {
                            layer.adhocGroup = {};
                            layer.adhocGroupSize = 0
                        }
                        //this variable is linked with its Checkbox
                        //it may be set by bbox which related to the function 'add all to adhoc'
                        var isChecked = oc.adhocGroup;
                        layer.adhocGroup[id] = isChecked;
                        //Caculate adhocGroupSize
                        this.countAdhocOccurences();
                        this.buildAdhocQuery();
                    };
                    this.getNextOccurrence = function () {
                        var nextIndex = self.index + 1;
                        if (nextIndex >= self.total) {
                            return
                        }
                        self.index += 1;
                        var query = this.getSearchLayerAndIndex();
                        this.getOccurrence(query.index, query.layer)
                    };
                    this.getPrevOccurrence = function () {
                        var nextIndex = self.index - 1;
                        if (nextIndex >= self.total || nextIndex < 0) {
                            return
                        }
                        self.index -= 1;
                        var query = this.getSearchLayerAndIndex();
                        this.getOccurrence(query.index, query.layer)
                    };
                    this.listRecords = function () {
                        if (self.layer !== undefined) {
                            var fq = self.getFq(self.layer, true, false, false)
                            var url = biocacheService.constructSearchResultUrl(self.layer, fq, 10, 0, true).then(function (url) {
                                self.listRecordsUrl = url
                            })
                        }
                    };
                    this.getSearchLayerAndIndex = function () {
                        var result = {layer: undefined, index: 0},
                            total = 0;
                        _.filter(self.layersWithResults, function (layer) {
                            return layer.isDisplayed
                        })
                            .forEach(function (layer) {
                                if (layer) {
                                    if ((self.index < (total + layer.total) && (self.index >= total))) {
                                        result.layer = layer;
                                        result.index = self.index - total
                                    }
                                    total += layer.total
                                }
                            });
                        return result
                    };
                    this.showThisLayerOnly = function (targetlayer) {
                        _.each(self.layersWithResults, function (layer) {
                            if (layer.name == targetlayer.name)
                                layer.isDisplayed = true;
                            else
                                layer.isDisplayed = false;
                        })
                        //Reset to show oc over the layer, not selected facets
                        targetlayer.isSelectedFacetsOnly = false;
                        targetlayer.isWithoutSelectedFacetsOnly = false;
                        this.toggleDisplayLayer(targetlayer)
                    }
                    this.showSelOfLayer = function (targetlayer, isSel) {
                        _.each(self.layersWithResults, function (layer) {
                            if (layer.name == targetlayer.name)
                                layer.isDisplayed = true;
                            else
                                layer.isDisplayed = false;
                        })
                        if (isSel) {
                            if (targetlayer.isSelectedFacetsOnly) {
                                self.index = 0;
                                self.total = targetlayer.selCount
                            } else if (targetlayer.isWithoutSelectedFacetsOnly) {
                                self.index = 0;
                                self.total = targetlayer.withoutSelCount;
                            }
                            this.getOccurrence(0, targetlayer);
                        } else {
                            targetlayer.isSelectedFacetsOnly = false;
                            targetlayer.isWithoutSelectedFacetsOnly = false;
                            this.toggleDisplayLayer(targetlayer)
                        }
                    }
                    this.toggleDisplayLayer = function (targetedlayer) {
                        var result = {layer: undefined, index: 0}
                        var futureLayerIdx = 0;
                        var selectedLayerIdx = _.findIndex(self.layersWithResults, function (layer) {
                            return layer.name == targetedlayer.name;
                        })
                        //if check in a layer
                        if (targetedlayer.isDisplayed) {
                            futureLayerIdx = selectedLayerIdx;
                        } else { //check out the layer
                            // index jumps to the first oc of next layer. Move to first layer if the selected is the last layer
                            var c = self.layersWithResults.length;
                            if (selectedLayerIdx == c - 1) { // last one - then move to the first item of first layer
                                futureLayerIdx = 0;
                            } else {
                                futureLayerIdx = selectedLayerIdx + 1;
                            }
                        }
                        result.layer = self.layersWithResults[futureLayerIdx];
                        //recalculate the GLOBAL idx and total count of selected layer for display
                        self.total = 0;
                        var displayedLayers = _.filter(self.layersWithResults, function (layer) {
                            return layer.isDisplayed
                        })
                        for (var i = 0; i < displayedLayers.length; i++) {
                            // find the current layer and caculate GLOBAL idx
                            if (result.layer.name == displayedLayers[i].name)
                                self.index = self.total + result.index;
                            self.total += displayedLayers[i].total
                        }
                        this.getOccurrence(result.index, result.layer);
                    }
                    this.toggleSelFacadeOnLayer = function (targetedLayer) {
                        targetedLayer.isWithoutSelectedFacetsOnly = false;
                        targetedLayer.isSelectedFacetsOnly = true;
                        this.showSelOfLayer(targetedLayer, true);
                    }
                    this.toggleUnSelFacadeOnLayer = function (targetedLayer) {
                        targetedLayer.isWithoutSelectedFacetsOnly = true;
                        targetedLayer.isSelectedFacetsOnly = false;
                        this.showSelOfLayer(targetedLayer, true);
                    }
                    this.toggleBBox = function () {
                        //New feautre: bind with Sel facade
                        var layer = self.layer;
                        if (layer.adhocBBoxes == undefined) layer.adhocBBoxes = [];
                        if (layer.adhocGroup == undefined) layer.adhocGroup = {};
                        // Toggle occurrenceBBox globally for the layer, regardless of the facet or layer selection
                        var jsonBBox = JSON.stringify(this.occurrenceBBox);
                        var idx = layer.adhocBBoxes.findIndex(function (box) {
                            return JSON.stringify(box) == jsonBBox
                        })
                        if (idx == -1) {
                            layer.adhocBBoxes.push(this.occurrenceBBox);
                            self.countAdhocOccurences();
                            //check if current oc is in the bbox
                            self.tickOccurence();
                        } else {
                            adhocBBoxes.splice(idx, 1)
                        }
                        this.buildAdhocQuery();
                    }
                    this.countAdhocOccurences = function () {
                        var layer = self.layer;
                        var fq = this.getFq(layer, false, true, false)
                        if (layer.adhocBBoxes.length > 0 || layer.adhocGroup.length > 0) {
                            biocacheService.count(layer, fq).then(function (count) {
                                layer.adhocGroupSize = count;
                            });
                        } else {
                            layer.adhocGroupSize = 0;
                        }
                    }
                    this.buildAdhocQuery = function () {
                        // //sync q to layer, which is used by spLegend or other place
                        self.layer.inAdhocQ = this.getFq(self.layer, false, true, false);
                        self.layer.outAdhocQ = this.getFq(self.layer, false, false, true);
                    }
                    this.viewRecord = function () {
                        if (self.occurrences && self.occurrences.length > 0) {
                            var url = self.layer.ws + "/occurrences/" + self.occurrences[0].uuid;
                            self.viewRecordUrl = url
                        }
                    };
                    this.zoomToRecord = function () {
                        self.zoomedToRecord = true;
                        var occ = self.occurrences[0];
                        var lattng = L.latLng(occ.decimalLatitude, occ.decimalLongitude);
                        mapService.leafletScope.zoomToPoint(lattng, 10);
                        $('.leaflet-popup-close-button')[0].click()
                    };
                    this.devMode = function () {
                        if ($SH.enviroment === 'DEVELOPMENT')
                            return true;
                        else
                            return false;
                    }
                    this.showAdhocBBoxList = function () {
                        this.hideAhhocBBoxList = !this.hideAhhocBBoxList
                    }
                    this.removeAdhocBBox = function (idx) {
                        this.layer.adhocBBoxes.splice(idx, 1);
                    }
                    speciesLayers.forEach(function (layer) {
                        var fq = self.getFq(layer, true, false, false)
                        biocacheService.count(layer, fq).then(function (count) {
                            if (count !== undefined && count > 0) {
                                self.layersWithResults.push(layer);
                                layer.total = count;
                                layer.isDisplayed = true;
                                self.total += count;
                                self.getFirstOccurrence(layer)
                            }
                            processedLayers[0] += 1;
                            self.listRecords();
                            self.viewRecord();
                        }).then(function () {
                            //Count occurences with facet selection
                            if (layer.sel) {
                                //layer.sel has been encoded in spLengend.js
                                var inFq = fq.slice();
                                inFq.push(decodeURIComponent(layer.sel));
                                biocacheService.count(layer, inFq).then(function (count) {
                                    layer.selCount = count;
                                })
                                // sel ends
                                var outFq = fq.slice()
                                var fq = decodeURIComponent(layer.sel)
                                // Use -(*:* AND -facet:*) instead of -(-facet:*)
                                if (fq.matches(/^-[^\s]*:\*$/) != null) {
                                    outFq.push('-(*:* AND ' + fq + ')')
                                } else {
                                    outFq.push('-(' + fq + ')')
                                }
                                biocacheService.count(layer, outFq).then(function (count) {
                                    layer.withoutSelCount = count;
                                })
                            }
                        })
                    });
                    var self = this;
                }
                return {
                    /**
                     * Coordinate
                     * @typedef {Object} latlng
                     * @property {number} lat - latitude
                     * @property {number} lng - longitude
                     */
                    /**
                     * Open a popup on the map with information about a coordinate.
                     *
                     * @memberof PopupService
                     * @param (latlng) latlng coordinate input coordinate as lat,lng
                     *
                     * @example:
                     * Input:
                     * - latlng
                     *  {lat:latitude, lng:longitude}
                     * Output:
                     *  [{
                        "studyId": 92,
                        "focalClade": "Acacia",
                        "treeFormat": "newick",
                        "studyName": "Miller, J. T., Murphy, D. J., Brown, G. K., Richardson, D. M. and González-Orozco, C. E. (2011), The evolution and phylogenetic placement of invasive Australian Acacia species. Diversity and Distributions, 17: 848–860. doi: 10.1111/j.1472-4642.2011.00780.x",
                        "year": 2011,
                        "authors": "Acacia – Miller et al 2012",
                        "doi": "http://onlinelibrary.wiley.com/doi/10.1111/j.1472-4642.2011.00780.x/full",
                        "numberOfLeaves": 510,
                        "numberOfInternalNodes": 509,
                        "treeId": null,
                        "notes": null,
                        "treeViewUrl": "http://phylolink.ala.org.au/phylo/getTree?studyId=92&treeId=null"
                        }]
                     */
                    click: function (latlng) {
                        if (!latlng) {
                            return;
                        }
                        var self = this;
                        loc = latlng;
                        // reset flag
                        addPopupFlag = true;
                        processedLayers[0] = 0;
                        intersects.splice(0, intersects.length);
                        layers.splice(0, layers.length);
                        occurrences.splice(0, occurrences.length);
                        speciesLayers.splice(0, speciesLayers.length);
                        areaLayers.splice(0, areaLayers.length);
                        ssLayers.splice(0, ssLayers.length);
                        leafletData.getMap().then(function (map) {
                            leafletMap = map;
                            mapService.mappedLayers && mapService.mappedLayers.forEach(function (layer) {
                                if (layer.visible) {
                                    switch (layer.layertype) {
                                        case "contextual":
                                            var f = LayersService.getLayer(layer.id);
                                            if (!$SH.wmsIntersect || (f && f.type === 'a')) {
                                                ssLayers.push(layer.id)
                                            } else {
                                                layers.push(layer)
                                            }
                                            break;
                                        case "grid":
                                            if ($SH.wmsIntersect) {
                                                layers.push(layer);
                                            } else {
                                                ssLayers.push(layer.id);
                                            }
                                            break;
                                        case "area":
                                            if (layer.type === "envelope") {
                                                var layerid = '' + layer.id;
                                                var f = LayersService.getLayer(layerid);
                                                if (!$SH.wmsIntersect || (f && f.type === 'a')) {
                                                    ssLayers.push(layerid)
                                                } else {
                                                    layers.push(layer)
                                                }
                                            } else {
                                                areaLayers.push(layer);
                                            }
                                            break;
                                        case "species":
                                            speciesLayers.push(layer);
                                            break;
                                    }
                                }
                            });
                            if (ssLayers.length) {
                                var promiseIntersect = LayersService.intersectLayers(ssLayers, latlng.lng, latlng.lat);
                                if (promiseIntersect) {
                                    promiseIntersect.then(function (content) {
                                        intersects.push.apply(intersects, content.data);
                                        addPopupToMap(loc, leafletMap, templatePromise, intersects, occurrenceList);
                                        processedLayers[0] += intersects.length;
                                    })
                                }
                            }
                            if (layers.length) {
                                var promiseIntersect = LayersService.getFeatureInfo(layers, leafletMap, latlng);
                                if (promiseIntersect) {
                                    promiseIntersect.then(function (content) {
                                        var result = content;
                                        intersects.push.apply(intersects, result);
                                        addPopupToMap(loc, leafletMap, templatePromise, intersects, occurrenceList);
                                        processedLayers[0] += intersects.length;
                                    })
                                }
                            }
                            if (areaLayers.length) {
                                areaLayers.forEach(function (layer) {
                                    LayersService.getAreaIntersects(layer.pid, latlng).then(function (resp) {
                                        if (resp.data.name) {
                                            intersects.push({layername: $i18n(402, "Area"), value: resp.data.name});
                                            addPopupToMap(loc, leafletMap, templatePromise, intersects, occurrenceList);
                                        }
                                        processedLayers[0] += 1
                                    })
                                })
                            }
                            //Caculate bbox of the clicked occurence - scope level
                            if (speciesLayers[0]) {
                                var layer = speciesLayers[0]
                                var dotradius = layer.size * 1 + 3;
                                var px = leafletMap.latLngToContainerPoint(loc);
                                var ll = leafletMap.containerPointToLatLng(L.point(px.x + dotradius, px.y + dotradius));
                                var lonSize = Math.abs(loc.lng - ll.lng);
                                var latSize = Math.abs(loc.lat - ll.lat);
                                occurrenceBBox = [[loc.lat - latSize, loc.lng - lonSize], [loc.lat + latSize, loc.lng + lonSize]]
                            }
                            occurrenceList = new OccurrenceList(speciesLayers, occurrenceBBox);
                        });
                    }
                }
            }])
}(angular));