<commit_before>/********************************************************
 * Copyright (c) 2013 Azavea, Inc.
 * See LICENSE in the project root for copying permission
 ********************************************************/

define(['jquery', 'Leaflet', 'text!templates/neighborhoodList.html', 'text!templates/locationList.html',
        'topojson', 'icons', 'favorites', 'location', 'common', CEL.serverVars.gmapRequire, 'styling',
        'leaflet-providers', 'history'],
    function($, L, neighborhoodList, locationList, topojson, icons, favorites, location, common) {

        'use strict';

        var map,   // Leaflet map
            $map = $('#map'),
            $filters = $('.filters-inner :checkbox'),
            $filterClearAll = $('#filter-clear-all'),
            listItemSelector = '.locations-wrapper .accordion-group',
            zoomSettings = CEL.serverVars.zoomSettings,   // setting for zoom transition
            defaultZoom = $map.data('zoom') || 10,
            latSettings = CEL.serverVars.latSettings,    // lng + lat settings for initial view
            lngSettings = CEL.serverVars.lngSettings,
            geolocatedIcon,
            geolocatedMarker,                         // marker for autocomplete request
            template,    // Hold handlebars template
            /**
             * Valid types of map layers
             */
            layerType = {
                // No map layer is currently selected
                none: 'none',

                // Neighborhood polygons layer
                neighborhood: 'neighborhood',

                // Individual locations/schools layer
                location: 'location'
            },
            locationLayer = new L.LayerGroup(),   // Location/school layer group
            neighborhoodLayer = new L.LayerGroup(),   // Neighborhood layer group
            popupLayer = new L.LayerGroup(),    // Popup Layer
            currentLayer = layerType.none,      // Layer being currently displayed
            dm = new location.DataManager($filters),    // DataManager object
            isAutocompleteSet = true,
            autocompleteLocationId,
            autocompleteNeighborhoodId,
            $locationWrapper;                   // Store div wrapper for results on left side

        // Initialize geojson for neighborhood layer
        neighborhoodLayer = L.geoJson(null, {
            style: {
                color: '#317DC1',
                fillColor: '#91C73D',
                weight: 1,
                opacity: 1,
                fillOpacity: 0.3
            },
            onEachFeature: function(feature, layer) {
                layer.on('click', function(e) {
                    neighborhoodPan(feature.properties.primary_name,
                        feature.properties.num_schools,
                        feature.properties.center.lat,
                        feature.properties.center.lng,
                        false);
                });
            }
        });

        /*
         * Set map pan/zoom centered on a neighborhood/location if requested in the url
         */
        var setAutocompleteLocation = function() {
            if (isAutocompleteSet) {
                if (autocompleteLocationId) {
                    var loc = dm.locations[autocompleteLocationId],
                        pos = loc.getLatLng();
                    loc.setIcon({ highlighted: true });
                    locationPan(pos.lat, pos.lng);
                } else if (autocompleteNeighborhoodId) {
                    var value = dm.neighborhoods.data[autocompleteNeighborhoodId];
                    neighborhoodPan(value.name, value.schools, value.center.lat, value.center.lng, true);
                }
                isAutocompleteSet = false;
            }
        };

        /**
         * Controls logic of whether to display locations or neighborhoods
         * based on current zoom level. Called when map is initialized
         * and after a change in zoom level. Listens to the dm.neighborhoodUpdated and
         * dm.locationUpdated events to modify the view.
         */
        var displayMap = common.debounce(function(e) {
            var zoomLevel = map.getZoom(),
                mapCenter = map.getCenter();

            if (isAutocompleteSet && autocompleteLocationId) {
                dm.locationUpdate(map, locationLayer);
            } else if (isAutocompleteSet && autocompleteNeighborhoodId) {
                dm.neighborhoodUpdate();
            } else if (currentLayer !== layerType.neighborhood) {
                if (zoomLevel < zoomSettings) {
                    // We zoomed out, switch to neighborhoods
                    dm.neighborhoodUpdate();
                } else {
                    // We're still good, update locations
                    dm.locationUpdate(map, locationLayer);
                }
            } else if (currentLayer !== layerType.location) {
                if (zoomLevel >= zoomSettings) {
                    // We zoomed in, switch to locations
                    dm.locationUpdate(map, locationLayer);
                }
                if (e && e.type === 'DataManager' && e.namespace === 'filtersUpdated') {
                    // Ok, we're still on neighborhooks, only need to update if filters changed
                    dm.neighborhoodUpdate();
                }
            };

            // If we move the map, don't want to go back to geolocated spot in history and also don't want the geolocated
            // marker at the center of the map user is going back to
            History.pushState({isGeolocated: false},
                              null,
                              common.getUrl('browse-latlng', {lat: mapCenter.lat, lng: mapCenter.lng, zoom: zoomLevel}));
        }, 250);

        /**
         * Changes list results display using Handlebars templates
         * @param {Array of neighborhoods or locations} data
         * @param {Type of current layer, see layerType} dataType
         */
        var listResults = function(data, dataType) {
            var isNb = (dataType === layerType.neighborhood),
                html = (isNb ? neighborhoodList : locationList),
                dataList,
                template = Handlebars.compile(html),
                handlebarsData = [];

            // Sort everything by name ascending
            dataList = $.map(data, function(v, k) {
                return v;
            }).sort(function(a, b) {
                if (isNb) {
                    return a.name.localeCompare(b.name);
                } else {
                    return a.data.item.site_name.localeCompare(b.data.item.site_name);
                }
            });

            $.each(dataList, function(key, value) {
                var item = isNb ? value : value.data;
                handlebarsData.push(item);
            });

            $locationWrapper.empty();
            $locationWrapper.append(template(handlebarsData));

            // set header title
            var $headerFav = $('#header-fav'),
                $headerDist = $('#header-dist');
            if (isNb) {
                $headerFav.text(gettext('Neighborhood'));
                $headerDist.text(gettext('Schools'));
            } else {
                $headerFav.text(gettext('School'));
                $headerDist.text(gettext('More Information'));
            }

            // bind social sharing button clicks for individual locations
            $locationWrapper.find('.share-btn').on('click', function() {
                var key = $(this).data('key');

                $('#share-modal').trigger('init-modal', {
                    // the url is passed in to the sharing urls, so it must be absolute
                    url: document.location.origin + '/location/' + key  + '/',
                    title: 'Check out this early learning program'
                });
            });

            favorites.syncUI();
            favorites.addToggleListener({
                button: ".favs-toggle"
            });

            /**
             * Watch for favorite events, if there is one, then setIcon again
             */
            $('.favs-toggle').on('click', function(e) {
                var $this = $(this),
                    key = $this.data('loc-id'),
                    loc = dm.locations[key],
                    iconkey = 'icon-' + loc.getIconKey();

                // always highlighted because the mouse will be over the accordion div for the click
                loc.setIcon({ highlighted: true });
                $('#loc-icon-' + key).attr('src', common.getUrl(iconkey));
            });

            // Watch for hover events on the list so we can highlight both
            // the list item and the icon on the map
            $('.location-container').each(function(index) {
                var $this = $(this),
                    key = $this.data('key'),
                    loc = dm.locations[key],
                    iconkey = 'icon-' + loc.getIconKey();
                $('#loc-icon-' + key).attr('src', common.getUrl(iconkey));
            }).hover(function(e) {
                var $this = $(this),
                    key = $this.data('key'),
                    loc = dm.locations[key];

                if (e.type === 'mouseenter') {
                    $this.addClass('highlight');
                    loc.setIcon({'highlighted': true});
                } else if (e.type === 'mouseleave') {
                    $this.removeClass('highlight');
                    loc.setIcon({'highlighted': false});
                }
            }).on('show.bs.collapse', function(e) {
                var $this = $(this),
                    $morelessbtn = $this.find('.more-less-btn'),
                    $directionsLink = $this.find('#loc-dirs');
                $morelessbtn.html(gettext('Less'));
                $morelessbtn.attr('data-hint', gettext('Click to show less information'));
            }).on('hide.bs.collapse', function(e) {
                var $this = $(this),
                    $morelessbtn = $this.find('.more-less-btn');
                $morelessbtn.html(gettext('More'));
                $morelessbtn.attr('data-hint', gettext('Click to show more information'));
            });
        };

        /**
         * Change tooltip when refining search
         */
        var refineListener = function(){
            $('#collapseFilters').on('show.bs.collapse', function(e) {
                $('#refineBtn').attr('data-hint', gettext('Click to hide filters'));
            }).on('hide.bs.collapse', function(e) {
                $('#refineBtn').attr('data-hint', gettext('Click to show filters'));
            });
        };

        /*
         * Get map state from DOM and override defaults if necessary
         */
        var getMapState = function() {
            var lat = latSettings,
                lng = lngSettings,
                geolat = $map.data('geo-lat'),
                geolng = $map.data('geo-lng'),
                isGeolocated = false;
            if (geolat && geolng) {
                lat = geolat;
                lng = geolng;
                isGeolocated = true;
            }
            return { point: [lat, lng], isGeolocated: isGeolocated };
        };

        /**
         * Add functionality to explore button when viewing neighborhoods.
         * On click - map pans to center of neighborhood and zooms, then
         * rebuilds list display
         */
        var exploreButton = function() {
            $('.explore-btn').click(function() {
                map.setView([$(this).data('lat'), $(this).data('lng')], zoomSettings);
            });
        };

        /**
         * Pans to neighborhood and zooms to reasonable level if current view
         * is too far out
         * @param {Name of neighborhood} name
         * @param {Number of schools in neighborhood} numSchools
         * @param {Latitude of neighborhood centroid} lat
         * @param {Longitude of neighborhood centroid} lng
         * @param {Flag to pan map to neighborhood} panFlag
         */
        var neighborhoodPan = function(name, numSchools, lat, lng, panFlag) {
            popupLayer.clearLayers();
            if (panFlag) {
                map.panTo([lat, lng]);
                if (map.getZoom() < zoomSettings - 3) {
                    // Check if at reasonable zoom level, if too far out
                    // zoom user in
                    map.setZoom(zoomSettings - 3);
                }
            }
            var popupContent = '' + name + '
' + gettext('Number of Schools') + ': ' + numSchools + '
' + gettext('Explore') + '',
                popup = L.popup().setLatLng([lat, lng]).setContent(popupContent).addTo(popupLayer);

            $('.neighborhood-popup').on('click', function(e) {
                map.setView([lat, lng], zoomSettings);
            });
        };

        /*
         * Pans to location and zooms to reasonable level if current view is too far out
         */
        var locationPan = function(lat, lng) {
            map.panTo([lat, lng]);
            if (map.getZoom() < zoomSettings) {
            // Check if at reasonable zoom level, if too far out
                // zoom user in
                map.setZoom(zoomSettings);
            }
        };

        /**
         * Function that handles pans to neighborhood when clicking on accordion group
         *
         * Mostly just a wrapper around neighborhoodPan
         */
        var panHandler = function() {
            $(listItemSelector).click(function() {
                var $this = $(this);
                neighborhoodPan($this.data('name'), $this.data('schools'),
                                $this.data('lat'), $this.data('lng'), true);
            });
        };

        /**
         * Function that toggles map view on mobile devices
         */
        var mapToggle = function() {
            $('#toggleMapBtn').click(function() {
                $('.results-left').toggle();
                var $resultsRight = $('.results-right');
                if ($resultsRight.css('visibility') === 'hidden') {
                    $resultsRight.css('visibility', 'visible');
                }
                else {
                    $resultsRight.css('visibility', 'hidden');
                }
            });
        };

        /******************************************************
         *                    Bind events                     *
         ******************************************************/

        /*
         * Update view when the dm triggers its neighborhood updated event
         * We only want to attach this event once...
         */
        dm.events.on("DataManager.neighborhoodUpdated", function(e) {
            // If not already displaying neighborhoods and zoomed out
            if (currentLayer !== layerType.neighborhood) {
                currentLayer = layerType.neighborhood;
            }

            listResults(dm.neighborhoods.data, currentLayer);
            locationLayer.clearLayers();
            neighborhoodLayer.clearLayers();
            neighborhoodLayer.addData(dm.neighborhoods.geojson);
            map.addLayer(neighborhoodLayer);
            panHandler();
            exploreButton();

            // set map to location/neighborhood if autocomplete requested it
            setAutocompleteLocation();
        });

        /*
         * Update view when the dm triggers its location updated event
         * We only want to attach this event once...
         */
        dm.events.on("DataManager.locationUpdated", function(e) {
            // If not already displaying locations and zoomed in
            if (currentLayer !== layerType.location) {
                currentLayer = layerType.location;
                popupLayer.clearLayers();
            }

            map.removeLayer(neighborhoodLayer);
            map.addLayer(locationLayer);
            listResults(dm.locations, currentLayer);

            // set map to location/neighborhood if autocomplete requested it
            setAutocompleteLocation();
        });

        // Load data and build map when page loads
        return {
            init: function() {
                var state = getMapState(),
                    historyState = History.getState().data;
                map = new L.map('map').setView(state.point, defaultZoom);   // Initialize Leaflet map
                L.tileLayer.provider('Acetate.all').addTo(map);             // basemap
                map.addLayer(popupLayer);

                // Use history for mapstate if not undefined (prevents geolocation when browsing back)
                state.isGeolocated = historyState.isGeolocated === undefined ? state.isGeolocated : historyState.isGeolocated;

                // draw marker for geolocated point
                if (state.isGeolocated) {
                    geolocatedIcon = L.icon({
                        iconUrl: common.getUrl('icon-geolocation')
                    });
                    geolocatedMarker = L.marker(state.point, {icon: geolocatedIcon}).addTo(map);
                }

                autocompleteLocationId = $map.data('location-id');
                autocompleteNeighborhoodId = $map.data('neighborhood-id');

                $locationWrapper = $('.locations-wrapper');
                map.on('zoomend', displayMap);    // Set event handler to call displayMap when zoom changes
                dm.events.on('DataManager.filtersUpdated', displayMap);
                map.on('moveend', displayMap);
                map.on('zoomend', displayMap);

                // highlight the appropriate list item when a location popup is shown
                map.on('popupopen', function(e) {
                    var $div = $('.location-container div[data-key=' + e.popup.options.key + ']');
                    $div.addClass('highlight');
                    $div[0].scrollIntoView();
                });

                // remove all highlighting when a location popup is closed
                map.on('popupclose', function() {
                    $('.location-container.highlight').removeClass('highlight');
                });

                // set up social sharing for the top button (next to favorites)
                $('#share-favorites-btn').on('click', favorites.initShareModal);

                // Bind filtering click handlers
                $filters.on('click', function() { dm.onFilterChange(); });
                $filterClearAll.on('click', function() {
                    $filters.prop('checked', false);
                    dm.onFilterChange();
                });
                mapToggle();
                displayMap();
                refineListener();
            }
        };
    }
);
<commit_msg>Syntax change as per Bennet's suggestion<commit_after>/********************************************************
 * Copyright (c) 2013 Azavea, Inc.
 * See LICENSE in the project root for copying permission
 ********************************************************/

define(['jquery', 'Leaflet', 'text!templates/neighborhoodList.html', 'text!templates/locationList.html',
        'topojson', 'icons', 'favorites', 'location', 'common', CEL.serverVars.gmapRequire, 'styling',
        'leaflet-providers', 'history'],
    function($, L, neighborhoodList, locationList, topojson, icons, favorites, location, common) {

        'use strict';

        var map,   // Leaflet map
            $map = $('#map'),
            $filters = $('.filters-inner :checkbox'),
            $filterClearAll = $('#filter-clear-all'),
            listItemSelector = '.locations-wrapper .accordion-group',
            zoomSettings = CEL.serverVars.zoomSettings,   // setting for zoom transition
            defaultZoom = $map.data('zoom') || 10,
            latSettings = CEL.serverVars.latSettings,    // lng + lat settings for initial view
            lngSettings = CEL.serverVars.lngSettings,
            geolocatedIcon,
            geolocatedMarker,                         // marker for autocomplete request
            template,    // Hold handlebars template
            /**
             * Valid types of map layers
             */
            layerType = {
                // No map layer is currently selected
                none: 'none',

                // Neighborhood polygons layer
                neighborhood: 'neighborhood',

                // Individual locations/schools layer
                location: 'location'
            },
            locationLayer = new L.LayerGroup(),   // Location/school layer group
            neighborhoodLayer = new L.LayerGroup(),   // Neighborhood layer group
            popupLayer = new L.LayerGroup(),    // Popup Layer
            currentLayer = layerType.none,      // Layer being currently displayed
            dm = new location.DataManager($filters),    // DataManager object
            isAutocompleteSet = true,
            autocompleteLocationId,
            autocompleteNeighborhoodId,
            $locationWrapper;                   // Store div wrapper for results on left side

        // Initialize geojson for neighborhood layer
        neighborhoodLayer = L.geoJson(null, {
            style: {
                color: '#317DC1',
                fillColor: '#91C73D',
                weight: 1,
                opacity: 1,
                fillOpacity: 0.3
            },
            onEachFeature: function(feature, layer) {
                layer.on('click', function(e) {
                    neighborhoodPan(feature.properties.primary_name,
                        feature.properties.num_schools,
                        feature.properties.center.lat,
                        feature.properties.center.lng,
                        false);
                });
            }
        });

        /*
         * Set map pan/zoom centered on a neighborhood/location if requested in the url
         */
        var setAutocompleteLocation = function() {
            if (isAutocompleteSet) {
                if (autocompleteLocationId) {
                    var loc = dm.locations[autocompleteLocationId],
                        pos = loc.getLatLng();
                    loc.setIcon({ highlighted: true });
                    locationPan(pos.lat, pos.lng);
                } else if (autocompleteNeighborhoodId) {
                    var value = dm.neighborhoods.data[autocompleteNeighborhoodId];
                    neighborhoodPan(value.name, value.schools, value.center.lat, value.center.lng, true);
                }
                isAutocompleteSet = false;
            }
        };

        /**
         * Controls logic of whether to display locations or neighborhoods
         * based on current zoom level. Called when map is initialized
         * and after a change in zoom level. Listens to the dm.neighborhoodUpdated and
         * dm.locationUpdated events to modify the view.
         */
        var displayMap = common.debounce(function(e) {
            var zoomLevel = map.getZoom(),
                mapCenter = map.getCenter();

            if (isAutocompleteSet && autocompleteLocationId) {
                dm.locationUpdate(map, locationLayer);
            } else if (isAutocompleteSet && autocompleteNeighborhoodId) {
                dm.neighborhoodUpdate();
            } else if (currentLayer !== layerType.neighborhood) {
                if (zoomLevel < zoomSettings) {
                    // We zoomed out, switch to neighborhoods
                    dm.neighborhoodUpdate();
                } else {
                    // We're still good, update locations
                    dm.locationUpdate(map, locationLayer);
                }
            } else if (currentLayer !== layerType.location) {
                if (zoomLevel >= zoomSettings) {
                    // We zoomed in, switch to locations
                    dm.locationUpdate(map, locationLayer);
                }
                if (e && e.type === 'DataManager' && e.namespace === 'filtersUpdated') {
                    // Ok, we're still on neighborhooks, only need to update if filters changed
                    dm.neighborhoodUpdate();
                }
            };

            // If we move the map, don't want to go back to geolocated spot in history and also don't want the geolocated
            // marker at the center of the map user is going back to
            History.pushState({isGeolocated: false},
                              null,
                              common.getUrl('browse-latlng', {lat: mapCenter.lat, lng: mapCenter.lng, zoom: zoomLevel}));
        }, 250);

        /**
         * Changes list results display using Handlebars templates
         * @param {Array of neighborhoods or locations} data
         * @param {Type of current layer, see layerType} dataType
         */
        var listResults = function(data, dataType) {
            var isNb = (dataType === layerType.neighborhood),
                html = (isNb ? neighborhoodList : locationList),
                dataList,
                template = Handlebars.compile(html),
                handlebarsData = [];

            // Sort everything by name ascending
            dataList = $.map(data, function(v, k) {
                return v;
            }).sort(function(a, b) {
                if (isNb) {
                    return a.name.localeCompare(b.name);
                } else {
                    return a.data.item.site_name.localeCompare(b.data.item.site_name);
                }
            });

            $.each(dataList, function(key, value) {
                var item = isNb ? value : value.data;
                handlebarsData.push(item);
            });

            $locationWrapper.empty();
            $locationWrapper.append(template(handlebarsData));

            // set header title
            var $headerFav = $('#header-fav'),
                $headerDist = $('#header-dist');
            if (isNb) {
                $headerFav.text(gettext('Neighborhood'));
                $headerDist.text(gettext('Schools'));
            } else {
                $headerFav.text(gettext('School'));
                $headerDist.text(gettext('More Information'));
            }

            // bind social sharing button clicks for individual locations
            $locationWrapper.find('.share-btn').on('click', function() {
                var key = $(this).data('key');

                $('#share-modal').trigger('init-modal', {
                    // the url is passed in to the sharing urls, so it must be absolute
                    url: document.location.origin + '/location/' + key  + '/',
                    title: 'Check out this early learning program'
                });
            });

            favorites.syncUI();
            favorites.addToggleListener({
                button: ".favs-toggle"
            });

            /**
             * Watch for favorite events, if there is one, then setIcon again
             */
            $('.favs-toggle').on('click', function(e) {
                var $this = $(this),
                    key = $this.data('loc-id'),
                    loc = dm.locations[key],
                    iconkey = 'icon-' + loc.getIconKey();

                // always highlighted because the mouse will be over the accordion div for the click
                loc.setIcon({ highlighted: true });
                $('#loc-icon-' + key).attr('src', common.getUrl(iconkey));
            });

            // Watch for hover events on the list so we can highlight both
            // the list item and the icon on the map
            $('.location-container').each(function(index) {
                var $this = $(this),
                    key = $this.data('key'),
                    loc = dm.locations[key],
                    iconkey = 'icon-' + loc.getIconKey();
                $('#loc-icon-' + key).attr('src', common.getUrl(iconkey));
            }).hover(function(e) {
                var $this = $(this),
                    key = $this.data('key'),
                    loc = dm.locations[key];

                if (e.type === 'mouseenter') {
                    $this.addClass('highlight');
                    loc.setIcon({'highlighted': true});
                } else if (e.type === 'mouseleave') {
                    $this.removeClass('highlight');
                    loc.setIcon({'highlighted': false});
                }
            }).on('show.bs.collapse', function(e) {
                var $this = $(this),
                    $morelessbtn = $this.find('.more-less-btn'),
                    $directionsLink = $this.find('#loc-dirs');
                $morelessbtn.html(gettext('Less'));
                $morelessbtn.attr('data-hint', gettext('Click to show less information'));
            }).on('hide.bs.collapse', function(e) {
                var $this = $(this),
                    $morelessbtn = $this.find('.more-less-btn');
                $morelessbtn.html(gettext('More'));
                $morelessbtn.attr('data-hint', gettext('Click to show more information'));
            });
        };

        /**
         * Change tooltip when refining search
         */
        var refineListener = function(){
            $('#collapseFilters').on('show.bs.collapse', function(e) {
                $('#refineBtn').attr('data-hint', gettext('Click to hide filters'));
            }).on('hide.bs.collapse', function(e) {
                $('#refineBtn').attr('data-hint', gettext('Click to show filters'));
            });
        };

        /*
         * Get map state from DOM and override defaults if necessary
         */
        var getMapState = function() {
            var lat = latSettings,
                lng = lngSettings,
                geolat = $map.data('geo-lat'),
                geolng = $map.data('geo-lng'),
                isGeolocated = false;
            if (geolat && geolng) {
                lat = geolat;
                lng = geolng;
                isGeolocated = true;
            }
            return { point: [lat, lng], isGeolocated: isGeolocated };
        };

        /**
         * Add functionality to explore button when viewing neighborhoods.
         * On click - map pans to center of neighborhood and zooms, then
         * rebuilds list display
         */
        var exploreButton = function() {
            $('.explore-btn').click(function() {
                map.setView([$(this).data('lat'), $(this).data('lng')], zoomSettings);
            });
        };

        /**
         * Pans to neighborhood and zooms to reasonable level if current view
         * is too far out
         * @param {Name of neighborhood} name
         * @param {Number of schools in neighborhood} numSchools
         * @param {Latitude of neighborhood centroid} lat
         * @param {Longitude of neighborhood centroid} lng
         * @param {Flag to pan map to neighborhood} panFlag
         */
        var neighborhoodPan = function(name, numSchools, lat, lng, panFlag) {
            popupLayer.clearLayers();
            if (panFlag) {
                map.panTo([lat, lng]);
                if (map.getZoom() < zoomSettings - 3) {
                    // Check if at reasonable zoom level, if too far out
                    // zoom user in
                    map.setZoom(zoomSettings - 3);
                }
            }
            var popupContent = '' + name + '
' + gettext('Number of Schools') + ': ' + numSchools + '
' + gettext('Explore') + '',
                popup = L.popup().setLatLng([lat, lng]).setContent(popupContent).addTo(popupLayer);

            $('.neighborhood-popup').on('click', function(e) {
                map.setView([lat, lng], zoomSettings);
            });
        };

        /*
         * Pans to location and zooms to reasonable level if current view is too far out
         */
        var locationPan = function(lat, lng) {
            map.panTo([lat, lng]);
            if (map.getZoom() < zoomSettings) {
            // Check if at reasonable zoom level, if too far out
                // zoom user in
                map.setZoom(zoomSettings);
            }
        };

        /**
         * Function that handles pans to neighborhood when clicking on accordion group
         *
         * Mostly just a wrapper around neighborhoodPan
         */
        var panHandler = function() {
            $(listItemSelector).click(function() {
                var $this = $(this);
                neighborhoodPan($this.data('name'), $this.data('schools'),
                                $this.data('lat'), $this.data('lng'), true);
            });
        };

        /**
         * Function that toggles map view on mobile devices
         */
        var mapToggle = function() {
            $('#toggleMapBtn').click(function() {
                $('.results-left').toggle();
                var $resultsRight = $('.results-right');
                if ($resultsRight.css('visibility') === 'hidden') {
                    $resultsRight.css('visibility', 'visible');
                }
                else {
                    $resultsRight.css('visibility', 'hidden');
                }
            });
        };

        /******************************************************
         *                    Bind events                     *
         ******************************************************/

        /*
         * Update view when the dm triggers its neighborhood updated event
         * We only want to attach this event once...
         */
        dm.events.on("DataManager.neighborhoodUpdated", function(e) {
            // If not already displaying neighborhoods and zoomed out
            if (currentLayer !== layerType.neighborhood) {
                currentLayer = layerType.neighborhood;
            }

            listResults(dm.neighborhoods.data, currentLayer);
            locationLayer.clearLayers();
            neighborhoodLayer.clearLayers();
            neighborhoodLayer.addData(dm.neighborhoods.geojson);
            map.addLayer(neighborhoodLayer);
            panHandler();
            exploreButton();

            // set map to location/neighborhood if autocomplete requested it
            setAutocompleteLocation();
        });

        /*
         * Update view when the dm triggers its location updated event
         * We only want to attach this event once...
         */
        dm.events.on("DataManager.locationUpdated", function(e) {
            // If not already displaying locations and zoomed in
            if (currentLayer !== layerType.location) {
                currentLayer = layerType.location;
                popupLayer.clearLayers();
            }

            map.removeLayer(neighborhoodLayer);
            map.addLayer(locationLayer);
            listResults(dm.locations, currentLayer);

            // set map to location/neighborhood if autocomplete requested it
            setAutocompleteLocation();
        });

        // Load data and build map when page loads
        return {
            init: function() {
                var state = getMapState(),
                    historyState = History.getState().data;
                map = new L.map('map').setView(state.point, defaultZoom);   // Initialize Leaflet map
                L.tileLayer.provider('Acetate.all').addTo(map);             // basemap
                map.addLayer(popupLayer);

                // Use history for mapstate if not undefined (prevents geolocation when browsing back)
                state.isGeolocated = historyState.isGeolocated || state.isGeolocated;

                // draw marker for geolocated point
                if (state.isGeolocated) {
                    geolocatedIcon = L.icon({
                        iconUrl: common.getUrl('icon-geolocation')
                    });
                    geolocatedMarker = L.marker(state.point, {icon: geolocatedIcon}).addTo(map);
                }

                autocompleteLocationId = $map.data('location-id');
                autocompleteNeighborhoodId = $map.data('neighborhood-id');

                $locationWrapper = $('.locations-wrapper');
                map.on('zoomend', displayMap);    // Set event handler to call displayMap when zoom changes
                dm.events.on('DataManager.filtersUpdated', displayMap);
                map.on('moveend', displayMap);
                map.on('zoomend', displayMap);

                // highlight the appropriate list item when a location popup is shown
                map.on('popupopen', function(e) {
                    var $div = $('.location-container div[data-key=' + e.popup.options.key + ']');
                    $div.addClass('highlight');
                    $div[0].scrollIntoView();
                });

                // remove all highlighting when a location popup is closed
                map.on('popupclose', function() {
                    $('.location-container.highlight').removeClass('highlight');
                });

                // set up social sharing for the top button (next to favorites)
                $('#share-favorites-btn').on('click', favorites.initShareModal);

                // Bind filtering click handlers
                $filters.on('click', function() { dm.onFilterChange(); });
                $filterClearAll.on('click', function() {
                    $filters.prop('checked', false);
                    dm.onFilterChange();
                });
                mapToggle();
                displayMap();
                refineListener();
            }
        };
    }
);
<|endoftext|>


<commit_before>var Util = {

    curry: function(orig_func) {
        /* Specify arguments of a function without actually calling
           that function yet.
           Source:
           http://benalman.com/news/2010/09/partial-application-in-javascript/ */
        var ap = Array.prototype,
            args = arguments;

        function fn() {
            ap.push.apply( fn.args, arguments );

            return fn.args.length < orig_func.length
                ? fn
                : orig_func.apply( this, fn.args );
        };

        return function() {
            fn.args = ap.slice.call( args, 1 );
            return fn.apply( this, arguments );
        };
    },

    refreshPage: function() {
        window.location.reload();
    },

    splitlines: function(s) {
        /* Split a string s by its newline characters. Return the
        resulting multiple strings as an array.
        This regex should handle \r\n, \r, and \n. */
        return s.split(/\r\n|[\n\r]/);
    },

    trimString: function(s) {
        /* Trim leading and trailing whitespace off of a string.
        http://blog.stevenlevithan.com/archives/faster-trim-javascript */
        return s.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
    },

    zfill: function(str, desiredLength) {
        /* Left-pad a string with 0s until the desired string length
        is reached. */
        var outStr = str;
        while (outStr.length < desiredLength) {
            outStr = '0' + outStr;
        }
        return outStr;
    },

    /* Arrays */

    removeFromArray: function(arr, value) {
        /* Remove all instances of value from the array arr. */
        for(var i = arr.length - 1; i >= 0; i--) {
            if(arr[i] === value) {
               arr.splice(i, 1);
            }
        }
    },

    sortedLocation: function(element, array, comparer, start, end) {
        /* Determine where the element would go in the already-sorted
        array if inserted.
        The return value is the index AFTER which the element would be
        inserted. -1 means at the beginning of the array.

        Source: http://stackoverflow.com/a/18341744/ and /a/20261974/ */
        if (array.length === 0) {return -1;}

        start = start || 0;
        end = end || array.length;
        var pivot = parseInt(start + (end - start) / 2, 10);
        if (array[pivot] === element) {return pivot;}

        var c = comparer(element, array[pivot]);
        if (end - start <= 1) {return c < 0 ? pivot - 1 : pivot;}

        if (c < 0) {
            return Util.sortedLocation(element, array, comparer, start, pivot);
        } else if (c === 0) {
            return pivot;
        } else {  // c > 0
            return Util.sortedLocation(element, array, comparer, pivot, end);
        }
    },

    sortedInsert: function(element, array, comparer) {
        /* Insert element into the already-sorted array. comparer
        defines the sorting comparison method.

        Source: http://stackoverflow.com/questions/1344500/ */

        array.splice(Util.sortedLocation(element, array, comparer) + 1, 0, element);
        return array;
    },

    /* Date/time utility functions */

    dateObjToTimeAgo: function(dateObj) {
        // Code adapted from:
        // http://stackoverflow.com/a/12475270
        //
        // For more features (internationalization, auto refreshes),
        // possibly try http://timeago.yarp.com/ (uses MIT license)

        var time = dateObj.getTime();
        var currentTime = Date.now();

        var time_formats = [
            [60, 'seconds', 1], // 60
            [120, '1 minute ago', '1 minute from now'], // 60*2
            [3600, 'minutes', 60], // 60*60, 60
            [7200, '1 hour ago', '1 hour from now'], // 60*60*2
            [86400, 'hours', 3600], // 60*60*24, 60*60
            [172800, '1 day ago', '1 day from now'], // 60*60*24*2
            [604800, 'days', 86400], // 60*60*24*7, 60*60*24
            [1209600, '1 week ago', '1 week from now'], // 60*60*24*7*4*2
            [2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
            [4838400, '1 month ago', '1 month from now'], // 60*60*24*7*4*2
            [29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
            [58060800, '1 year ago', '1 year from now'], // 60*60*24*7*4*12*2
            [2903040000, 'years', 29030400] // 60*60*24*7*4*12*100, 60*60*24*7*4*12
        ];

        var seconds = (currentTime - time) / 1000;
        var token = 'ago';
        var format_choice = 1;

        if (seconds == 0) {
            return 'Just now'
        }
        if (seconds < 0) {
            seconds = Math.abs(seconds);
            token = 'from now';
            format_choice = 2;
        }

        var i, format;
        for (i = 0; i < time_formats.length; i++) {
            format = time_formats[i];

            if (seconds < format[0]) {
                if (typeof format[2] == 'string')
                    return format[format_choice];
                else  // number
                    return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
            }
        }
        return "At an unknown date";
    },

    dateObjToHMElapsed: function(dateObj) {
        var time = dateObj.getTime();
        var currentTime = Date.now();

        var totalMillis = currentTime - time;
        var totalMinutes = totalMillis / (1000*60);
        var minutes = totalMinutes % 60;
        var hours = (totalMinutes-minutes)/60;

        var hoursStr = hours.toFixed();
        var minutesStrPadded = Util.zfill(minutes.toFixed(), 2);

        return (hoursStr + ":" + minutesStrPadded);
    },

    timeSecToHMS: function(totalSeconds) {
        var seconds = totalSeconds % 60;
        var totalMinutes = (totalSeconds-seconds)/60;
        var minutes = totalMinutes % 60;
        var hours = (totalMinutes-minutes)/60;

        var hoursStr = hours.toFixed();
        var minutesStr = minutes.toFixed();
        var minutesStrPadded = Util.zfill(minutes.toFixed(), 2);
        var secondsStr = Util.zfill(seconds.toFixed(), 2);

        if (hours === 0) {
            return (minutesStr + ":" + secondsStr);
        }
        else {
            return (hoursStr + ":" + minutesStrPadded + ":" + secondsStr);
        }
    }

};
<commit_msg>Fix stream uptime display bug where it would say e.g. "0:60". Effectively, hours were rounded down while minutes were rounded to nearest. Now both round down.<commit_after>var Util = {

    curry: function(orig_func) {
        /* Specify arguments of a function without actually calling
           that function yet.
           Source:
           http://benalman.com/news/2010/09/partial-application-in-javascript/ */
        var ap = Array.prototype,
            args = arguments;

        function fn() {
            ap.push.apply( fn.args, arguments );

            return fn.args.length < orig_func.length
                ? fn
                : orig_func.apply( this, fn.args );
        };

        return function() {
            fn.args = ap.slice.call( args, 1 );
            return fn.apply( this, arguments );
        };
    },

    refreshPage: function() {
        window.location.reload();
    },

    splitlines: function(s) {
        /* Split a string s by its newline characters. Return the
        resulting multiple strings as an array.
        This regex should handle \r\n, \r, and \n. */
        return s.split(/\r\n|[\n\r]/);
    },

    trimString: function(s) {
        /* Trim leading and trailing whitespace off of a string.
        http://blog.stevenlevithan.com/archives/faster-trim-javascript */
        return s.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
    },

    zfill: function(str, desiredLength) {
        /* Left-pad a string with 0s until the desired string length
        is reached. */
        var outStr = str;
        while (outStr.length < desiredLength) {
            outStr = '0' + outStr;
        }
        return outStr;
    },

    /* Arrays */

    removeFromArray: function(arr, value) {
        /* Remove all instances of value from the array arr. */
        for(var i = arr.length - 1; i >= 0; i--) {
            if(arr[i] === value) {
               arr.splice(i, 1);
            }
        }
    },

    sortedLocation: function(element, array, comparer, start, end) {
        /* Determine where the element would go in the already-sorted
        array if inserted.
        The return value is the index AFTER which the element would be
        inserted. -1 means at the beginning of the array.

        Source: http://stackoverflow.com/a/18341744/ and /a/20261974/ */
        if (array.length === 0) {return -1;}

        start = start || 0;
        end = end || array.length;
        var pivot = parseInt(start + (end - start) / 2, 10);
        if (array[pivot] === element) {return pivot;}

        var c = comparer(element, array[pivot]);
        if (end - start <= 1) {return c < 0 ? pivot - 1 : pivot;}

        if (c < 0) {
            return Util.sortedLocation(element, array, comparer, start, pivot);
        } else if (c === 0) {
            return pivot;
        } else {  // c > 0
            return Util.sortedLocation(element, array, comparer, pivot, end);
        }
    },

    sortedInsert: function(element, array, comparer) {
        /* Insert element into the already-sorted array. comparer
        defines the sorting comparison method.

        Source: http://stackoverflow.com/questions/1344500/ */

        array.splice(Util.sortedLocation(element, array, comparer) + 1, 0, element);
        return array;
    },

    /* Date/time utility functions */

    dateObjToTimeAgo: function(dateObj) {
        // Code adapted from:
        // http://stackoverflow.com/a/12475270
        //
        // For more features (internationalization, auto refreshes),
        // possibly try http://timeago.yarp.com/ (uses MIT license)

        var time = dateObj.getTime();
        var currentTime = Date.now();

        var time_formats = [
            [60, 'seconds', 1], // 60
            [120, '1 minute ago', '1 minute from now'], // 60*2
            [3600, 'minutes', 60], // 60*60, 60
            [7200, '1 hour ago', '1 hour from now'], // 60*60*2
            [86400, 'hours', 3600], // 60*60*24, 60*60
            [172800, '1 day ago', '1 day from now'], // 60*60*24*2
            [604800, 'days', 86400], // 60*60*24*7, 60*60*24
            [1209600, '1 week ago', '1 week from now'], // 60*60*24*7*4*2
            [2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
            [4838400, '1 month ago', '1 month from now'], // 60*60*24*7*4*2
            [29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
            [58060800, '1 year ago', '1 year from now'], // 60*60*24*7*4*12*2
            [2903040000, 'years', 29030400] // 60*60*24*7*4*12*100, 60*60*24*7*4*12
        ];

        var seconds = (currentTime - time) / 1000;
        var token = 'ago';
        var format_choice = 1;

        if (seconds == 0) {
            return 'Just now'
        }
        if (seconds < 0) {
            seconds = Math.abs(seconds);
            token = 'from now';
            format_choice = 2;
        }

        var i, format;
        for (i = 0; i < time_formats.length; i++) {
            format = time_formats[i];

            if (seconds < format[0]) {
                if (typeof format[2] == 'string')
                    return format[format_choice];
                else  // number
                    return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
            }
        }
        return "At an unknown date";
    },

    dateObjToHMElapsed: function(dateObj) {
        var time = dateObj.getTime();
        var currentTime = Date.now();

        var totalMillis = currentTime - time;
        // Round down to the minute
        var totalMinutes = Math.floor(totalMillis / (1000*60));
        var minutes = totalMinutes % 60;
        var hours = Math.floor(totalMinutes / 60);

        var hoursStr = hours.toString();
        var minutesStrPadded = Util.zfill(minutes.toString(), 2);

        return (hoursStr + ":" + minutesStrPadded);
    },

    timeSecToHMS: function(totalSeconds) {
        var seconds = totalSeconds % 60;
        var totalMinutes = (totalSeconds-seconds)/60;
        var minutes = totalMinutes % 60;
        var hours = (totalMinutes-minutes)/60;

        var hoursStr = hours.toFixed();
        var minutesStr = minutes.toFixed();
        var minutesStrPadded = Util.zfill(minutes.toFixed(), 2);
        var secondsStr = Util.zfill(seconds.toFixed(), 2);

        if (hours === 0) {
            return (minutesStr + ":" + secondsStr);
        }
        else {
            return (hoursStr + ":" + minutesStrPadded + ":" + secondsStr);
        }
    }

};
<|endoftext|>


<commit_before>var _      = require('underscore')
    , Step   = require('step')
    , pg     = require('pg');//.native; // disabled for now due to: https://github.com/brianc/node-postgres/issues/48
_.mixin(require('underscore.string'));

// Max database connections in the pool
// Subsequent connections will block waiting for a free slot
pg.defaults.poolSize = global.settings.db_pool_size || 16;

// Milliseconds of idle time before removing connection from pool
// TODO: make config setting ?
pg.defaults.poolIdleTimeout = global.settings.db_pool_idleTimeout || 30000;

// Frequency to check for idle clients within the pool, ms
// TODO: make config setting ?
pg.defaults.reapIntervalMillis = global.settings.db_pool_reapInterval || 1000;

pg.on('error', function(err, client) {
  console.log("PostgreSQL connection error: " + err);
});

// Workaround for https://github.com/Vizzuality/CartoDB-SQL-API/issues/100
var types = require(__dirname + '/../../node_modules/pg/lib/types');
var arrayParser = require(__dirname + '/../../node_modules/pg/lib/types/arrayParser');
var floatParser = function(val) {
  return parseFloat(val);
};
var floatArrayParser = function(val) {
  if(!val) { return null; }
  var p = arrayParser.create(val, function(entry) {
    return floatParser(entry);
  });
  return p.parse();
};
types.setTypeParser(1700, floatParser);
types.setTypeParser(700, floatParser);
types.setTypeParser(701, floatParser);
types.setTypeParser(1021, floatArrayParser);
types.setTypeParser(1022, floatArrayParser);
types.setTypeParser(1231, floatArrayParser);

// PSQL
//
// A simple postgres wrapper with logic about username and database to connect
//
// * intended for use with pg_bouncer
// * defaults to connecting with a "READ ONLY" user to given DB if not passed a specific user_id
var PSQL = function(user_id, db) {

    var error_text = "Incorrect access parameters. If you are accessing via OAuth, please check your tokens are correct. For public users, please ensure your table is published."
    if (!_.isString(user_id) && !_.isString(db)) throw new Error(error_text);

    var me = {
        public_user: "publicuser"
        , user_id: user_id
        , db: db
    };

    me.username = function(){
        var username = this.public_user;
        if (_.isString(this.user_id))
            username = _.template(global.settings.db_user, {user_id: this.user_id});

        return username;
    };

    me.database = function(){
        var database = db;
        if (_.isString(this.user_id))
            database = _.template(global.settings.db_base_name, {user_id: this.user_id});

        return database;
    };

    me.conString = "tcp://" + me.username() + "@" +
                    global.settings.db_host + ":" +
                    global.settings.db_port + "/" +
                    me.database();

    me.eventedQuery = function(sql, callback){
        var that = this;

        Step(
            function(){
                that.sanitize(sql, this);
            },
            function(err, clean){
                if (err) throw err;
                pg.connect(that.conString, this);
            },
            function(err, client, done){
                if (err) throw err;
                var query = client.query(sql);
                // NOTE: for some obscure reason passing "done" directly
                //       as the listener works but can be slower
                //      (by x2 factor!)
                query.on('end', function() { done(); });
                return query;
            },
            function(err, query){
                callback(err, query)
            }
        );
    },

    me.query = function(sql, callback){
        var that = this;
        var finish;

        Step(
            function(){
                that.sanitize(sql, this);
            },
            function(err, clean){
                if (err) throw err;
                pg.connect(that.conString, this);
            },
            function(err, client, done){
                if (err) throw err;
                finish = done;
                client.query(sql, this);
            },
            function(err, res){

                // Release client to the pool
                // should this be postponed to after the callback ?
                // NOTE: if we pass a true value to finish() the client
                //       will be removed from the pool.
                //       We don't want this. Not now.
                if ( finish ) finish();

                callback(err, res)
            }
        );
    };

    // throw exception if illegal operations are detected
    // NOTE: this check is weak hack, better database
    //       permissions should be used instead.
    me.sanitize = function(sql, callback){
        // NOTE: illegal table access is checked in main app
        if (sql.match(/^\s+set\s+/i)){
            var error = new SyntaxError("SET command is forbidden");
            error.http_status = 403;
            callback(error);
            return;
        }
        callback(null,true);
    };

    return me;
};

// little hack for UI
//
// TODO:drop, fix in the UI (it's not documented in doc/API)
//
PSQL.window_sql = function(sql, limit, offset) {
  // only window select functions (NOTE: "values" will be broken, "with" will be broken)
  if (!_.isNumber(limit) || !_.isNumber(offset) ) return sql;

  var cte = '';

  if ( sql.match(/^\s*WITH\s/i) ) {

    var rem = sql; // analyzed portion of sql
    var q; // quote char
    var n = 0; // nested parens level
    var s = 0; // 0:outQuote, 1:inQuote
    var l;
    while (1) {
      l = rem.search(/[("')]/);
//console.log("REM Is " + rem);
      if ( l < 0 ) {
        console.log("Malformed SQL");
        return sql;
      }
      var f = rem.charAt(l);
//console.log("n:" + n + " s:" + s + " l:" + l + " charAt(l):" + f + " charAt(l+1):" + rem.charAt(l+1));
      if ( s == 0 ) {
        if ( f == '(' ) ++n;
        else if ( f == ')' ) {
          if ( ! --n ) { // end of CTE
            cte += rem.substr(0, l+1);
            rem = rem.substr(l+1);
  //console.log("Out of cte, rem is " + rem);
            if ( rem.search(/^s*,/) < 0 ) break;
            else continue; // cte and rem already updated
          }
        }
        else { // enter quoting
          s = 1; q = f;
        }
      }
      else if ( f == q ) {
        if ( rem.charAt(l+1) == f ) ++l; // escaped
        else s = 0; // exit quoting
      }
      cte += rem.substr(0, l+1);
      rem = rem.substr(l+1);
    }
/*
console.log("cte: " + cte);
console.log("rem: " + rem);
*/
    sql = rem; //sql.substr(l+1);
  }

  if ( sql.match(/^\s*SELECT\s/i) ) {
      return cte + "SELECT * FROM (" + sql + ") AS cdbq_1 LIMIT " + limit + " OFFSET " + offset;
  }

  return cte + sql;
}

module.exports = PSQL;
<commit_msg>Use float parsing for int8 too<commit_after>var _      = require('underscore')
    , Step   = require('step')
    , pg     = require('pg');//.native; // disabled for now due to: https://github.com/brianc/node-postgres/issues/48
_.mixin(require('underscore.string'));

// Max database connections in the pool
// Subsequent connections will block waiting for a free slot
pg.defaults.poolSize = global.settings.db_pool_size || 16;

// Milliseconds of idle time before removing connection from pool
// TODO: make config setting ?
pg.defaults.poolIdleTimeout = global.settings.db_pool_idleTimeout || 30000;

// Frequency to check for idle clients within the pool, ms
// TODO: make config setting ?
pg.defaults.reapIntervalMillis = global.settings.db_pool_reapInterval || 1000;

pg.on('error', function(err, client) {
  console.log("PostgreSQL connection error: " + err);
});

// Workaround for https://github.com/Vizzuality/CartoDB-SQL-API/issues/100
var types = require(__dirname + '/../../node_modules/pg/lib/types');
var arrayParser = require(__dirname + '/../../node_modules/pg/lib/types/arrayParser');
var floatParser = function(val) {
  return parseFloat(val);
};
var floatArrayParser = function(val) {
  if(!val) { return null; }
  var p = arrayParser.create(val, function(entry) {
    return floatParser(entry);
  });
  return p.parse();
};
types.setTypeParser(20, floatParser); // int8
types.setTypeParser(700, floatParser); // float4
types.setTypeParser(701, floatParser); // float8
types.setTypeParser(1700, floatParser); // numeric
types.setTypeParser(1021, floatArrayParser); // _float4
types.setTypeParser(1022, floatArrayParser); // _float8
types.setTypeParser(1231, floatArrayParser); // _numeric
types.setTypeParser(1016, floatArrayParser); // _int8

// PSQL
//
// A simple postgres wrapper with logic about username and database to connect
//
// * intended for use with pg_bouncer
// * defaults to connecting with a "READ ONLY" user to given DB if not passed a specific user_id
var PSQL = function(user_id, db) {

    var error_text = "Incorrect access parameters. If you are accessing via OAuth, please check your tokens are correct. For public users, please ensure your table is published."
    if (!_.isString(user_id) && !_.isString(db)) throw new Error(error_text);

    var me = {
        public_user: "publicuser"
        , user_id: user_id
        , db: db
    };

    me.username = function(){
        var username = this.public_user;
        if (_.isString(this.user_id))
            username = _.template(global.settings.db_user, {user_id: this.user_id});

        return username;
    };

    me.database = function(){
        var database = db;
        if (_.isString(this.user_id))
            database = _.template(global.settings.db_base_name, {user_id: this.user_id});

        return database;
    };

    me.conString = "tcp://" + me.username() + "@" +
                    global.settings.db_host + ":" +
                    global.settings.db_port + "/" +
                    me.database();

    me.eventedQuery = function(sql, callback){
        var that = this;

        Step(
            function(){
                that.sanitize(sql, this);
            },
            function(err, clean){
                if (err) throw err;
                pg.connect(that.conString, this);
            },
            function(err, client, done){
                if (err) throw err;
                var query = client.query(sql);
                // NOTE: for some obscure reason passing "done" directly
                //       as the listener works but can be slower
                //      (by x2 factor!)
                query.on('end', function() { done(); });
                return query;
            },
            function(err, query){
                callback(err, query)
            }
        );
    },

    me.query = function(sql, callback){
        var that = this;
        var finish;

        Step(
            function(){
                that.sanitize(sql, this);
            },
            function(err, clean){
                if (err) throw err;
                pg.connect(that.conString, this);
            },
            function(err, client, done){
                if (err) throw err;
                finish = done;
                client.query(sql, this);
            },
            function(err, res){

                // Release client to the pool
                // should this be postponed to after the callback ?
                // NOTE: if we pass a true value to finish() the client
                //       will be removed from the pool.
                //       We don't want this. Not now.
                if ( finish ) finish();

                callback(err, res)
            }
        );
    };

    // throw exception if illegal operations are detected
    // NOTE: this check is weak hack, better database
    //       permissions should be used instead.
    me.sanitize = function(sql, callback){
        // NOTE: illegal table access is checked in main app
        if (sql.match(/^\s+set\s+/i)){
            var error = new SyntaxError("SET command is forbidden");
            error.http_status = 403;
            callback(error);
            return;
        }
        callback(null,true);
    };

    return me;
};

// little hack for UI
//
// TODO:drop, fix in the UI (it's not documented in doc/API)
//
PSQL.window_sql = function(sql, limit, offset) {
  // only window select functions (NOTE: "values" will be broken, "with" will be broken)
  if (!_.isNumber(limit) || !_.isNumber(offset) ) return sql;

  var cte = '';

  if ( sql.match(/^\s*WITH\s/i) ) {

    var rem = sql; // analyzed portion of sql
    var q; // quote char
    var n = 0; // nested parens level
    var s = 0; // 0:outQuote, 1:inQuote
    var l;
    while (1) {
      l = rem.search(/[("')]/);
//console.log("REM Is " + rem);
      if ( l < 0 ) {
        console.log("Malformed SQL");
        return sql;
      }
      var f = rem.charAt(l);
//console.log("n:" + n + " s:" + s + " l:" + l + " charAt(l):" + f + " charAt(l+1):" + rem.charAt(l+1));
      if ( s == 0 ) {
        if ( f == '(' ) ++n;
        else if ( f == ')' ) {
          if ( ! --n ) { // end of CTE
            cte += rem.substr(0, l+1);
            rem = rem.substr(l+1);
  //console.log("Out of cte, rem is " + rem);
            if ( rem.search(/^s*,/) < 0 ) break;
            else continue; // cte and rem already updated
          }
        }
        else { // enter quoting
          s = 1; q = f;
        }
      }
      else if ( f == q ) {
        if ( rem.charAt(l+1) == f ) ++l; // escaped
        else s = 0; // exit quoting
      }
      cte += rem.substr(0, l+1);
      rem = rem.substr(l+1);
    }
/*
console.log("cte: " + cte);
console.log("rem: " + rem);
*/
    sql = rem; //sql.substr(l+1);
  }

  if ( sql.match(/^\s*SELECT\s/i) ) {
      return cte + "SELECT * FROM (" + sql + ") AS cdbq_1 LIMIT " + limit + " OFFSET " + offset;
  }

  return cte + sql;
}

module.exports = PSQL;
<|endoftext|>


<commit_before>/*
 * grunt-html-head-urls-min-toggle
 * https://github.com/pinestec/grunt-html-head-urls-min-toggle
 *
 * Copyright (c) 2017 Michael Hartung
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function(grunt) {
	grunt
	    .registerMultiTask(
	        'html_head_urls_min_toggle',
	        'Point the html-head href and src urls to minified sources and vice versa.',
	        function() {
		        var global_functions = {};
		        global_functions = require('./html_head_urls_min_toggle__global_functions.js');
		        // global_functions.serialize_object_to_disk(this,
		        // './etc/this_sample_object.json');
		        var fileSystem_Module = global_functions.globalModule_Try('fs');
		        var currentFileHandle = fileSystem_Module.openSync("./etc/global_options.json", 'r');
		        var options_reference_object = JSON.parse(fileSystem_Module.readFileSync(currentFileHandle));
		        fileSystem_Module.closeSync(currentFileHandle);

		        var action__current_value = global_functions.casual__options_property_servant(this.data.options,
		            options_reference_object, 'action');
		        if (action__current_value) {
			        switch (action__current_value) {
			        case 'switch':
				        var file_source = global_functions.casual__options_property_servant(this.data.options,
				            options_reference_object, 'file_source');
				        if (file_source === 'list') {
					        console.log("SWITCHING A LIST...!");
				        }
				        if (file_source === 'wildcard') {
					        for (var ow = 0; ow < this.data.custom_files.length; ow++) {
						        if (this.data.custom_files[ow].hasOwnProperty('cwd') &&
						            this.data.custom_files[ow].hasOwnProperty('src')) {
							        var inner_files_array = global_functions.process_wildcard_input(this.data.custom_files[ow].src,
							            this.data.custom_files[ow].cwd);
							        for (var iw = 0; iw < inner_files_array.length; iw++) {
								        grunt.log.write("\t" + (iw + 1) + ": \"" + inner_files_array[iw] + "\"");
								        if (global_functions.toggle_all_head_links(this.data.options.direction, inner_files_array[iw])) {
									        grunt.log.write("\t- to \""['green'] + this.data.options.direction['green'] + "\" o.k."['green'] +
									            "\n");
								        } else {
									        grunt.log.write("\t- to \""['red'] + this.data.options.direction['red'] +
									            "\">>> Failed...! <<<"['red'] + "\n");
								        }
							        }
						        } else {
							        console.log("MISSING NEEDED \"WILDCARD\" FILE PROPERTIES...!");
							        break;
						        }
					        }
					        break;
				        }
				        break;
			        case 'clean':
				        console.log("SELECTED CLEAN...!");
				        break;
			        default:
				        break;
			        }
		        } else {
			        console.log("Could not derive a valid action...! ");
			        console.log("Please match your Gruntfile with the packages documentation...");
		        }
		        var reference_options_switch_object = {
		          action : [ 'switch', 'clean' ],
		          direction : [ 'min', 'regular' ],
		          file_source : [ 'wildcard', 'list' ]
		        };

		        if (global_functions.private_action_checker(this, reference_options_switch_object, false)) {
			        if (this.data.options.action === 'switch') {
				        if (this.data.options.file_source === 'list') {
					        for (var l = 0; l < this.data.custom_files.length; l++) {
						        grunt.log.write("\t" + (l + 1) + ": \"" + this.data.custom_files[l] + "\"");
						        if (global_functions.toggle_all_head_links(this.data.options.direction, this.data.custom_files[l])) {
							        grunt.log.write("\t- to \""['green'] + this.data.options.direction['green'] + "\" o.k."['green'] +
							            "\n");
						        } else {
							        grunt.log.write("\t- to \""['red'] + this.data.options.direction['red'] +
							            "\">>> Failed...! <<<"['red'] + "\n");
						        }
					        }
				        }
			        }
		        } else {

			        var reference_options_clean_object = {
			          action : [ 'clean' ],
			          file_source : [ 'wildcard', 'list' ]
			        };
			        if (global_functions.private_action_checker(this, reference_options_clean_object, false)) {
				        if (this.data.options.file_source === 'list') {
					        for (var i = 0; i < this.data.custom_files.length; i++) {
						        grunt.log.write("\t" + (i + 1) + ": \"" + this.data.custom_files[i] + "\"");
						        if (global_functions.end_of_line_refresh(this.data.custom_files[i])) {
							        grunt.log.write("\t- action \""['green'] + this.data.options.action['green'] + "\" o.k."['green'] +
							            "\n");
						        } else {
							        grunt.log.write("\t- action \""['red'] + this.data.options.action['red'] +
							            "\" >>> Failed...! <<<"['red'] + "\n");
						        }
					        }
				        } else {
					        for (var k = 0; k < this.data.custom_files.length; k++) {
						        if (this.data.custom_files[k].hasOwnProperty('cwd') && this.data.custom_files[k].hasOwnProperty('src')) {
							        var current_files_array = global_functions.process_wildcard_input(this.data.custom_files[k].src,
							            this.data.custom_files[k].cwd);
							        for (var j = 0; j < current_files_array.length; j++) {
								        grunt.log.write("\t" + (j + 1) + ": \"" + current_files_array[j] + "\"");
								        if (global_functions.end_of_line_refresh(current_files_array[j])) {
									        grunt.log.write("\t- action \""['green'] + this.data.options.action['green'] +
									            "\" o.k."['green'] + "\n");
								        } else {
									        grunt.log.write("\t- action \""['red'] + this.data.options.action['red'] +
									            "\" >>> Failed...! <<<"['red'] + "\n");
								        }
							        }
						        } else {
							        console.log("MISSING NEEDED \"WILDCARD\" FILE PROPERTIES...!");
						        }
					        }
				        }
			        }
		        }

	        }); // End of the major function...
};
<commit_msg>Added some more empty lines... - MH - PT<commit_after>/*
 * grunt-html-head-urls-min-toggle
 * https://github.com/pinestec/grunt-html-head-urls-min-toggle
 *
 * Copyright (c) 2017 Michael Hartung
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function(grunt) {
	grunt
	    .registerMultiTask(
	        'html_head_urls_min_toggle',
	        'Point the html-head href and src urls to minified sources and vice versa.',
	        function() {
		        var global_functions = {};
		        global_functions = require('./html_head_urls_min_toggle__global_functions.js');
		        // global_functions.serialize_object_to_disk(this,
		        // './etc/this_sample_object.json');
		        var fileSystem_Module = global_functions.globalModule_Try('fs');
		        var currentFileHandle = fileSystem_Module.openSync("./etc/global_options.json", 'r');
		        var options_reference_object = JSON.parse(fileSystem_Module.readFileSync(currentFileHandle));
		        fileSystem_Module.closeSync(currentFileHandle);

		        var action__current_value = global_functions.casual__options_property_servant(this.data.options,
		            options_reference_object, 'action');
		        if (action__current_value) {
			        switch (action__current_value) {
			        case 'switch':
				        var file_source = global_functions.casual__options_property_servant(this.data.options,
				            options_reference_object, 'file_source');
				        if (file_source === 'list') {
					        console.log("SWITCHING A LIST...!");
				        }
				        if (file_source === 'wildcard') {
					        for (var ow = 0; ow < this.data.custom_files.length; ow++) {
						        if (this.data.custom_files[ow].hasOwnProperty('cwd') &&
						            this.data.custom_files[ow].hasOwnProperty('src')) {
							        var inner_files_array = global_functions.process_wildcard_input(this.data.custom_files[ow].src,
							            this.data.custom_files[ow].cwd);
							        for (var iw = 0; iw < inner_files_array.length; iw++) {
								        grunt.log.write("\t" + (iw + 1) + ": \"" + inner_files_array[iw] + "\"");
								        if (global_functions.toggle_all_head_links(this.data.options.direction, inner_files_array[iw])) {
									        grunt.log.write("\t- to \""['green'] + this.data.options.direction['green'] + "\" o.k."['green'] +
									            "\n");
								        } else {
									        grunt.log.write("\t- to \""['red'] + this.data.options.direction['red'] +
									            "\">>> Failed...! <<<"['red'] + "\n");
								        }
							        }
						        } else {
							        console.log("MISSING NEEDED \"WILDCARD\" FILE PROPERTIES...!");
							        break;
						        }
					        }
					        break;
				        }
				        break;
			        case 'clean':
				        console.log("SELECTED CLEAN...!");
				        break;
			        default:
				        break;
			        }
		        } else {
			        console.log("Could not derive a valid action...! ");
			        console.log("Please match your Gruntfile with the packages documentation...");
		        }
		        var reference_options_switch_object = {
		          action : [ 'switch', 'clean' ],
		          direction : [ 'min', 'regular' ],
		          file_source : [ 'wildcard', 'list' ]
		        };

		        if (global_functions.private_action_checker(this, reference_options_switch_object, false)) {

			        if (this.data.options.action === 'switch') {
				        if (this.data.options.file_source === 'list') {

					        for (var l = 0; l < this.data.custom_files.length; l++) {
						        grunt.log.write("\t" + (l + 1) + ": \"" + this.data.custom_files[l] + "\"");
						        if (global_functions.toggle_all_head_links(this.data.options.direction, this.data.custom_files[l])) {
							        grunt.log.write("\t- to \""['green'] + this.data.options.direction['green'] + "\" o.k."['green'] +
							            "\n");
						        } else {
							        grunt.log.write("\t- to \""['red'] + this.data.options.direction['red'] +
							            "\">>> Failed...! <<<"['red'] + "\n");
						        }
					        }
				        }

			        }

		        } else {

			        var reference_options_clean_object = {
			          action : [ 'clean' ],
			          file_source : [ 'wildcard', 'list' ]
			        };
			        if (global_functions.private_action_checker(this, reference_options_clean_object, false)) {
				        if (this.data.options.file_source === 'list') {
					        for (var i = 0; i < this.data.custom_files.length; i++) {
						        grunt.log.write("\t" + (i + 1) + ": \"" + this.data.custom_files[i] + "\"");
						        if (global_functions.end_of_line_refresh(this.data.custom_files[i])) {
							        grunt.log.write("\t- action \""['green'] + this.data.options.action['green'] + "\" o.k."['green'] +
							            "\n");
						        } else {
							        grunt.log.write("\t- action \""['red'] + this.data.options.action['red'] +
							            "\" >>> Failed...! <<<"['red'] + "\n");
						        }
					        }
				        } else {
					        for (var k = 0; k < this.data.custom_files.length; k++) {
						        if (this.data.custom_files[k].hasOwnProperty('cwd') && this.data.custom_files[k].hasOwnProperty('src')) {
							        var current_files_array = global_functions.process_wildcard_input(this.data.custom_files[k].src,
							            this.data.custom_files[k].cwd);
							        for (var j = 0; j < current_files_array.length; j++) {
								        grunt.log.write("\t" + (j + 1) + ": \"" + current_files_array[j] + "\"");
								        if (global_functions.end_of_line_refresh(current_files_array[j])) {
									        grunt.log.write("\t- action \""['green'] + this.data.options.action['green'] +
									            "\" o.k."['green'] + "\n");
								        } else {
									        grunt.log.write("\t- action \""['red'] + this.data.options.action['red'] +
									            "\" >>> Failed...! <<<"['red'] + "\n");
								        }
							        }
						        } else {
							        console.log("MISSING NEEDED \"WILDCARD\" FILE PROPERTIES...!");
						        }
					        }
				        }
			        }
		        }

	        }); // End of the major function...
};
<|endoftext|>


<commit_before>var url="http://192.168.1.12:8080";

var KEY_UP = 38;
var KEY_DOWN = 40;
var canvas = null, ctx = null;

window.onload = (function() {
	var nombre = prompt("Please enter your name");

	var socket = io.connect(url);

	socket.on('connect', function() {
		socket.emit('adduser',{'nombre':nombre});
		//console.log('Connected!');
	});

	socket.on('draw', function(data) {
		//console.log(data);

		paint(data);
	});

	socket.on('disconnect', function() {
		console.log('Disconnected!');
	});

	socket.on('initData', function(data) {
		//console.log(data);
		init(data);
	});

	//Leemos que tecla estamos pulsando
	document.addEventListener('keydown', function(evt) {
		var lastPress = evt.keyCode;
		if (lastPress == KEY_UP || lastPress == KEY_DOWN) {
			//console.log("emitiendo "+lastPress);
			socket.emit('keypress', {
				'key' : lastPress
			});
		}
	}, false);

});

function init(data) {
	canvas = document.getElementById('juego');
	ctx = canvas.getContext('2d');

	setFullScreen(data);

	paint(data);
}

function setFullScreen(data) {
	canvas.width = data.width;
	canvas.height = data.height;

	var w = window.innerWidth / canvas.width;
	var h = window.innerHeight / canvas.height;
	var scale = Math.min(h, w);

	//DEFINIR ANCHO Y ALTO DEL CANVAS
	canvas.style.width = (canvas.width * scale) + 'px';
	canvas.style.height = (canvas.height * scale) + 'px';

	//CENTRAR PANTALLA
	canvas.style.position = 'fixed';
	canvas.style.left = '50%';
	canvas.style.top = '50%';
	canvas.style.marginLeft = -(canvas.width * scale) / 2 + 'px';
	canvas.style.marginTop = -(canvas.height * scale) / 2 + 'px';
}

function paint(data) {
	//FONDO
	ctx.fillStyle = '#000';
	ctx.fillRect(0, 0, canvas.width, canvas.height);

	//BARRA DEL JUGADOR 1
	ctx.fillStyle = '#fff';
	ctx.fillRect(data.p1.x, data.p1.y, data.p1.ancho, data.p1.alto);

	//BARRA DEL JUGADOR 2
	ctx.fillRect(data.p2.x, data.p2.y, data.p2.ancho, data.p2.alto);

	//BOLITA
	ctx.fillRect(data.bola.x, data.bola.y, data.bola.diametro, data.bola.diametro);

	//RED
	pintaRed();

	//DATOS
	pintaDatos(data);
}

function pintaRed()
{
	var i;
	for(i=10;i<500;i+=20)
	{
		ctx.fillRect(495,i,10,10);
	}
}

function pintaDatos(data)
{
	//GOLES J1
	ctx.font="75px Arial";
	ctx.fillText(data.p1.goles,390,75);

	//GOLES J2
	ctx.fillText(data.p2.goles,560,75);

	//USERS
	ctx.font="15px Arial";
	ctx.fillText(data.p1.nombre,10,485);
	ctx.fillText(data.p2.nombre,990-(data.p2.nombre.length)*7,485); //HAY QUE CORREGIR LA SITUACION DEL NOMBRE
}

<commit_msg>Quitados comentarios<commit_after>var url="http://192.168.1.12:8080";

var KEY_UP = 38;
var KEY_DOWN = 40;
var canvas = null, ctx = null;

window.onload = (function() {
	var nombre = prompt("Please enter your name");

	var socket = io.connect(url);

	socket.on('connect', function() {
		socket.emit('adduser',{'nombre':nombre});
	});

	socket.on('draw', function(data) {
		paint(data);
	});

	socket.on('disconnect', function() {
		console.log('Disconnected!');
	});

	socket.on('initData', function(data) {
		init(data);
	});

	//Leemos que tecla estamos pulsando
	document.addEventListener('keydown', function(evt) {
		var lastPress = evt.keyCode;
		if (lastPress == KEY_UP || lastPress == KEY_DOWN) {
			socket.emit('keypress', {
				'key' : lastPress
			});
		}
	}, false);

});

function init(data) {
	canvas = document.getElementById('juego');
	ctx = canvas.getContext('2d');

	setFullScreen(data);

	paint(data);
}

function setFullScreen(data) {
	canvas.width = data.width;
	canvas.height = data.height;

	var w = window.innerWidth / canvas.width;
	var h = window.innerHeight / canvas.height;
	var scale = Math.min(h, w);

	//DEFINIR ANCHO Y ALTO DEL CANVAS
	canvas.style.width = (canvas.width * scale) + 'px';
	canvas.style.height = (canvas.height * scale) + 'px';

	//CENTRAR PANTALLA
	canvas.style.position = 'fixed';
	canvas.style.left = '50%';
	canvas.style.top = '50%';
	canvas.style.marginLeft = -(canvas.width * scale) / 2 + 'px';
	canvas.style.marginTop = -(canvas.height * scale) / 2 + 'px';
}

function paint(data) {
	//FONDO
	ctx.fillStyle = '#000';
	ctx.fillRect(0, 0, canvas.width, canvas.height);

	//BARRA DEL JUGADOR 1
	ctx.fillStyle = '#fff';
	ctx.fillRect(data.p1.x, data.p1.y, data.p1.ancho, data.p1.alto);

	//BARRA DEL JUGADOR 2
	ctx.fillRect(data.p2.x, data.p2.y, data.p2.ancho, data.p2.alto);

	//BOLITA
	ctx.fillRect(data.bola.x, data.bola.y, data.bola.diametro, data.bola.diametro);

	//RED
	pintaRed();

	//DATOS
	pintaDatos(data);
}

function pintaRed()
{
	var i;
	for(i=10;i<500;i+=20)
	{
		ctx.fillRect(495,i,10,10);
	}
}

function pintaDatos(data)
{
	//GOLES J1
	ctx.font="75px Arial";
	ctx.fillText(data.p1.goles,390,75);

	//GOLES J2
	ctx.fillText(data.p2.goles,560,75);

	//USERS
	ctx.font="15px Arial";
	ctx.fillText(data.p1.nombre,10,485);
	ctx.fillText(data.p2.nombre,990-(data.p2.nombre.length)*7,485); //HAY QUE CORREGIR LA SITUACION DEL NOMBRE
}<|endoftext|>