<commit_before>define([
    'jquery',
    'underscore',
    'arches',
    'knockout',
    'knockout-mapping',
    'mapbox-gl',
    'mapbox-gl-geocoder',
    'text!templates/views/components/map-popup.htm'
], function($, _, arches, ko, koMapping, mapboxgl, MapboxGeocoder, popupTemplate) {
    var viewModel = function(params) {
        var self = this;
        var geojsonSourceFactory = function() {
            return {
                "type": "geojson",
                "generateId": true,
                "data": {
                    "type": "FeatureCollection",
                    "features": []
                }
            };
        };

        var x = ko.unwrap(params.x) ? params.x : arches.mapDefaultX;
        var y = ko.unwrap(params.y) ? params.y : arches.mapDefaultY;
        var bounds = ko.unwrap(params.bounds) ? params.bounds : arches.hexBinBounds;
        var zoom = ko.unwrap(params.zoom) ? params.zoom : arches.mapDefaultZoom;

        var minZoom = arches.mapDefaultMinZoom;
        var maxZoom = arches.mapDefaultMaxZoom;
        var sources = Object.assign({
            "resource": geojsonSourceFactory(),
            "search-results-hex": geojsonSourceFactory(),
            "search-results-hashes": geojsonSourceFactory(),
            "search-results-points": geojsonSourceFactory()
        }, arches.mapSources, params.sources);
        var mapLayers = params.mapLayers || arches.mapLayers;

        this.map = ko.isObservable(params.map) ? params.map : ko.observable();
        this.popupTemplate = popupTemplate;
        this.basemaps = params.basemaps || [];
        this.overlays = params.overlaysObservable || ko.observableArray();

        this.overlayConfigs = ko.observableArray(params.overlayConfigs());
        this.overlayConfigs.subscribe(function() {
            params.overlayConfigs(self.overlayConfigs());
        })

        this.activeBasemap = params.activeBasemap || ko.observable();
        this.activeBasemap.subscribe(function(basemap) {
            if (this.config) {
                if (ko.isObservable(this.config)) {  // in widget
                    this.config({
                        ...this.config,
                        'basemap': basemap.name,
                    })
                } else {  // in card
                    this.config.basemap(basemap.name);
                }
            }
        }, this);

        this.activeTab = ko.observable(params.activeTab);
        this.hideSidePanel = function() {
            self.activeTab(undefined);
        };
        this.activeTab.subscribe(function() {
            var map = self.map();
            if (map && map.getStyle()) setTimeout(function() { map.resize(); }, 1);
        });

        mapLayers.forEach(function(layer) {
            if (!layer.isoverlay) {
                if (!params.basemaps) self.basemaps.push(layer);

                if (!ko.unwrap(self.activeBasemap)) {
                    if (self.config) {
                        var config = ko.unwrap(self.config);
                        if (ko.unwrap(config.basemap) === layer.name) { self.activeBasemap(layer); }
                    } else if (layer.addtomap) {
                        self.activeBasemap(layer);
                    }
                }
            }
            else if (!params.overlaysObservable) {
                if (layer.searchonly && !params.search) return;
                layer.opacity = ko.observable(layer.addtomap ? 100 : 0);
                layer.onMap = ko.pureComputed({
                    read: function() { return layer.opacity() > 0; },
                    write: function(value) {
                        layer.opacity(value ? 100 : 0);
                    }
                });

                if (self.overlayConfigs.indexOf(layer.maplayerid) > -1) {
                    layer.onMap(100);  // exact value unneccesary but keeps it semantic
                }

                layer.onMap.subscribe(function(onMap) {
                    onMap ? self.overlayConfigs.push(layer.maplayerid) : self.overlayConfigs.remove(layer.maplayerid);
                });

                self.overlays.push(layer);
            }
        });

        _.each(sources, function(sourceConfig) {
            if (sourceConfig.tiles) {
                sourceConfig.tiles.forEach(function(url, i) {
                    if (url.startsWith('/')) {
                        sourceConfig.tiles[i] = window.location.origin + url;
                    }
                });
            }
            if (sourceConfig.data && typeof sourceConfig.data === 'string' && sourceConfig.data.startsWith('/')) {
                sourceConfig.data = arches.urls.root + sourceConfig.data.substr(1);
            }
        });

        var multiplyStopValues = function(stops, multiplier) {
            _.each(stops, function(stop) {
                if (Array.isArray(stop[1])) {
                    multiplyStopValues(stop[1], multiplier);
                } else {
                    stop[1] = stop[1] * multiplier;
                }
            });
        };

        var updateOpacity = function(layer, val) {
            var opacityVal = Number(val) / 100.0;
            layer = JSON.parse(JSON.stringify(layer));
            if (layer.paint === undefined) {
                layer.paint = {};
            }
            _.each([
                'background',
                'fill',
                'line',
                'text',
                'icon',
                'raster',
                'circle',
                'fill-extrusion',
                'heatmap'
            ], function(opacityType) {
                var startVal = layer.paint ? layer.paint[opacityType + '-opacity'] : null;

                if (startVal) {
                    if (parseFloat(startVal)) {
                        layer.paint[opacityType + '-opacity'].base = startVal * opacityVal;
                    } else {
                        layer.paint[opacityType + '-opacity'] = JSON.parse(JSON.stringify(startVal));
                        if (startVal.base) {
                            layer.paint[opacityType + '-opacity'].base = startVal.base * opacityVal;
                        }
                        if (startVal.stops) {
                            multiplyStopValues(layer.paint[opacityType + '-opacity'].stops, opacityVal);
                        }
                    }
                } else if (layer.type === opacityType ||
                     (layer.type === 'symbol' && (opacityType === 'text' || opacityType === 'icon'))) {
                    layer.paint[opacityType + '-opacity'] = opacityVal;
                }
            }, self);
            return layer;
        };

        this.additionalLayers = params.layers;
        this.layers = ko.pureComputed(function() {
            var layers = [];
            self.overlays().forEach(function(layer) {
                if (layer.onMap()) {
                    var opacity = layer.opacity();
                    layers = layer.layer_definitions.map(function(layer) {
                        return updateOpacity(layer, opacity);
                    }).concat(layers);
                }
            });
            if (self.activeBasemap()) {
                layers = self.activeBasemap().layer_definitions.slice(0).concat(layers);
            }
            if (this.additionalLayers) {
                layers = layers.concat(ko.unwrap(this.additionalLayers));
            }
            return layers;
        }, this);

        this.mapOptions = {
            style: {
                version: 8,
                sources: sources,
                sprite: arches.mapboxSprites,
                glyphs: arches.mapboxGlyphs,
                layers: self.layers(),
                center: [
                    parseFloat(ko.unwrap(x)),
                    parseFloat(ko.unwrap(y))
                ],
                zoom: parseFloat(ko.unwrap(zoom)),
            },
            maxZoom: maxZoom,
            minZoom: minZoom,
        };
        if (!params.usePosition) {
            this.mapOptions.bounds = bounds;
            this.mapOptions.fitBoundsOptions = params.fitBoundsOptions;
        }

        this.toggleTab = function(tabName) {
            if (self.activeTab() === tabName) {
                self.activeTab(null);
            } else {
                self.activeTab(tabName);
            }
        };

        this.updateLayers = function(layers) {
            var map = self.map();
            var style = map.getStyle();
            if (style) {
                style.layers = layers;
                map.setStyle(style);
            }
        };

        this.isFeatureClickable = function(feature) {
            return feature.properties.resourceinstanceid;
        };

        this.expandSidePanel = function() {
            return false;
        };

        this.resourceLookup = {};
        this.getPopupData = function(feature) {
            var data = feature.properties;
            var id = data.resourceinstanceid;
            data.showEditButton = false;
            if (id) {
                if (!self.resourceLookup[id]){
                    data = _.defaults(data, {
                        'loading': true,
                        'displayname': '',
                        'graph_name': '',
                        'map_popup': ''
                    });
                    if (data.permissions) {
                        try {
                            data.permissions = JSON.parse(ko.unwrap(data.permissions));
                        } catch (err) {
                            data.permissions = koMapping.toJS(ko.unwrap(data.permissions));
                        }
                        if (data.permissions.users_without_edit_perm.indexOf(ko.unwrap(self.userid)) === -1) {
                            data.showEditButton = true;
                        }
                    }
                    data = ko.mapping.fromJS(data);
                    data.reportURL = arches.urls.resource_report;
                    data.editURL = arches.urls.resource_editor;
                    self.resourceLookup[id] = data;
                    $.get(arches.urls.resource_descriptors + id, function(data) {
                        data.loading = false;
                        ko.mapping.fromJS(data, self.resourceLookup[id]);
                    });
                }
                self.resourceLookup[id].feature = feature;
                self.resourceLookup[id].mapCard = self;
                return self.resourceLookup[id];
            } else {
                data.resourceinstanceid = ko.observable(false);
                data.loading = ko.observable(false);
                data.feature = feature;
                data.mapCard = self;
                return data;
            }
        };

        this.onFeatureClick = function(feature, lngLat) {
            var map = self.map();
            self.popup = new mapboxgl.Popup()
                .setLngLat(lngLat)
                .setHTML(self.popupTemplate)
                .addTo(map);
            ko.applyBindingsToDescendants(
                self.getPopupData(feature),
                self.popup._content
            );
            if (map.getStyle() && feature.id) map.setFeatureState(feature, { selected: true });
            self.popup.on('close', function() {
                if (map.getStyle() && feature.id) map.setFeatureState(feature, { selected: false });
                self.popup = undefined;
            });
        };

        this.setupMap = function(map) {
            map.on('load', function() {
                map.addControl(new mapboxgl.NavigationControl(), 'top-left');
                map.addControl(new mapboxgl.FullscreenControl({
                    container: $(map.getContainer()).closest('.workbench-card-wrapper')[0]
                }), 'top-left');
                map.addControl(new MapboxGeocoder({
                    accessToken: mapboxgl.accessToken,
                    mapboxgl: mapboxgl,
                    placeholder: arches.geocoderPlaceHolder,
                    bbox: arches.hexBinBounds
                }), 'top-right');

                self.layers.subscribe(self.updateLayers);

                var hoverFeature;
                map.on('mousemove', function(e) {
                    var style = map.getStyle();
                    if (hoverFeature && hoverFeature.id && style) map.setFeatureState(hoverFeature, { hover: false });
                    hoverFeature = _.find(
                        map.queryRenderedFeatures(e.point),
                        self.isFeatureClickable
                    );
                    if (hoverFeature && hoverFeature.id && style) map.setFeatureState(hoverFeature, { hover: true });
                    map.getCanvas().style.cursor = hoverFeature ? 'pointer' : '';
                });

                map.on('click', function(e) {
                    if (hoverFeature) {
                        self.onFeatureClick(hoverFeature, e.lngLat);
                    }
                });

                self.map(map);
                if (params.fitBounds){
                    var padding = 40;
                    var activeTab = self.activeTab();
                    var options = {
                        padding: {
                            top: padding,
                            left: padding + (activeTab ? 200: 0),
                            bottom: padding,
                            right: padding + (activeTab ? 200: 0)
                        },
                        animate: false
                    };
                    var bounds = params.fitBounds();
                    if (bounds) {
                        map.fitBounds(bounds, options);
                    } else {
                        var fitBounds = params.fitBounds.subscribe(function(bounds) {
                            map.fitBounds(bounds, options);
                            fitBounds.dispose();
                        });
                    }
                }
                setTimeout(function() { map.resize(); }, 1);

                if (ko.isObservable(zoom)) {
                    map.on('zoomend', function() {
                        zoom(map.getZoom());
                    });
                    zoom.subscribe(function(level) {
                        level = parseFloat(level);
                        if (level) map.setZoom(level);
                    });
                }

                if (ko.isObservable(x)) {
                    map.on('dragend', function() {
                        var center = map.getCenter();
                        x(center.lng);
                    });
                    x.subscribe(function(lng) {
                        var center = map.getCenter();
                        lng = parseFloat(lng);
                        if (lng) {
                            center.lng = lng;
                            map.setCenter(center);
                        }
                    });
                }

                if (ko.isObservable(y)) {
                    map.on('dragend', function() {
                        var center = map.getCenter();
                        y(center.lat);
                    });
                    y.subscribe(function(lat) {
                        var center = map.getCenter();
                        lat = parseFloat(lat);
                        if (lat) {
                            center.lat = lat;
                            map.setCenter(center);
                        }
                    });
                }
            });
        };
    };
    return viewModel;
});
<commit_msg>possibly breaking refactor #6729<commit_after>define([
    'jquery',
    'underscore',
    'arches',
    'knockout',
    'knockout-mapping',
    'mapbox-gl',
    'mapbox-gl-geocoder',
    'text!templates/views/components/map-popup.htm'
], function($, _, arches, ko, koMapping, mapboxgl, MapboxGeocoder, popupTemplate) {
    var viewModel = function(params) {
        var self = this;
        var geojsonSourceFactory = function() {
            return {
                "type": "geojson",
                "generateId": true,
                "data": {
                    "type": "FeatureCollection",
                    "features": []
                }
            };
        };

        var x = ko.unwrap(params.x) ? params.x : arches.mapDefaultX;
        var y = ko.unwrap(params.y) ? params.y : arches.mapDefaultY;
        var bounds = ko.unwrap(params.bounds) ? params.bounds : arches.hexBinBounds;
        var zoom = ko.unwrap(params.zoom) ? params.zoom : arches.mapDefaultZoom;

        var minZoom = arches.mapDefaultMinZoom;
        var maxZoom = arches.mapDefaultMaxZoom;
        var sources = Object.assign({
            "resource": geojsonSourceFactory(),
            "search-results-hex": geojsonSourceFactory(),
            "search-results-hashes": geojsonSourceFactory(),
            "search-results-points": geojsonSourceFactory()
        }, arches.mapSources, params.sources);
        var mapLayers = params.mapLayers || arches.mapLayers;

        this.map = ko.isObservable(params.map) ? params.map : ko.observable();
        this.popupTemplate = popupTemplate;
        this.basemaps = params.basemaps || [];
        this.overlays = params.overlaysObservable || ko.observableArray();

        this.overlayConfigs = ko.observableArray(params.overlayConfigs());
        this.overlayConfigs.subscribe(function(foo) {
            if (params.overlayConfigs !== foo) {
                params.overlayConfigs(foo)
            }
            // params.overlayConfigs(self.overlayConfigs());
        })
        console.log(self, params, self.overlayConfigs())

        this.activeBasemap = params.activeBasemap || ko.observable();
        this.activeBasemap.subscribe(function(basemap) {
            if (this.config) {
                if (ko.isObservable(this.config)) {  // in widget
                    this.config({
                        ...this.config,
                        'basemap': basemap.name,
                    })
                } else {  // in card
                    this.config.basemap(basemap.name);
                }
            }
        }, this);

        this.activeTab = ko.observable(params.activeTab);
        this.hideSidePanel = function() {
            self.activeTab(undefined);
        };
        this.activeTab.subscribe(function() {
            var map = self.map();
            if (map && map.getStyle()) setTimeout(function() { map.resize(); }, 1);
        });

        mapLayers.forEach(function(layer) {
            if (!layer.isoverlay) {
                if (!params.basemaps) self.basemaps.push(layer);

                if (!ko.unwrap(self.activeBasemap)) {
                    if (self.config) {
                        var config = ko.unwrap(self.config);
                        if (ko.unwrap(config.basemap) === layer.name) { self.activeBasemap(layer); }
                    } else if (layer.addtomap) {
                        self.activeBasemap(layer);
                    }
                }
            }
            else if (!params.overlaysObservable) {
                if (layer.searchonly && !params.search) return;
                layer.opacity = ko.observable(layer.addtomap ? 100 : 0);
                layer.onMap = ko.computed({
                    read: function() { return layer.opacity() > 0; },
                    write: function(value) {
                        layer.opacity(value ? 100 : 0);
                    }
                });

                self.overlays.push(layer);
            }
        });

        for (var overlay of self.overlays()) {
            if (self.overlayConfigs.indexOf(overlay.maplayerid) > -1) {
                overlay.opacity(100);  // exact value unneccesary but keeps it semantic
            }

            overlay.onMap.subscribe(function(onMap) {
                if (onMap && self.overlayConfigs.indexOf(overlay.maplayerid) === -1) {
                    self.overlayConfigs.push(overlay.maplayerid)
                }

                if (!onMap && self.overlayConfigs.indexOf(overlay.maplayerid) > -1) {
                    self.overlayConfigs.remove(overlay.maplayerid);
                }
            });
        }

        _.each(sources, function(sourceConfig) {
            if (sourceConfig.tiles) {
                sourceConfig.tiles.forEach(function(url, i) {
                    if (url.startsWith('/')) {
                        sourceConfig.tiles[i] = window.location.origin + url;
                    }
                });
            }
            if (sourceConfig.data && typeof sourceConfig.data === 'string' && sourceConfig.data.startsWith('/')) {
                sourceConfig.data = arches.urls.root + sourceConfig.data.substr(1);
            }
        });

        var multiplyStopValues = function(stops, multiplier) {
            _.each(stops, function(stop) {
                if (Array.isArray(stop[1])) {
                    multiplyStopValues(stop[1], multiplier);
                } else {
                    stop[1] = stop[1] * multiplier;
                }
            });
        };

        var updateOpacity = function(layer, val) {
            var opacityVal = Number(val) / 100.0;
            layer = JSON.parse(JSON.stringify(layer));
            if (layer.paint === undefined) {
                layer.paint = {};
            }
            _.each([
                'background',
                'fill',
                'line',
                'text',
                'icon',
                'raster',
                'circle',
                'fill-extrusion',
                'heatmap'
            ], function(opacityType) {
                var startVal = layer.paint ? layer.paint[opacityType + '-opacity'] : null;

                if (startVal) {
                    if (parseFloat(startVal)) {
                        layer.paint[opacityType + '-opacity'].base = startVal * opacityVal;
                    } else {
                        layer.paint[opacityType + '-opacity'] = JSON.parse(JSON.stringify(startVal));
                        if (startVal.base) {
                            layer.paint[opacityType + '-opacity'].base = startVal.base * opacityVal;
                        }
                        if (startVal.stops) {
                            multiplyStopValues(layer.paint[opacityType + '-opacity'].stops, opacityVal);
                        }
                    }
                } else if (layer.type === opacityType ||
                     (layer.type === 'symbol' && (opacityType === 'text' || opacityType === 'icon'))) {
                    layer.paint[opacityType + '-opacity'] = opacityVal;
                }
            }, self);
            return layer;
        };

        this.additionalLayers = params.layers;
        this.layers = ko.pureComputed(function() {
            var layers = [];
            self.overlays().forEach(function(layer) {
                if (layer.onMap()) {
                    var opacity = layer.opacity();
                    layers = layer.layer_definitions.map(function(layer) {
                        return updateOpacity(layer, opacity);
                    }).concat(layers);
                }
            });
            if (self.activeBasemap()) {
                layers = self.activeBasemap().layer_definitions.slice(0).concat(layers);
            }
            if (this.additionalLayers) {
                layers = layers.concat(ko.unwrap(this.additionalLayers));
            }
            return layers;
        }, this);

        this.mapOptions = {
            style: {
                version: 8,
                sources: sources,
                sprite: arches.mapboxSprites,
                glyphs: arches.mapboxGlyphs,
                layers: self.layers(),
                center: [
                    parseFloat(ko.unwrap(x)),
                    parseFloat(ko.unwrap(y))
                ],
                zoom: parseFloat(ko.unwrap(zoom)),
            },
            maxZoom: maxZoom,
            minZoom: minZoom,
        };
        if (!params.usePosition) {
            this.mapOptions.bounds = bounds;
            this.mapOptions.fitBoundsOptions = params.fitBoundsOptions;
        }

        this.toggleTab = function(tabName) {
            if (self.activeTab() === tabName) {
                self.activeTab(null);
            } else {
                self.activeTab(tabName);
            }
        };

        this.updateLayers = function(layers) {
            var map = self.map();
            var style = map.getStyle();
            if (style) {
                style.layers = layers;
                map.setStyle(style);
            }
        };

        this.isFeatureClickable = function(feature) {
            return feature.properties.resourceinstanceid;
        };

        this.expandSidePanel = function() {
            return false;
        };

        this.resourceLookup = {};
        this.getPopupData = function(feature) {
            var data = feature.properties;
            var id = data.resourceinstanceid;
            data.showEditButton = false;
            if (id) {
                if (!self.resourceLookup[id]){
                    data = _.defaults(data, {
                        'loading': true,
                        'displayname': '',
                        'graph_name': '',
                        'map_popup': ''
                    });
                    if (data.permissions) {
                        try {
                            data.permissions = JSON.parse(ko.unwrap(data.permissions));
                        } catch (err) {
                            data.permissions = koMapping.toJS(ko.unwrap(data.permissions));
                        }
                        if (data.permissions.users_without_edit_perm.indexOf(ko.unwrap(self.userid)) === -1) {
                            data.showEditButton = true;
                        }
                    }
                    data = ko.mapping.fromJS(data);
                    data.reportURL = arches.urls.resource_report;
                    data.editURL = arches.urls.resource_editor;
                    self.resourceLookup[id] = data;
                    $.get(arches.urls.resource_descriptors + id, function(data) {
                        data.loading = false;
                        ko.mapping.fromJS(data, self.resourceLookup[id]);
                    });
                }
                self.resourceLookup[id].feature = feature;
                self.resourceLookup[id].mapCard = self;
                return self.resourceLookup[id];
            } else {
                data.resourceinstanceid = ko.observable(false);
                data.loading = ko.observable(false);
                data.feature = feature;
                data.mapCard = self;
                return data;
            }
        };

        this.onFeatureClick = function(feature, lngLat) {
            var map = self.map();
            self.popup = new mapboxgl.Popup()
                .setLngLat(lngLat)
                .setHTML(self.popupTemplate)
                .addTo(map);
            ko.applyBindingsToDescendants(
                self.getPopupData(feature),
                self.popup._content
            );
            if (map.getStyle() && feature.id) map.setFeatureState(feature, { selected: true });
            self.popup.on('close', function() {
                if (map.getStyle() && feature.id) map.setFeatureState(feature, { selected: false });
                self.popup = undefined;
            });
        };

        this.setupMap = function(map) {
            map.on('load', function() {
                map.addControl(new mapboxgl.NavigationControl(), 'top-left');
                map.addControl(new mapboxgl.FullscreenControl({
                    container: $(map.getContainer()).closest('.workbench-card-wrapper')[0]
                }), 'top-left');
                map.addControl(new MapboxGeocoder({
                    accessToken: mapboxgl.accessToken,
                    mapboxgl: mapboxgl,
                    placeholder: arches.geocoderPlaceHolder,
                    bbox: arches.hexBinBounds
                }), 'top-right');

                self.layers.subscribe(self.updateLayers);

                var hoverFeature;
                map.on('mousemove', function(e) {
                    var style = map.getStyle();
                    if (hoverFeature && hoverFeature.id && style) map.setFeatureState(hoverFeature, { hover: false });
                    hoverFeature = _.find(
                        map.queryRenderedFeatures(e.point),
                        self.isFeatureClickable
                    );
                    if (hoverFeature && hoverFeature.id && style) map.setFeatureState(hoverFeature, { hover: true });
                    map.getCanvas().style.cursor = hoverFeature ? 'pointer' : '';
                });

                map.on('click', function(e) {
                    if (hoverFeature) {
                        self.onFeatureClick(hoverFeature, e.lngLat);
                    }
                });

                self.map(map);
                if (params.fitBounds){
                    var padding = 40;
                    var activeTab = self.activeTab();
                    var options = {
                        padding: {
                            top: padding,
                            left: padding + (activeTab ? 200: 0),
                            bottom: padding,
                            right: padding + (activeTab ? 200: 0)
                        },
                        animate: false
                    };
                    var bounds = params.fitBounds();
                    if (bounds) {
                        map.fitBounds(bounds, options);
                    } else {
                        var fitBounds = params.fitBounds.subscribe(function(bounds) {
                            map.fitBounds(bounds, options);
                            fitBounds.dispose();
                        });
                    }
                }
                setTimeout(function() { map.resize(); }, 1);

                if (ko.isObservable(zoom)) {
                    map.on('zoomend', function() {
                        zoom(map.getZoom());
                    });
                    zoom.subscribe(function(level) {
                        level = parseFloat(level);
                        if (level) map.setZoom(level);
                    });
                }

                if (ko.isObservable(x)) {
                    map.on('dragend', function() {
                        var center = map.getCenter();
                        x(center.lng);
                    });
                    x.subscribe(function(lng) {
                        var center = map.getCenter();
                        lng = parseFloat(lng);
                        if (lng) {
                            center.lng = lng;
                            map.setCenter(center);
                        }
                    });
                }

                if (ko.isObservable(y)) {
                    map.on('dragend', function() {
                        var center = map.getCenter();
                        y(center.lat);
                    });
                    y.subscribe(function(lat) {
                        var center = map.getCenter();
                        lat = parseFloat(lat);
                        if (lat) {
                            center.lat = lat;
                            map.setCenter(center);
                        }
                    });
                }
            });
        };
    };
    return viewModel;
});
<|endoftext|>


<commit_before>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2022 Dan "Ducky" Little
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import React from 'react';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { TableVirtuoso } from 'react-virtuoso';

import {unparse as writeCsv} from 'papaparse';
import Mark from 'markup-js';
import FileSaver from 'file-saver';

import {FORMAT_OPTIONS, matchFeatures} from '../../util';

import { getLayerFromPath } from '../../actions/mapSource';

import { getFlatResults, getQueryResults } from '../../selectors/query';
import { SERVICE_STEPS } from '../../reducers/query';

import FilterModal from './filter-modal';

/* Renders query results as a table that can be sorted, filtered,
 * and downloaded as a CSV file.
 *
 */
class Grid extends React.Component {

    constructor() {
        super();

        // configure the default state of the sort,
        //  null means in results-order.
        this.state = {
            sortBy: null,
            sortAs: 'string',
            sortAsc: true,
            minimized: false,
            filterModal: null,
        };

        this.nextSort = this.nextSort.bind(this);
    }

    nextSort(column) {
        // rotate to the next sort type
        if(this.state.sortBy === column.property) {
            // when sorted ascending, go to descending
            if(this.state.sortAsc) {
                this.setState({sortAsc: false});
            // if the sort is descending already, go
            //  back to a neutral state
            } else {
                this.setState({sortBy: null, sortAsc: true});
            }
        } else {
            const sort_as = column.sortAs ? column.sortAs : 'string';
            this.setState({sortBy: column.property, sortAsc: true, sortAs: sort_as} );
        }
    }

    getHeaderRow(results, headerConf) {
        return headerConf.map((columnDef, colIdx) => {
            let sortTool = false;
            let filterControl = false;
            let sortClasses = 'icon sort';

            if (columnDef.sortAs) {
                const sortTitle = this.props.t('filter-sort');
                if (this.state.sortBy === columnDef.property) {
                    sortClasses += this.state.sortAsc ? ' asc' : ' desc';
                }
                sortTool = ( { this.nextSort(columnDef); }} className={sortClasses}>);
            }

            if (columnDef.filter) {
                // TODO: Get a translation friendly title attribute
                filterControl = (
                     {
                            this.setState({
                                filterModal: columnDef,
                            });
                        }}
                        className='filter icon'
                    />
                );
            }

            return (
                <th key={`col${colIdx}`} >{columnDef.title} {sortTool}{' '}
                    {filterControl}

            );
        });
    }

    sortResults(results) {
        const collator = Intl.Collator();
        const sorted_results = [].concat(results);

        const sort_col = this.state.sortBy;
        const sort_asc = this.state.sortAsc;
        const sort_as = this.state.sortAs;

        sorted_results.sort((a, b) => {
            let value_a = a.properties[sort_col];
            let value_b = b.properties[sort_col];
            if (sort_as === 'number') {
                value_a = parseFloat(value_a);
                value_b = parseFloat(value_b);
                if (sort_asc) {
                    return value_a < value_b ? -1 : 1;
                }
                return value_b < value_a ? -1 : 1;
            }
            if (sort_asc) {
                return collator.compare(value_a, value_b);
            }
            return collator.compare(value_b, value_a);
        });
        return sorted_results;
    }

    getRow(result, rowTemplate) {
        const html = Mark.up(rowTemplate, result, FORMAT_OPTIONS);
        // WARNING! If odd grid errors show up with row rendering,
        //          start here!
        // This code is potentially fragile but keeps the table
        //  templates backwards compatible.
        const strippedHTML = html.substring(html.indexOf('<td'), html.lastIndexOf('	') + 5);
        // get the contents of the "" cells
        const tmpElement = document.createElement('tr');
        tmpElement.innerHTML = strippedHTML;
        const cells = tmpElement.getElementsByTagName('td');
        const elements = [];

        for (let i = 0, ii = cells.length; i < ii; i++) {
            elements.push(
                <td key={`cell-${i}`} dangerouslySetInnerHTML={{__html: cells[i].innerHTML}} />
            );
        }
        return elements;
    }

    resultsAsCSV(gridCols, features) {
        const attributes = [];
        const feature_data = [];

        // get the export columns
        for(const column of gridCols) {
            if(column.property) {
                attributes.push(column.property);
            }
        }

        // add the 'header' row with the attribute names
        feature_data.push(attributes);

        // for each feature, create a row.
        for(const feature of features) {
            const row = [];
            for(const attr of attributes) {
                row.push(feature.properties[attr]);
            }
            feature_data.push(row);
        }

        // create the data
        const csv_blob = new Blob([writeCsv(feature_data)], {type: 'text/csv;charset=utf-8'});

        // create a unique string
        const uniq = '' + (new Date()).getTime();
        const csv_name = 'download_' + uniq + '.csv';

        // now have FileSaver 'normalize' how to do a save-as.
        FileSaver.saveAs(csv_blob, csv_name);
    }

    render() {
        const query = this.props.query;

        let features = [];
        let display_table = false;

        let grid_cols, grid_row;

        if (query.step === SERVICE_STEPS.RESULTS) {
            const service = this.props.services[query.serviceName];
            const serviceName = service.alias || service.name;
            const paths = Object.keys(query.results);
            paths.forEach(layerPath => {
                let layer = null;
                try {
                    layer = getLayerFromPath(this.props.mapSources, layerPath);
                } catch(err) {
                    // no layer, no problem.
                }

                if(layer !== null) {
                    const columnTemplate = layer.templates[serviceName + '-grid-columns']
                        || layer.templates.gridColumns;

                    // try to parse the grid columns
                    if (columnTemplate && typeof columnTemplate.contents === 'object') {
                        grid_cols = columnTemplate.contents;
                    } else {
                        try {
                            grid_cols = JSON.parse(columnTemplate.contents);
                        } catch(err) {
                            // swallow the error
                        }
                    }

                    const rowTemplate = layer.templates[serviceName + '-grid-row']
                        || layer.templates.gridRow;

                    if (rowTemplate) {
                        grid_row = rowTemplate.contents;
                    }

                    if(grid_cols && grid_row) {
                        // render as a grid.
                        features = matchFeatures(query.results[layerPath], query.filter);
                        if(query.results[layerPath].length > 0) {
                            display_table = true;
                        }
                    } else {
                        // console.error(layerPath + ' does not have gridColumns or gridRow templates.');
                    }
                }
            });
        }

        // render the empty string if there is nothing to show.
        if(!display_table) {
            return false;
        }

        if (this.state.sortBy !== null ) {
            features = this.sortResults(features);
        }

        // when minimized, show the maximize button.
        const min_btn_class = this.state.minimized ? 'maximize' : 'minimize';
        const grid_class = this.state.minimized ? 'hide' : '';
        const toggle_grid = () => {
            this.setState({minimized: !this.state.minimized});
        };

        return (

                {this.state.filterModal && (
                    <FilterModal
                        store={this.props.store}
                        column={this.state.filterModal}
                        filters={this.props.filters}
                        results={this.props.allResults}
                        onClose={() => this.setState({filterModal: null})}
                    />
                )}

                     { this.resultsAsCSV(grid_cols, features) } }
                        className={'tool download'}
                        title={ this.props.t('grid-download-csv')}
                    >

                <TableVirtuoso
                    style={{transition: 'height 500ms', height: this.state.minimized ? 0 : 400}}
                    data={features}
                    fixedHeaderContent={(index, feature) => {
                        return (

{this.getHeaderRow(features, grid_cols)}

                        );
                    }}
                    itemContent={(index, feature) => (
                        this.getRow(feature, grid_row, grid_cols)
                    )}
                />

        );
    }
}

const mapStateToProps = state => ({
    query: state.query,
    results: getQueryResults(state),
    allResults: getFlatResults(state),
    mapSources: state.mapSources,
    filters: state.query.filter,
});

export default connect(mapStateToProps)(withTranslation()(Grid));
<commit_msg>Fix sorting for null/undefined numbers #740<commit_after>/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2022 Dan "Ducky" Little
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import React from 'react';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { TableVirtuoso } from 'react-virtuoso';

import {unparse as writeCsv} from 'papaparse';
import Mark from 'markup-js';
import FileSaver from 'file-saver';

import {FORMAT_OPTIONS, matchFeatures} from '../../util';

import { getLayerFromPath } from '../../actions/mapSource';

import { getFlatResults, getQueryResults } from '../../selectors/query';
import { SERVICE_STEPS } from '../../reducers/query';

import FilterModal from './filter-modal';

/* Renders query results as a table that can be sorted, filtered,
 * and downloaded as a CSV file.
 *
 */
class Grid extends React.Component {

    constructor() {
        super();

        // configure the default state of the sort,
        //  null means in results-order.
        this.state = {
            sortBy: null,
            sortAs: 'string',
            sortAsc: true,
            minimized: false,
            filterModal: null,
        };

        this.nextSort = this.nextSort.bind(this);
    }

    nextSort(column) {
        // rotate to the next sort type
        if(this.state.sortBy === column.property) {
            // when sorted ascending, go to descending
            if(this.state.sortAsc) {
                this.setState({sortAsc: false});
            // if the sort is descending already, go
            //  back to a neutral state
            } else {
                this.setState({sortBy: null, sortAsc: true});
            }
        } else {
            const sort_as = column.sortAs ? column.sortAs : 'string';
            this.setState({sortBy: column.property, sortAsc: true, sortAs: sort_as} );
        }
    }

    getHeaderRow(results, headerConf) {
        return headerConf.map((columnDef, colIdx) => {
            let sortTool = false;
            let filterControl = false;
            let sortClasses = 'icon sort';

            if (columnDef.sortAs) {
                const sortTitle = this.props.t('filter-sort');
                if (this.state.sortBy === columnDef.property) {
                    sortClasses += this.state.sortAsc ? ' asc' : ' desc';
                }
                sortTool = ( { this.nextSort(columnDef); }} className={sortClasses}>);
            }

            if (columnDef.filter) {
                // TODO: Get a translation friendly title attribute
                filterControl = (
                     {
                            this.setState({
                                filterModal: columnDef,
                            });
                        }}
                        className='filter icon'
                    />
                );
            }

            return (
                <th key={`col${colIdx}`} >{columnDef.title} {sortTool}{' '}
                    {filterControl}

            );
        });
    }

    sortResults(results) {
        const collator = Intl.Collator();
        const sorted_results = [].concat(results);

        const sort_col = this.state.sortBy;
        const sort_asc = this.state.sortAsc;
        const sort_as = this.state.sortAs;

        sorted_results.sort((a, b) => {
            let value_a = a.properties[sort_col];
            let value_b = b.properties[sort_col];
            if (sort_as === 'number') {
                value_a = parseFloat(value_a);
                value_b = parseFloat(value_b);

                // assume value a is less than value b
                let sort_v = -1;
                if (isNaN(value_b) || value_b === null) {
                    sort_v = 1;
                } else if (value_b < value_a) {
                    sort_v = 1;
                }
                if (!sort_asc) {
                    sort_v = sort_v * -1;
                }
                return sort_v;
            }
            if (sort_asc) {
                return collator.compare(value_a, value_b);
            }
            return collator.compare(value_b, value_a);
        });
        return sorted_results;
    }

    getRow(result, rowTemplate) {
        const html = Mark.up(rowTemplate, result, FORMAT_OPTIONS);
        // WARNING! If odd grid errors show up with row rendering,
        //          start here!
        // This code is potentially fragile but keeps the table
        //  templates backwards compatible.
        const strippedHTML = html.substring(html.indexOf('<td'), html.lastIndexOf('	') + 5);
        // get the contents of the "" cells
        const tmpElement = document.createElement('tr');
        tmpElement.innerHTML = strippedHTML;
        const cells = tmpElement.getElementsByTagName('td');
        const elements = [];

        for (let i = 0, ii = cells.length; i < ii; i++) {
            elements.push(
                <td key={`cell-${i}`} dangerouslySetInnerHTML={{__html: cells[i].innerHTML}} />
            );
        }
        return elements;
    }

    resultsAsCSV(gridCols, features) {
        const attributes = [];
        const feature_data = [];

        // get the export columns
        for(const column of gridCols) {
            if(column.property) {
                attributes.push(column.property);
            }
        }

        // add the 'header' row with the attribute names
        feature_data.push(attributes);

        // for each feature, create a row.
        for(const feature of features) {
            const row = [];
            for(const attr of attributes) {
                row.push(feature.properties[attr]);
            }
            feature_data.push(row);
        }

        // create the data
        const csv_blob = new Blob([writeCsv(feature_data)], {type: 'text/csv;charset=utf-8'});

        // create a unique string
        const uniq = '' + (new Date()).getTime();
        const csv_name = 'download_' + uniq + '.csv';

        // now have FileSaver 'normalize' how to do a save-as.
        FileSaver.saveAs(csv_blob, csv_name);
    }

    render() {
        const query = this.props.query;

        let features = [];
        let display_table = false;

        let grid_cols, grid_row;

        if (query.step === SERVICE_STEPS.RESULTS) {
            const service = this.props.services[query.serviceName];
            const serviceName = service.alias || service.name;
            const paths = Object.keys(query.results);
            paths.forEach(layerPath => {
                let layer = null;
                try {
                    layer = getLayerFromPath(this.props.mapSources, layerPath);
                } catch(err) {
                    // no layer, no problem.
                }

                if(layer !== null) {
                    const columnTemplate = layer.templates[serviceName + '-grid-columns']
                        || layer.templates.gridColumns;

                    // try to parse the grid columns
                    if (columnTemplate && typeof columnTemplate.contents === 'object') {
                        grid_cols = columnTemplate.contents;
                    } else {
                        try {
                            grid_cols = JSON.parse(columnTemplate.contents);
                        } catch(err) {
                            // swallow the error
                        }
                    }

                    const rowTemplate = layer.templates[serviceName + '-grid-row']
                        || layer.templates.gridRow;

                    if (rowTemplate) {
                        grid_row = rowTemplate.contents;
                    }

                    if(grid_cols && grid_row) {
                        // render as a grid.
                        features = matchFeatures(query.results[layerPath], query.filter);
                        if(query.results[layerPath].length > 0) {
                            display_table = true;
                        }
                    } else {
                        // console.error(layerPath + ' does not have gridColumns or gridRow templates.');
                    }
                }
            });
        }

        // render the empty string if there is nothing to show.
        if(!display_table) {
            return false;
        }

        if (this.state.sortBy !== null ) {
            features = this.sortResults(features);
        }

        // when minimized, show the maximize button.
        const min_btn_class = this.state.minimized ? 'maximize' : 'minimize';
        const grid_class = this.state.minimized ? 'hide' : '';
        const toggle_grid = () => {
            this.setState({minimized: !this.state.minimized});
        };

        return (

                {this.state.filterModal && (
                    <FilterModal
                        store={this.props.store}
                        column={this.state.filterModal}
                        filters={this.props.filters}
                        results={this.props.allResults}
                        onClose={() => this.setState({filterModal: null})}
                    />
                )}

                     { this.resultsAsCSV(grid_cols, features) } }
                        className={'tool download'}
                        title={ this.props.t('grid-download-csv')}
                    >

                <TableVirtuoso
                    style={{transition: 'height 500ms', height: this.state.minimized ? 0 : 400}}
                    data={features}
                    fixedHeaderContent={(index, feature) => {
                        return (

{this.getHeaderRow(features, grid_cols)}

                        );
                    }}
                    itemContent={(index, feature) => (
                        this.getRow(feature, grid_row, grid_cols)
                    )}
                />

        );
    }
}

const mapStateToProps = state => ({
    query: state.query,
    results: getQueryResults(state),
    allResults: getFlatResults(state),
    mapSources: state.mapSources,
    filters: state.query.filter,
});

export default connect(mapStateToProps)(withTranslation()(Grid));
<|endoftext|>


<commit_before>define([], function (
) {
	'use strict';

	/**
	 * Library of particles

	 * @@example-link http://code.gooengine.com/latest/visual-test/goo/misc/ParticleLib/ParticleLib-vtest.html Working example
	 */
	var ParticleLib = {};

	/**
	 * Returns a set of parameters for a smoke particle emitter
	 */
	ParticleLib.getSmoke = function (options) {
		options = options || {};
		options.scale = typeof options.scale !== 'undefined' ? options.scale : 1;
		options.spread = typeof options.spread !== 'undefined' ? options.spread : 2;
		options.rate = typeof options.spread !== 'undefined' ? options.rate : 25;
		options.velocity = typeof options.velocity !== 'undefined' ? options.velocity :
			function (particle/*, particleEntity*/) {
				// not nice, will end up a square
				var vec3 = particle.velocity;
				vec3.data[0] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				vec3.data[1] = (Math.random() + 4.0) * 2 * options.scale;
				vec3.data[2] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				return vec3;
			};
		options.color = options.color || [0, 0, 0];

		return {
			totalParticlesToSpawn: -1,
			releaseRatePerSecond: options.rate,
			minLifetime: 0.5,
			maxLifetime: 4.0,
			getEmissionVelocity: options.velocity,
			timeline: [{
				timeOffset: 0.0,
				spin: 0,
				mass: 1,
				size: 3.0 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 1]
			}, {
				timeOffset: 1.0,
				size: 6.0 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 0]
			}]
		};
	};

	/**
	 * Returns a set of parameters for a fire particle emitter
	 */
	ParticleLib.getFire = function (options) {
		options = options || {};
		options.scale = typeof options.scale !== 'undefined' ? options.scale : 1;
		options.spread = typeof options.spread !== 'undefined' ? options.spread : 2;
		options.velocity = typeof options.velocity !== 'undefined' ? options.velocity : 10;
		options.startColor = options.startColor || [1, 1, 0];
		options.endColor = options.endColor || [1, 0, 0];

		return {
			totalParticlesToSpawn: -1,
			releaseRatePerSecond: 30,
			minLifetime: 0.5,
			maxLifetime: 2.0,
			getEmissionVelocity: function (particle/*, particleEntity*/) {
				// not nice, will end up a square
				var vec3 = particle.velocity;
				vec3.data[0] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				vec3.data[1] = (Math.random() + 1.0) * options.velocity * options.scale;
				vec3.data[2] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				return vec3;
			},
			timeline: [{
				timeOffset : 0.0,
				spin: 0,
				mass: 1,
				size: 2.0 * options.scale,
				color: [options.startColor[0], options.startColor[1], options.startColor[2], 0]
			}, {
				timeOffset: 0.05,
				color: [options.startColor[0], options.startColor[1], options.startColor[2], 1.0]
			}, {
				timeOffset: 0.45,
				color: [options.endColor[0], options.endColor[1], options.endColor[2], 0.8]
			}, {
				timeOffset: 0.5,
				size: 3.0 * options.scale,
				color: [0, 0, 0, 0]
			}]
		};
	};

	/**
	 * Returns a set of parameters for a snow particle emitter
	 */
	ParticleLib.getSnow = function (options) {
		options = options || {};
		options.scale = typeof options.scale !== 'undefined' ? options.scale : 2;
		options.spread = typeof options.spread !== 'undefined' ? options.spread : 50;
		options.velocity = typeof options.velocity !== 'undefined' ? options.velocity : 3;
		options.color = options.color || [1, 1, 1];

		return {
			particleCount: 1000,
			totalParticlesToSpawn: -1,
			releaseRatePerSecond: 50,
			minLifetime: 15.0,
			maxLifetime: 25.0,
			getEmissionPoint: function (particle/*, particleEntity*/) {
				var vec3 = particle.position;
				options.getEmissionPoint(vec3);
				return vec3;
			},
			getEmissionVelocity: function (particle/*, particleEntity*/) {
				var vec3 = particle.velocity;
				options.getEmissionVelocity(vec3);
				return vec3;
			},
			timeline: [{
				timeOffset : 0.0,
				spin: 0,
				mass: 1,
				size: 1.0 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 0.0]
			}, {
				timeOffset: 0.05,
				color: [options.color[0], options.color[1], options.color[2], 1.0]
			}, {
				timeOffset: 0.70,
				color: [options.color[0], options.color[1], options.color[2], 0.8]
			}, {
				timeOffset: 0.25,
				spin: 5,
				size: 0.5 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 0]
			}]
		};
	};

	return ParticleLib;
});<commit_msg>Include ParticleLib in docs by defining it as function.<commit_after>define([], function (
) {
	'use strict';

	/**
	 * Library of particles

	 * @example-link http://code.gooengine.com/latest/visual-test/goo/misc/ParticleLib/ParticleLib-vtest.html Working example
	 */
	function ParticleLib() {};

	/**
	 * Returns a set of parameters for a smoke particle emitter
	 */
	ParticleLib.getSmoke = function (options) {
		options = options || {};
		options.scale = typeof options.scale !== 'undefined' ? options.scale : 1;
		options.spread = typeof options.spread !== 'undefined' ? options.spread : 2;
		options.rate = typeof options.spread !== 'undefined' ? options.rate : 25;
		options.velocity = typeof options.velocity !== 'undefined' ? options.velocity :
			function (particle/*, particleEntity*/) {
				// not nice, will end up a square
				var vec3 = particle.velocity;
				vec3.data[0] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				vec3.data[1] = (Math.random() + 4.0) * 2 * options.scale;
				vec3.data[2] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				return vec3;
			};
		options.color = options.color || [0, 0, 0];

		return {
			totalParticlesToSpawn: -1,
			releaseRatePerSecond: options.rate,
			minLifetime: 0.5,
			maxLifetime: 4.0,
			getEmissionVelocity: options.velocity,
			timeline: [{
				timeOffset: 0.0,
				spin: 0,
				mass: 1,
				size: 3.0 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 1]
			}, {
				timeOffset: 1.0,
				size: 6.0 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 0]
			}]
		};
	};

	/**
	 * Returns a set of parameters for a fire particle emitter
	 */
	ParticleLib.getFire = function (options) {
		options = options || {};
		options.scale = typeof options.scale !== 'undefined' ? options.scale : 1;
		options.spread = typeof options.spread !== 'undefined' ? options.spread : 2;
		options.velocity = typeof options.velocity !== 'undefined' ? options.velocity : 10;
		options.startColor = options.startColor || [1, 1, 0];
		options.endColor = options.endColor || [1, 0, 0];

		return {
			totalParticlesToSpawn: -1,
			releaseRatePerSecond: 30,
			minLifetime: 0.5,
			maxLifetime: 2.0,
			getEmissionVelocity: function (particle/*, particleEntity*/) {
				// not nice, will end up a square
				var vec3 = particle.velocity;
				vec3.data[0] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				vec3.data[1] = (Math.random() + 1.0) * options.velocity * options.scale;
				vec3.data[2] = (Math.random() - 0.5) * 2 * options.spread * options.scale;
				return vec3;
			},
			timeline: [{
				timeOffset : 0.0,
				spin: 0,
				mass: 1,
				size: 2.0 * options.scale,
				color: [options.startColor[0], options.startColor[1], options.startColor[2], 0]
			}, {
				timeOffset: 0.05,
				color: [options.startColor[0], options.startColor[1], options.startColor[2], 1.0]
			}, {
				timeOffset: 0.45,
				color: [options.endColor[0], options.endColor[1], options.endColor[2], 0.8]
			}, {
				timeOffset: 0.5,
				size: 3.0 * options.scale,
				color: [0, 0, 0, 0]
			}]
		};
	};

	/**
	 * Returns a set of parameters for a snow particle emitter
	 */
	ParticleLib.getSnow = function (options) {
		options = options || {};
		options.scale = typeof options.scale !== 'undefined' ? options.scale : 2;
		options.spread = typeof options.spread !== 'undefined' ? options.spread : 50;
		options.velocity = typeof options.velocity !== 'undefined' ? options.velocity : 3;
		options.color = options.color || [1, 1, 1];

		return {
			particleCount: 1000,
			totalParticlesToSpawn: -1,
			releaseRatePerSecond: 50,
			minLifetime: 15.0,
			maxLifetime: 25.0,
			getEmissionPoint: function (particle/*, particleEntity*/) {
				var vec3 = particle.position;
				options.getEmissionPoint(vec3);
				return vec3;
			},
			getEmissionVelocity: function (particle/*, particleEntity*/) {
				var vec3 = particle.velocity;
				options.getEmissionVelocity(vec3);
				return vec3;
			},
			timeline: [{
				timeOffset : 0.0,
				spin: 0,
				mass: 1,
				size: 1.0 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 0.0]
			}, {
				timeOffset: 0.05,
				color: [options.color[0], options.color[1], options.color[2], 1.0]
			}, {
				timeOffset: 0.70,
				color: [options.color[0], options.color[1], options.color[2], 0.8]
			}, {
				timeOffset: 0.25,
				spin: 5,
				size: 0.5 * options.scale,
				color: [options.color[0], options.color[1], options.color[2], 0]
			}]
		};
	};

	return ParticleLib;
});<|endoftext|>


<commit_before>const extension = {
  options: {
    wrapNavigation: false,
    autoSelectFirst: true,
    nextKey: 'down, j',
    previousKey: 'up, k',
    navigatePreviousResultPage: 'left, h',
    navigateNextResultPage: 'right, l',
    navigateKey: 'return, space',
    navigateNewTabKey: 'ctrl+return, command+return, ctrl+space',
    navigateSearchTab: 'a, s',
    navigateImagesTab: 'i',
    navigateVideosTab: 'v',
    navigateMapsTab: 'm',
    navigateNewsTab: 'n',
    focusSearchInput: '/, escape'
  },

  lastNavigation: {
    lastQueryUrl: false,
    lastFocusedIndex: 0
  },

  init: function() {
    if (!/^(www|encrypted)\.google\./.test(window.location.hostname)) {
      return;
    }

    const params = getQueryStringParams();
    let optionsTask = this.loadOptions();

    // Don't initialize results navigation on image search, since it doesn't work
    // there.
    if (params['tbm'] !== 'isch') {
      // This file is loaded only after the DOM is ready, so no need to wait for
      // DOMContentLoaded.
      let afterOptions = () => {
        this.initResultsNavigation();
      };
      optionsTask.then(afterOptions, afterOptions);
    }

    optionsTask.then(
      () => this.initCommonGoogleSearchNavigation(),
      () => this.initCommonGoogleSearchNavigation()
    );
  },

  initResultsNavigation: function() {
    let options = this.options;
    let lastNavigation = this.lastNavigation;
    let results = getGoogleSearchLinks();
    let isFirstNavigation = true;

    if (options.autoSelectFirst) {
      // Highlight the first result when the page is loaded.
      results.focus(0);
    }
    loadLastNavigation().then(() => {
      if (location.href === lastNavigation.lastQueryUrl) {
        isFirstNavigation = false;
        results.focus(lastNavigation.lastFocusedIndex);
      }
    });
    this.register(options.nextKey, () => {
      if (!options.autoSelectFirst && isFirstNavigation) {
        results.focus(0);
        isFirstNavigation = false;
      }
      else {
        results.focusNext(options.wrapNavigation);
      }
    });
    this.register(options.previousKey, () => {
      if (!options.autoSelectFirst && isFirstNavigation) {
        results.focus(0);
        isFirstNavigation = false;
      }
      else {
        results.focusPrevious(options.wrapNavigation);
      }
    });
    this.register(options.navigateKey, () => {
      let link = results[results.focusedIndex];
      saveLastNavigation(results.focusedIndex);
      link.click();
    });
    this.register(options.navigateNewTabKey, () => {
      let link = results[results.focusedIndex];
      window.open(link.href);
    });
  },

  initCommonGoogleSearchNavigation: function() {
    let searchInput = document.getElementById('lst-ib');
    let options = this.options;

    this.register(options.focusSearchInput, () => {
      searchInput.focus();
      searchInput.select();
    });
    let all = getElementByXpath(
      '//a[contains(@class, \'q qs\') and not (contains(@href, \'&tbm=\')) and not (contains(@href, \'maps.google.\'))]');
    this.register(options.navigateSearchTab, () => {
      updateUrlWithNodeHref(all);
    });
    let images = getElementByXpath(
      '//a[contains(@class, \'q qs\') and (contains(@href, \'&tbm=isch\'))]');
    this.register(options.navigateImagesTab, () => {
      updateUrlWithNodeHref(images);
    });
    let videos = getElementByXpath(
      '//a[contains(@class, \'q qs\') and (contains(@href, \'&tbm=vid\'))]');
    this.register(options.navigateVideosTab, () => {
      updateUrlWithNodeHref(videos);
    });
    let maps = getElementByXpath(
      '//a[contains(@class, \'q qs\') and (contains(@href, \'maps.google.\'))]');
    this.register(options.navigateMapsTab, () => {
      updateUrlWithNodeHref(maps);
    });
    let news = getElementByXpath(
      '//a[contains(@class, \'q qs\') and (contains(@href, \'&tbm=nws\'))]');
    this.register(options.navigateNewsTab, () => {
      updateUrlWithNodeHref(news);
    });
    let previousResultPage = document.querySelector('#pnprev');
    this.register(options.navigatePreviousResultPage, () => {
      updateUrlWithNodeHref(previousResultPage);
    });
    let nextResultPage = document.querySelector('#pnnext');
    this.register(options.navigateNextResultPage, () => {
      updateUrlWithNodeHref(nextResultPage);
    });
  },

  register: function(shortcut, callback) {
    key(shortcut, function(event) {
      callback();

      if (event !== null) {
        event.stopPropagation();
        event.preventDefault();
      }

      return false;
    });
  },

  loadOptions: function() {
    return new Promise((resolve, reject) => {
      chrome.storage.sync.get(
        this.options,
        (items) => {
          if (chrome.runtime.lastError) {
            reject();
          } else {
            this.options = items;
            resolve();
          }
        });
    });
  }
};

function SearchResults(nodes) {
  this.items = Array.prototype.slice.call(nodes);
  this.focusedIndex = 0;

  this.focus = function(index) {
    if (this.focusedIndex >= 0) {
      this.items[this.focusedIndex].classList.remove('highlighted-search-result');
    }
    let newItem = this.items[index];
    newItem.classList.add('highlighted-search-result');
    newItem.focus();
    this.focusedIndex = index;
  };

  this.focusNext = function(shouldWrap) {
    let nextIndex = 0;

    if (this.focusedIndex < this.items.length - 1) {
      nextIndex = this.focusedIndex + 1;
    }
    else if (!shouldWrap) {
      nextIndex = this.focusedIndex;
    }

    this.focus(nextIndex);
  };

  this.focusPrevious = function(shouldWrap) {
    let previousIndex = this.items.length - 1;

    if (this.focusedIndex > 0) {
      previousIndex = this.focusedIndex - 1;
    }
    else if (!shouldWrap) {
      previousIndex = this.focusedIndex;
    }

    this.focus(previousIndex);
  }
}

const loadLastNavigation = () => {
  return new Promise((resolve, reject) => {
    chrome.storage.local.get(
      extension.lastNavigation,
      (items) => {
        if (chrome.runtime.lastError) {
          reject();
        } else {
          extension.lastNavigation = items;
          resolve();
        }
      });
  });
};

const updateUrlWithNodeHref = (node) => {
  if (node !== null) {
    location.href = node.href;
  }
};

const getQueryStringParams = () => {
  const encodedQueryString = window.location.search.slice(1);
  const encodedParams = encodedQueryString.split('&');
  let params = {};
  for (const encodedParam of encodedParams) {
    let [key, encodedValue] = encodedParam.split('=');
    if (!encodedValue) {
      encodedValue = '';
    }
    // + (plus sign) is not decoded by decodeURIComponent so we need to decode
    // it manually.
    encodedValue = encodedValue.replace(/\+/g, ' ');
    params[key] = decodeURIComponent(encodedValue);
  }
  return params;
};

const getGoogleSearchLinks = function() {
  // the nodes are returned in the document order which is what we want
  return new SearchResults(document.querySelectorAll('h3.r a, #pnprev, #pnnext'));
};

function getElementByXpath(path) {
  return document
    .evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
    .singleNodeValue;
}

const saveLastNavigation = (visitedIndex) => {
  chrome.storage.local.set(
    {
      lastQueryUrl: location.href,
      lastFocusedIndex: visitedIndex
    },
    null);
};

extension.init();
<commit_msg>simplify the common shortcut registration.<commit_after>const extension = {
  options: {
    sync: {
      wrapNavigation: false,
      autoSelectFirst: true,
      nextKey: 'down, j',
      previousKey: 'up, k',
      navigatePreviousResultPage: 'left, h',
      navigateNextResultPage: 'right, l',
      navigateKey: 'return, space',
      navigateNewTabKey: 'ctrl+return, command+return, ctrl+space',
      navigateSearchTab: 'a, s',
      navigateImagesTab: 'i',
      navigateVideosTab: 'v',
      navigateMapsTab: 'm',
      navigateNewsTab: 'n',
      focusSearchInput: '/, escape'
    },

    load: function() {
      return new Promise((resolve) => {
        chrome.storage.sync.get(
          this.sync,
          (values) => {
            if (!chrome.runtime.lastError) {
              this.sync = values;
            }
            resolve();
          }
        );
      });
    }
  },

  lastNavigation: {
    lastQueryUrl: false,
    lastFocusedIndex: 0
  },

  init: function() {
    if (!/^(www|encrypted)\.google\./.test(window.location.hostname)) {
      return;
    }

    const params = getQueryStringParams();
    let loadOptions = this.options.load();

    // Don't initialize results navigation on image search, since it doesn't work
    // there.
    if (params['tbm'] !== 'isch') {
      // This file is loaded only after the DOM is ready, so no need to wait for
      // DOMContentLoaded.
      loadOptions.then(() => this.initResultsNavigation());
    }

    loadOptions.then(() => this.initCommonGoogleSearchNavigation());
  },

  initResultsNavigation: function() {
    let options = this.options.sync;
    let lastNavigation = this.lastNavigation;
    let results = getGoogleSearchLinks();
    let isFirstNavigation = true;

    if (options.autoSelectFirst) {
      // Highlight the first result when the page is loaded.
      results.focus(0);
    }
    loadLastNavigation().then(() => {
      if (location.href === lastNavigation.lastQueryUrl) {
        isFirstNavigation = false;
        results.focus(lastNavigation.lastFocusedIndex);
      }
    });
    this.register(options.nextKey, () => {
      if (!options.autoSelectFirst && isFirstNavigation) {
        results.focus(0);
        isFirstNavigation = false;
      }
      else {
        results.focusNext(options.wrapNavigation);
      }
    });
    this.register(options.previousKey, () => {
      if (!options.autoSelectFirst && isFirstNavigation) {
        results.focus(0);
        isFirstNavigation = false;
      }
      else {
        results.focusPrevious(options.wrapNavigation);
      }
    });
    this.register(options.navigateKey, () => {
      let link = results[results.focusedIndex];
      saveLastNavigation(results.focusedIndex);
      link.click();
    });
    this.register(options.navigateNewTabKey, () => {
      let link = results[results.focusedIndex];
      window.open(link.href);
    });
  },

  initCommonGoogleSearchNavigation: function() {
    let options = this.options.sync;

    this.register(options.focusSearchInput, () => {
      let searchInput = document.getElementById('lst-ib');
      searchInput.focus();
      searchInput.select();
    });

    let tabs = [
      [options.navigateSearchTab, '//a[contains(@class, \'q qs\') and not (contains(@href, \'&tbm=\')) and not (contains(@href, \'maps.google.\'))]'],
      [options.navigateImagesTab, '//a[contains(@class, \'q qs\') and (contains(@href, \'&tbm=isch\'))]'],
      [options.navigateVideosTab, '//a[contains(@class, \'q qs\') and (contains(@href, \'&tbm=vid\'))]'],
      [options.navigateMapsTab, '//a[contains(@class, \'q qs\') and (contains(@href, \'maps.google.\'))]'],
      [options.navigateNewsTab, '//a[contains(@class, \'q qs\') and (contains(@href, \'&tbm=nws\'))]'],
      [options.navigatePreviousResultPage, "//a[@id='pnprev']"],
      [options.navigateNextResultPage, "//a[@id='pnnext']"]
    ];

    for (let i = 0; i < tabs.length; i++) {
      let tabCommand = tabs[i];
      this.register(tabCommand[0], () => {
        let node = getElementByXpath(tabCommand[1]);

        if (node !== null) {
          location.href = node.href;
        }
      });
    }
  },

  register: function(shortcut, callback) {
    key(shortcut, function(event) {
      callback();

      if (event !== null) {
        event.stopPropagation();
        event.preventDefault();
      }

      return false;
    });
  }
};

function SearchResults(nodes) {
  this.items = Array.prototype.slice.call(nodes);
  this.focusedIndex = 0;

  this.focus = function(index) {
    if (this.focusedIndex >= 0) {
      this.items[this.focusedIndex].classList.remove('highlighted-search-result');
    }
    let newItem = this.items[index];
    newItem.classList.add('highlighted-search-result');
    newItem.focus();
    this.focusedIndex = index;
  };

  this.focusNext = function(shouldWrap) {
    let nextIndex = 0;

    if (this.focusedIndex < this.items.length - 1) {
      nextIndex = this.focusedIndex + 1;
    }
    else if (!shouldWrap) {
      nextIndex = this.focusedIndex;
    }

    this.focus(nextIndex);
  };

  this.focusPrevious = function(shouldWrap) {
    let previousIndex = this.items.length - 1;

    if (this.focusedIndex > 0) {
      previousIndex = this.focusedIndex - 1;
    }
    else if (!shouldWrap) {
      previousIndex = this.focusedIndex;
    }

    this.focus(previousIndex);
  }
}

const loadLastNavigation = () => {
  return new Promise((resolve, reject) => {
    chrome.storage.local.get(
      extension.lastNavigation,
      (items) => {
        if (chrome.runtime.lastError) {
          reject();
        } else {
          extension.lastNavigation = items;
          resolve();
        }
      });
  });
};

const getQueryStringParams = () => {
  const encodedQueryString = window.location.search.slice(1);
  const encodedParams = encodedQueryString.split('&');
  let params = {};
  for (const encodedParam of encodedParams) {
    let [key, encodedValue] = encodedParam.split('=');
    if (!encodedValue) {
      encodedValue = '';
    }
    // + (plus sign) is not decoded by decodeURIComponent so we need to decode
    // it manually.
    encodedValue = encodedValue.replace(/\+/g, ' ');
    params[key] = decodeURIComponent(encodedValue);
  }
  return params;
};

const getGoogleSearchLinks = function() {
  // the nodes are returned in the document order which is what we want
  return new SearchResults(document.querySelectorAll('h3.r a, #pnprev, #pnnext'));
};

function getElementByXpath(path) {
  return document
    .evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)
    .singleNodeValue;
}

const saveLastNavigation = (visitedIndex) => {
  chrome.storage.local.set(
    {
      lastQueryUrl: location.href,
      lastFocusedIndex: visitedIndex
    },
    null);
};

extension.init();
<|endoftext|>


<commit_before>$(document).ready(function() {

	// Sticky Illustrator Information

	var illustratorInfo = $('aside#illustrator-meta');

	var top = illustratorInfo.offset().top - parseFloat(illustratorInfo.css('marginTop').replace(/auto/, 0));
	  $(window).scroll(function (event) {
	    // what the y position of the scroll is
	    var y = $(this).scrollTop();

	    // whether that's below the form
	    if (y >= top) {
	      // if so, ad the fixed class
	      illustratorInfo.addClass('fixed');
	    } else {
	      // otherwise remove it
	      illustratorInfo.removeClass('fixed');
	    }
	  });

});
<commit_msg>added masonry<commit_after>$(document).ready(function() {

	// Sticky Illustrator Information

	var illustratorInfo = $('aside#illustrator-meta');

	var top = illustratorInfo.offset().top - parseFloat(illustratorInfo.css('marginTop').replace(/auto/, 0));

  $(window).scroll(function (event) {
    // what the y position of the scroll is
    var y = $(this).scrollTop();

    // whether that's below the form
    if (y >= top) {
      // if so, ad the fixed class
      illustratorInfo.addClass('fixed');
    } else {
      // otherwise remove it
      illustratorInfo.removeClass('fixed');
    }
  });

	// Masonry for Illustrator Pages

	var gallery = $('.gallery');
	var galleryItems = '.gallery-item';

	gallery.imagesLoaded( function(){
		gallery.masonry({
			isAnimated: true,
		  itemSelector: galleryItems,
		  // set columnWidth a fraction of the container width
		  columnWidth: function( containerWidth ) {
		    return containerWidth / 2;
		  }
		});
	});

	// Footer copyright show hide

});
<|endoftext|>


<commit_before>(function(Channel, go, listen, unique, pace, jsonp) {

    function searchWikipedia(term) {
        var url = 'http://en.wikipedia.org/w/api.php?action=opensearch&format=json&callback=?&search=' +
                encodeURIComponent(term);
        return jsonp(url);
    }

    go(function* () {
        var input = document.getElementById('searchtext'),
            results = document.getElementById('results'),
            keyup = listen(input, 'keyup'),
            pacedKeyup = pace(keyup, 300),
            evt, data, res, text, previousText,
            minLength,
            i, len,
            p;

        while (true) {
            evt = yield pacedKeyup.get();
            text = evt.target.value;
            minLength = text.length > 2;

            if (minLength && text !== previousText) {

                data = yield searchWikipedia(text).get();

                if (data.error) {
                    results.innerHTML = '<h1p>Error:
' + JSON.stringify(data.error);
                } else {
                    if (data[0] && data[1]) {
                        results.innerHTML = '';
                        res = data[1];
                        for (i = 0, len = res.length; i < len; i++) {
                            p = document.createElement('p');
                            p.innerHTML = res[i];
                            results.appendChild(p);
                        }
                    }
                }

            } else if (!minLength) {
                results.innerHTML = '';
            }

            previousText = text;
        }
    });

})($async.Channel, $async.go, $async.listen, $async.unique, $async.pace, $async.jsonp);
<commit_msg>adds prompt to reload page if for some reason our traceur-transpiled code isn't ready<commit_after>if (!$async) {
    // It seems that traceur *sometimes*, fails to properly insert our
    // code from async-es.js
    // When that happens, we simply ask the user to reload the page.
    alert('An unexpected error occurred. Please reload the page.');
}

(function(Channel, go, listen, unique, pace, jsonp) {

    function searchWikipedia(term) {
        var url = 'http://en.wikipedia.org/w/api.php?action=opensearch&format=json&callback=?&search=' +
                encodeURIComponent(term);
        return jsonp(url);
    }

    go(function* () {
        var input = document.getElementById('searchtext'),
            results = document.getElementById('results'),
            keyup = listen(input, 'keyup'),
            pacedKeyup = pace(keyup, 300),
            evt, data, res, text, previousText,
            minLength,
            i, len,
            p;

        while (true) {
            evt = yield pacedKeyup.get();
            text = evt.target.value;
            minLength = text.length > 2;

            if (minLength && text !== previousText) {

                data = yield searchWikipedia(text).get();

                if (data.error) {
                    results.innerHTML = '<h1p>Error:
' + JSON.stringify(data.error);
                } else {
                    if (data[0] && data[1]) {
                        results.innerHTML = '';
                        res = data[1];
                        for (i = 0, len = res.length; i < len; i++) {
                            p = document.createElement('p');
                            p.innerHTML = res[i];
                            results.appendChild(p);
                        }
                    }
                }

            } else if (!minLength) {
                results.innerHTML = '';
            }

            previousText = text;
        }
    });

})($async.Channel, $async.go, $async.listen, $async.unique, $async.pace, $async.jsonp);
<|endoftext|>


<commit_before>/**
 * Web Worker for the HCS algorithm to cluster a network.
 */
importScripts('../vendor/lodash.min.js');

/**
 * Calculate the minimum spanning tree using Kruskal's algorithm.
 *
 * @param nodes
 * @param edges
 * @returns forest The clusters.
 */
function kruskal(nodes, edges) {
    var nodeCount = nodes.length,
	    clusterCount = nodeCount,
	    i = edges.length,
	    clusterIndex = 0,
	    nodeMap = {},
	    clusterMap = {};

	while(i--) {
		if(clusterCount <= 2) break;

		var edge          = edges[i],
	        sourceNode    = edge[0],
            targetNode    = edge[1],
            sourceCluster = nodeMap[sourceNode],
            targetCluster = nodeMap[targetNode];

		if(sourceCluster === undefined && targetCluster === undefined) {
			nodeMap[sourceNode] = clusterIndex;
			nodeMap[targetNode] = clusterIndex;

			clusterMap[clusterIndex] = [sourceNode, targetNode];
			clusterIndex++;
			clusterCount--;
			nodeCount -= 2;

		} else if(sourceCluster === undefined) {
			nodeMap[sourceNode] = targetCluster;
			clusterMap[targetCluster].push(sourceNode);
			clusterCount--;
			nodeCount--;

		} else if(targetCluster === undefined) {
			nodeMap[targetNode] = sourceCluster;
			clusterMap[sourceCluster].push(targetNode);
			clusterCount--;
			nodeCount--;

		} else {
			if(sourceCluster != targetCluster) {
				var sourceNodes = clusterMap[sourceCluster],
				    targetNodes = clusterMap[targetCluster];

				if(sourceNodes.length > targetNodes.length) {
					clusterMap[sourceCluster] = _.union(sourceNodes, targetNodes);
					_.each(targetNodes, function(node) { nodeMap[node] = sourceCluster; });
					delete clusterMap[targetCluster];
				} else {
					clusterMap[targetCluster] = _.union(sourceNodes, targetNodes);
					_.each(sourceNodes, function(node) { nodeMap[node] = targetCluster; });
					delete clusterMap[sourceCluster];
				}

				clusterCount--;
			}
		}
	}

	return { clusters: _.values(clusterMap) };

	// Nicer but much slower version.
	/*var forest = _.map(nodes, function(node) { return [node]; });
	    var i = edges.length;

	    while(i--) {
	    	if(forest.length <= 2) break;
	    	var edge = edges[i];
	        var t1 = _.find(forest, function(tree) { return _.includes(tree, edge[0]); });
	        var t2 = _.find(forest, function(tree) { return _.includes(tree, edge[1]); });

	        if (!_.isEqual(t1, t2)) {
	            forest = _.without(forest, t1, t2);
	            forest.push(_.union(t1, t2));
	        }
	    }

	    return forest;*/
}

/**
 * Recursive function to apply the HCS algorithm, given the current graph as input.
 *
 * @param nodes
 * @param edges
 */
function hcs(nodes, edges, zoomed) {

	// A graph with one or two nodes is ignored.
	//if(edges.length <= 1) return;

	// Get the clusters by calculating the minimum spanning tree (Kruskal).
	var clusters = kruskal(nodes, edges);

	// Split clusters into subgraphs.
	_.each(clusters, function(cluster, i) {
		var clusterSize = cluster.length;

		// Drop clusters too small from the result (set them to the 0 group).
		if(zoomed && clusterSize <= 1) {
			self.postMessage( { finished: false, nodes: cluster } );
			return;
		} else if(!zoomed && clusterSize <= 2) {
			self.postMessage( { finished: false, nodes: cluster } );
			return;
		}

		var clusterEdges = [];
		var degrees = _.map(cluster, function() { return 0; });

		// Find the edges within the cluster, as well as the degrees of the edges.
		_.each(edges, function(edge) {
			var s = _.indexOf(cluster, edge[0]);
			if(s >= 0) {
				var t = _.indexOf(cluster, edge[1]);
				if(t >= 0) {
					clusterEdges.push(edge);
					degrees[s]++;
					degrees[t]++;
				}
			}
		});

		// Calculate the minimum degree.
		var minDegree = _.min(degrees);
		//console.log("# of edges: " + clusterEdges.length + ", # of nodes: " + clusterSize + ", min. degree: " + minDegree);

		// Check for highly-connectedness. If so, we're done with this cluster, else call this function again with the subgraph.
		if( (zoomed && minDegree >= clusterSize/2) || (!zoomed && minDegree > clusterSize/2) )
			self.postMessage( { finished: false, nodes: cluster, edges: clusterEdges } );
		else
			hcs(cluster, clusterEdges);
	});
}

self.addEventListener('message', function(e) {
	// Generate the nodes.
	var nodes = _.range(e.data.nodeCount);
	// Sort the edges by similarity score.
	var sortedEdges = _.sortBy(e.data.links, function(ln) { return ln[2]; });

	// First iteration. Input is the full network.
	hcs(nodes, sortedEdges, e.data.zoomed);

	// We're done. Return this fact.
	self.postMessage( { finished: true } );
}, false);
<commit_msg>Revert Kruskal<commit_after>/**
 * Web Worker for the HCS algorithm to cluster a network.
 */
importScripts('../vendor/lodash.min.js');

/**
 * Calculate the minimum spanning tree using Kruskal's algorithm.
 *
 * @param nodes
 * @param edges
 * @returns forest The clusters.
 */
function kruskal(nodes, edges) {
	// Nicer but much slower version.
	var forest = _.map(nodes, function(node) { return [node]; });
	    var i = edges.length;

	    while(i--) {
	    	if(forest.length <= 2) break;
	    	var edge = edges[i];
	        var t1 = _.find(forest, function(tree) { return _.includes(tree, edge[0]); });
	        var t2 = _.find(forest, function(tree) { return _.includes(tree, edge[1]); });

	        if (!_.isEqual(t1, t2)) {
	            forest = _.without(forest, t1, t2);
	            forest.push(_.union(t1, t2));
	        }
	    }

	    return forest;
}

/**
 * Recursive function to apply the HCS algorithm, given the current graph as input.
 *
 * @param nodes
 * @param edges
 */
function hcs(nodes, edges, zoomed) {

	// A graph with one or two nodes is ignored.
	//if(edges.length <= 1) return;

	// Get the clusters by calculating the minimum spanning tree (Kruskal).
	var clusters = kruskal(nodes, edges);

	// Split clusters into subgraphs.
	_.each(clusters, function(cluster, i) {
		var clusterSize = cluster.length;

		// Drop clusters too small from the result (set them to the 0 group).
		if(zoomed && clusterSize <= 1) {
			self.postMessage( { finished: false, nodes: cluster } );
			return;
		} else if(!zoomed && clusterSize <= 2) {
			self.postMessage( { finished: false, nodes: cluster } );
			return;
		}

		var clusterEdges = [];
		var degrees = _.map(cluster, function() { return 0; });

		// Find the edges within the cluster, as well as the degrees of the edges.
		_.each(edges, function(edge) {
			var s = _.indexOf(cluster, edge[0]);
			if(s >= 0) {
				var t = _.indexOf(cluster, edge[1]);
				if(t >= 0) {
					clusterEdges.push(edge);
					degrees[s]++;
					degrees[t]++;
				}
			}
		});

		// Calculate the minimum degree.
		var minDegree = _.min(degrees);
		//console.log("# of edges: " + clusterEdges.length + ", # of nodes: " + clusterSize + ", min. degree: " + minDegree);

		// Check for highly-connectedness. If so, we're done with this cluster, else call this function again with the subgraph.
		if( (zoomed && minDegree >= clusterSize/2) || (!zoomed && minDegree > clusterSize/2) )
			self.postMessage( { finished: false, nodes: cluster, edges: clusterEdges } );
		else
			hcs(cluster, clusterEdges);
	});
}

self.addEventListener('message', function(e) {
	// Generate the nodes.
	var nodes = _.range(e.data.nodeCount);
	// Sort the edges by similarity score.
	var sortedEdges = _.sortBy(e.data.links, function(ln) { return ln[2]; });

	// First iteration. Input is the full network.
	hcs(nodes, sortedEdges, e.data.zoomed);

	// We're done. Return this fact.
	self.postMessage( { finished: true } );
}, false);
<|endoftext|>