<commit_before>"use strict"

var VolTextObject;
var map, annotationLayer;

var annotationChanges = 0
var selvol = "0"
var annotationClassesAndAppliers
var keyCodeActions

var recentLocations = []
var recentLocationsMaxLength = 5

$(document).ready(function() {
    // This handles selection in dataTable
    var table = $('#vol_table').DataTable();

    $('#vol_table tbody').on( 'click', 'tr', function () {
        if ( $(this).hasClass('selected') ) {
            $(this).removeClass('selected');
        }
        else {
            table.$('tr.selected').removeClass('selected');
            $(this).addClass('selected');
        }
    } );

    $('#button').click( function () {
        table.row('.selected').remove().draw( false );
    } );

    $("#col2text").on("cut copy paste", function(e) {
        e.preventDefault()
    })
    // Prevent changes in a content-editable div
    $("#col2text").on("keydown", function(e) {
        e = e || window.event;
        // Allow arrow keys, home, end, pgup, pgdn
        if (e.keyCode < 33 || e.keyCode > 40)
            e.preventDefault()
        keyCodeActions.forEach(function(action) {
            if (e.keyCode == action.code)
                action.action()
        })
    })
} );

var DeleteFeature = OpenLayers.Class(OpenLayers.Control, {
    initialize: function(layer, options) {
        OpenLayers.Control.prototype.initialize.apply(this, [options]);
        this.layer = layer;
        this.handler = new OpenLayers.Handler.Feature(
            this, layer, {click: this.clickFeature}
        );
    },
    clickFeature: function(feature) {
        // if feature doesn't have a fid, destroy it
        if(feature.fid == undefined) {
            this.layer.destroyFeatures([feature]);
        } else {
            feature.state = OpenLayers.State.DELETE;
            this.layer.events.triggerEvent("afterfeaturemodified",
                                           {feature: feature});
            feature.renderIntent = "select";
            this.layer.drawFeature(feature);
        }
    },
    setMap: function(map) {
        this.handler.setMap(map);
        OpenLayers.Control.prototype.setMap.apply(this, arguments);
    },
    CLASS_NAME: "OpenLayers.Control.DeleteFeature"
});

function getMapFeatures() {
    //var mLayers = map.layers;
    // var annLayer = map.getLayersByName("Annotations")
    var annLayer = annotationLayer

    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeatsarr = annLayer.features.map(function(feature) {
        var layer_geom = feature.geometry.transform("EPSG:900913", "EPSG:4326")
        var geoJSONText = geoJSON.write(layer_geom)
        return geoJSONText
    })
    var jsonfeats = jsonfeatsarr.join("@@")
    // window.alert(jsonfeats)
    return jsonfeats
}

function jsonToMapFeatures(jsonstr) {
    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeats = jsonstr.split("@@")
    // debugger
    var feats = jsonfeats.map(function(jsonfeat) {
        var geom = geoJSON.read(jsonfeat, "Geometry")
        var transformedGeom = geom.transform("EPSG:4326", "EPSG:900913")
        return new OpenLayers.Feature.Vector(transformedGeom)
    })
    return feats
}

function getSelectionNodes() {
    var selectionRange = getSelectionRange()
    return getRangeNodes(selectionRange, annotationClasses)
}

function getStoredMapFeatures(node) {
    return $.data(node, "features")
}

function setStoredMapFeatures(node, feats) {
    $.data(node, "features", feats)
}

function addMapFeaturesToSelection() {
    var jsonfeats = getMapFeatures()
    var rangenodes = getSelectionNodes()
    rangenodes.forEach(function(node) {
        setStoredMapFeatures(node, jsonfeats)
    })
    if (rangenodes.length > 0)
        addToRecentLocations(rangenodes[0].innerHTML.substring(0, 20), jsonfeats)
}

function zoomFeatures() {
    window.alert("Zoom Action Needed")
}

function populateRecentLocations() {
    var htmlarr = []
    for (var i = recentLocations.length - 1; i >= 0; i--) {
        var recentLoc = recentLocations[i]
        var html = '
' +
                     recentLoc.html + '
'
        console.log(html)
        htmlarr.push(html)
    }
    $('#recentlocs').html(htmlarr.join("\n"))
}

function addToRecentLocations(html, jsonfeats) {
    if (recentLocations.length >= recentLocationsMaxLength)
        recentLocations.slice(1)
    recentLocations.push({html: html, jsonfeats: jsonfeats})
    populateRecentLocations()
}

function locClicked(e) {
    alert($(e.target).attr('data-jsonfeats'))
}

// Save annotations in a serialized format.
function saveVolumeAnnotations() {
    // Fetch annotations
    var annotations = getAnnotations(annotationClasses)
    // Convert to an array of serialized annotations in the form "CLASS$START$END".
    var serialAnnotations = annotations.map(function(ann) {
        var jsonmapfeats = getStoredMapFeatures(ann.node) || ""
        return ann.node.className + "$" + ann.start + "$" + ann.end + "$" + jsonmapfeats
    })
    // Join to a single serialized string
    var serialString = serialAnnotations.join("|")
    var base64 = utf8ToB64(serialString);
    var parse_file = new Parse.File((annotUser + "-" + selvol +".txt"), { base64: base64 });
    // Save to Parse. First look for an existing entry for the user and volume.
    // If found, update it. Else create a new entry.
    var query = new Parse.Query(SpansObject)
    query.equalTo("user", annotUser)
    query.equalTo("vol", selvol)
    query.first().then(function(existing) {
        if (existing) {
            existing.set("spans", parse_file)
            return existing.save()
        } else {
            var spansObject = new SpansObject()
            return spansObject.save({"user":annotUser, "vol":selvol, "spans":parse_file})
        }
    }, savefailure("finding existing entry")
    ).then(savesuccess(function() {
        annotationChanges = 0
        logMessage("Saved " + annotations.length + " annotations")
    }),
        savefailure("saving new or updating existing entry"))
}

// Called from HTML. Save annotations. If saved successfully, reset list of
// article changes.
function saveAnnotations() {
    if (annotUser != "Default") {
        saveVolumeAnnotations()
    } else {
        logMessage("Please select a non-default Annotator Name prior prior to saving")
    }
}

// Load volume annotations
function loadVolumeAnnotations(results) {
    var textDivNode = getTextNode()
    textDivNode.normalize()
    alert(textDivNode.childNodes.length)
    var textNode = textDivNode.childNodes[0]
    httpGet(results[0].get("spans").url(), function(spansText) {
        var spansSerialized = spansText.split("|")
        var spans = spansSerialized.map(function(span) {
            var splitSpan = span.split("$")
            var className = splitSpan[0]
            var start = splitSpan[1]
            var end = splitSpan[2]
            var jsonmapfeats = splitSpan[3]
            return {start: start, end: end, className: className, jsonmapfeats: jsonmapfeats}
        })
        spans.sort(function(a, b) { return b.start - a.start })
        for (var i = 0; i < spans.length; i++) {
            var span = spans[i]
            if (span.start > textNode.length || span.end > textNode.length) {
                console.log("Skipped span [" + span.start + "," +
                    span.end + "] because > " + textNode.length)
            } else {
                var range = rangy.createRange()
                range.setStartAndEnd(textNode, span.start, span.end)
                annotationClassesAndAppliers.forEach(function(ca) {
                    if (span.className == ca.clazz) {
                        ca.applier.applyToRange(range)
                    }
                })
                getRangeNodes(range, annotationClasses).forEach(function(node) {
                    if (span.jsonmapfeats)
                        setStoredMapFeatures(node, span.jsonmapfeats)
                })
            }
        }
    })
}

function closeDialog(node) {
    $(node).dialog("close")
}

function checkVol(tableSelector) {
    if (annotUser != "Default") {
        var table = $(tableSelector).DataTable()
        var ret = table.$('tr.selected')
        if (ret.length > 0) {
            var newvol = table.$('tr.selected').find('td:first').text()
            if (annotationChanges > 0) {
                $("
Do you want to save the existing annotations?
").dialog({
                    resizable: false,
                    modal: true,
                    buttons: {
                        "Yes": function() {
                            saveAnnotations()
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "No": function() {
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "Cancel": function() {
                            console.log("Canceled")
                            window.alert("FIXME: We should set the visibly selected volume to the old one")
                            closeDialog(this)
                        }
                    }
                })
            }
            loadVolumeText(newvol, SpansObject)
        }
    } else {
        logMessage("Please select a non-default Annotator name prior to loading a volume")
    }
}

function nameChangeAnnotator() {
    var el = document.getElementById("selectUserAnnotator");
    annotUser = el.options[el.selectedIndex].innerHTML;
}

function removeAnnotations() {
    annotationLayer.destroyFeatures()
    annotationClassesAndAppliers.forEach(function(ca) {
        ca.unapplier.undoToRange(makeRange(document.body))
    })
    annotationChanges = 0
}

function removeAnnotation() {
    var selectionRange = getSelectionRange()
    if (overlapsAnnotation(selectionRange, true, annotationClasses))
        logMessage("Selection contains part of an annotation")
    else {
        annotationClassesAndAppliers.forEach(function(ca) {
            ca.unapplier.undoToSelection()
        })
    }
}

function spanClick(element) {
    //window.alert("Clicked inside article")
    var range = rangy.createRange()
    range.selectNodeContents(element)
    var sel = rangy.getSelection()
    sel.setSingleRange(range)
    var jsonfeats = getStoredMapFeatures(element)
    // alert("GeoJSON: " + jsonfeats)
    annotationLayer.destroyFeatures()
    if (jsonfeats)
        annotationLayer.addFeatures(jsonToMapFeatures(jsonfeats))
}

function annotationFeatureChanged(event) {
    addMapFeaturesToSelection()
    // var bounds = event.feature.geometry.getBounds();
    // var answer = "bottom: " + bounds.bottom + "\n";
    // answer += "left: " + bounds.left + "\n";
    // answer += "right: " + bounds.right + "\n";
    // answer += "top: " + bounds.top + "\n";
    // alert("Feature modified: " + answer);
}

function commonMapInit() {
    var extent = new OpenLayers.Bounds(
        -12003508, 3009847, -9005759, 6557774
    );

    Parse.initialize("Dxi3BvGT3mHiDC7B1YjeEuiUQKtWIeQNofT5FIIx", "QG352rxcZvLrYeV4jOCsIZvM8mIeQyhvHzDNINAb");

    VolTextObject = Parse.Object.extend("VolumeText");

    rangy.init();

    var table = $('#vol_table').DataTable()
    var rows = getVolTableRows(table)

    map = new OpenLayers.Map('map', {
        projection: new OpenLayers.Projection("EPSG:900913"),
        displayProjection: new OpenLayers.Projection("EPSG:4326"),
        controls: [
            new OpenLayers.Control.PanZoom(),
            new OpenLayers.Control.Navigation(),
            new OpenLayers.Control.MousePosition({prefix: 'Long: ', separator: '  Lat: ', numDigits: 3, emptyString:''})
        ]
    });

    var gphy = new OpenLayers.Layer.Google(
        "Google Streets"
    );

    var saveStrategy = new OpenLayers.Strategy.Save();

    annotationLayer = new OpenLayers.Layer.Vector("Annotations", {
        strategies: [new OpenLayers.Strategy.BBOX(), saveStrategy],
        projection: new OpenLayers.Projection("EPSG:4326"),
        protocol : new OpenLayers.Protocol.HTTP({
                url : 'polygons.geojson',
                format : new OpenLayers.Format.GeoJSON()
            })
    })

    map.addLayers([gphy, annotationLayer]);

    annotationLayer.events.on({
        featureadded: annotationFeatureChanged,
        featuremodified: annotationFeatureChanged,
        featureremoved: annotationFeatureChanged
    })

    var panel = new OpenLayers.Control.Panel({
        displayClass: 'customEditingToolbar',
        allowDepress: true
    });

    var draw = new OpenLayers.Control.DrawFeature(
        annotationLayer, OpenLayers.Handler.Polygon,
        {
            title: "Draw Feature",
            displayClass: "olControlDrawFeaturePolygon",
            multi: true
        }
    );

    var edit = new OpenLayers.Control.ModifyFeature(annotationLayer, {
        title: "Modify Feature",
        displayClass: "olControlModifyFeature"
    });

    var del = new DeleteFeature(annotationLayer, {title: "Delete Feature"});

    var save = new OpenLayers.Control.Button({
        title: "Save Changes",
        trigger: function() {
            if(edit.feature) {
                edit.selectControl.unselectAll();
            }
            saveStrategy.save();
            logMessage("Done Saving")
        },
        displayClass: "olControlSaveFeatures"
    });

    panel.addControls([save, del, edit, draw]);
    map.addControl(panel);
    map.zoomToExtent(extent, true);
}

// Set 4-space indentation for vi
// vi:sw=4
<commit_msg>Added improved handling of long text nodes<commit_after>"use strict"

var VolTextObject;
var map, annotationLayer;

var annotationChanges = 0
var selvol = "0"
var annotationClassesAndAppliers
var keyCodeActions

var recentLocations = []
var recentLocationsMaxLength = 10

$(document).ready(function() {
    // This handles selection in dataTable
    var table = $('#vol_table').DataTable();

    $('#vol_table tbody').on( 'click', 'tr', function () {
        if ( $(this).hasClass('selected') ) {
            $(this).removeClass('selected');
        }
        else {
            table.$('tr.selected').removeClass('selected');
            $(this).addClass('selected');
        }
    } );

    $('#button').click( function () {
        table.row('.selected').remove().draw( false );
    } );

    $("#col2text").on("cut copy paste", function(e) {
        e.preventDefault()
    })
    // Prevent changes in a content-editable div
    $("#col2text").on("keydown", function(e) {
        e = e || window.event;
        // Allow arrow keys, home, end, pgup, pgdn
        if (e.keyCode < 33 || e.keyCode > 40)
            e.preventDefault()
        keyCodeActions.forEach(function(action) {
            if (e.keyCode == action.code)
                action.action()
        })
    })
} );

function getSelectionNodes() {
    var selectionRange = getSelectionRange()
    return getRangeNodes(selectionRange, annotationClasses)
}

var DeleteFeature = OpenLayers.Class(OpenLayers.Control, {
    initialize: function(layer, options) {
        OpenLayers.Control.prototype.initialize.apply(this, [options]);
        this.layer = layer;
        this.handler = new OpenLayers.Handler.Feature(
            this, layer, {click: this.clickFeature}
        );
    },
    clickFeature: function(feature) {
        // if feature doesn't have a fid, destroy it
        if(feature.fid == undefined) {
            this.layer.destroyFeatures([feature]);
        } else {
            feature.state = OpenLayers.State.DELETE;
            this.layer.events.triggerEvent("afterfeaturemodified",
                                           {feature: feature});
            feature.renderIntent = "select";
            this.layer.drawFeature(feature);
        }
    },
    setMap: function(map) {
        this.handler.setMap(map);
        OpenLayers.Control.prototype.setMap.apply(this, arguments);
    },
    CLASS_NAME: "OpenLayers.Control.DeleteFeature"
});

function getMapFeatures() {
    //var mLayers = map.layers;
    // var annLayer = map.getLayersByName("Annotations")
    var annLayer = annotationLayer

    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeatsarr = annLayer.features.map(function(feature) {
        var layer_geom = feature.geometry.transform("EPSG:900913", "EPSG:4326")
        var geoJSONText = geoJSON.write(layer_geom)
        return geoJSONText
    })
    var jsonfeats = jsonfeatsarr.join("@@")
    // window.alert(jsonfeats)
    return jsonfeats
}

function jsonToMapFeatures(jsonstr) {
    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeats = jsonstr.split("@@")
    // debugger
    var feats = jsonfeats.map(function(jsonfeat) {
        var geom = geoJSON.read(jsonfeat, "Geometry")
        var transformedGeom = geom.transform("EPSG:4326", "EPSG:900913")
        return new OpenLayers.Feature.Vector(transformedGeom)
    })
    return feats
}

function getStoredMapFeatures(node) {
    return $.data(node, "features")
}

function setStoredMapFeatures(node, feats) {
    $.data(node, "features", feats)
}

function addMapFeaturesToSelection() {
    var jsonfeats = getMapFeatures()
    var rangenodes = getSelectionNodes()
    rangenodes.forEach(function(node) {
        setStoredMapFeatures(node, jsonfeats)
    })
    if (rangenodes.length > 0)
        addToRecentLocations(rangenodes[0].innerHTML.substring(0, 20), jsonfeats)
}

function displayMapFeatures(jsonfeats) {
    annotationLayer.destroyFeatures()
    if (jsonfeats)
        annotationLayer.addFeatures(jsonToMapFeatures(jsonfeats))
}

function populateRecentLocations() {
    var htmlarr = []
    for (var i = recentLocations.length - 1; i >= 0; i--) {
        var recentLoc = recentLocations[i]
        var html = '
' +
                     recentLoc.html + '
'
        console.log(html)
        htmlarr.push(html)
    }
    $('#recentlocs').html(htmlarr.join("\n"))
}

function addToRecentLocations(html, jsonfeats) {
    if (recentLocations.length >= recentLocationsMaxLength)
        recentLocations = recentLocations.slice(1)
    recentLocations.push({html: html, jsonfeats: jsonfeats})
    populateRecentLocations()
}

function locClicked(e) {
    var jsonfeats = unescape($(e.target).attr('data-jsonfeats'))
    alert(jsonfeats)
    displayMapFeatures(jsonfeats)
}

function zoomFeatures() {
    window.alert("Zoom Action Needed")
}

// Save annotations in a serialized format.
function saveVolumeAnnotations() {
    // Fetch annotations
    var annotations = getAnnotations(annotationClasses)
    // Convert to an array of serialized annotations in the form "CLASS$START$END".
    var serialAnnotations = annotations.map(function(ann) {
        var jsonmapfeats = getStoredMapFeatures(ann.node) || ""
        return ann.node.className + "$" + ann.start + "$" + ann.end + "$" + jsonmapfeats
    })
    // Join to a single serialized string
    var serialString = serialAnnotations.join("|")
    var base64 = utf8ToB64(serialString);
    var parse_file = new Parse.File((annotUser + "-" + selvol +".txt"), { base64: base64 });
    // Save to Parse. First look for an existing entry for the user and volume.
    // If found, update it. Else create a new entry.
    var query = new Parse.Query(SpansObject)
    query.equalTo("user", annotUser)
    query.equalTo("vol", selvol)
    query.first().then(function(existing) {
        if (existing) {
            existing.set("spans", parse_file)
            return existing.save()
        } else {
            var spansObject = new SpansObject()
            return spansObject.save({"user":annotUser, "vol":selvol, "spans":parse_file})
        }
    }, savefailure("finding existing entry")
    ).then(savesuccess(function() {
        annotationChanges = 0
        logMessage("Saved " + annotations.length + " annotations")
    }),
        savefailure("saving new or updating existing entry"))
}

// Called from HTML. Save annotations. If saved successfully, reset list of
// article changes.
function saveAnnotations() {
    if (annotUser != "Default") {
        saveVolumeAnnotations()
    } else {
        logMessage("Please select a non-default Annotator Name prior prior to saving")
    }
}

// Load volume annotations
function loadVolumeAnnotations(results) {
    var textDivNode = getTextNode()
    textDivNode.normalize()
    alert(textDivNode.childNodes.length)
    var textNode = textDivNode.childNodes[0]
    httpGet(results[0].get("spans").url(), function(spansText) {
        var spansSerialized = spansText.split("|")
        var spans = spansSerialized.map(function(span) {
            var splitSpan = span.split("$")
            var className = splitSpan[0]
            var start = splitSpan[1]
            var end = splitSpan[2]
            var jsonmapfeats = splitSpan[3]
            return {start: start, end: end, className: className, jsonmapfeats: jsonmapfeats}
        })
        spans.sort(function(a, b) { return b.start - a.start })
        for (var i = 0; i < spans.length; i++) {
            var span = spans[i]
            if (span.start > textNode.length || span.end > textNode.length) {
                console.log("Skipped span [" + span.start + "," +
                    span.end + "] because > " + textNode.length)
            } else {
                var range = rangy.createRange()
                range.setStartAndEnd(textNode, span.start, span.end)
                annotationClassesAndAppliers.forEach(function(ca) {
                    if (span.className == ca.clazz) {
                        ca.applier.applyToRange(range)
                    }
                })
                getRangeNodes(range, annotationClasses).forEach(function(node) {
                    if (span.jsonmapfeats)
                        setStoredMapFeatures(node, span.jsonmapfeats)
                })
            }
        }
    })
}

function closeDialog(node) {
    $(node).dialog("close")
}

function checkVol(tableSelector) {
    if (annotUser != "Default") {
        var table = $(tableSelector).DataTable()
        var ret = table.$('tr.selected')
        if (ret.length > 0) {
            var newvol = table.$('tr.selected').find('td:first').text()
            if (annotationChanges > 0) {
                $("
Do you want to save the existing annotations?
").dialog({
                    resizable: false,
                    modal: true,
                    buttons: {
                        "Yes": function() {
                            saveAnnotations()
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "No": function() {
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "Cancel": function() {
                            console.log("Canceled")
                            window.alert("FIXME: We should set the visibly selected volume to the old one")
                            closeDialog(this)
                        }
                    }
                })
            }
            loadVolumeText(newvol, SpansObject)
        }
    } else {
        logMessage("Please select a non-default Annotator name prior to loading a volume")
    }
}

function nameChangeAnnotator() {
    var el = document.getElementById("selectUserAnnotator");
    annotUser = el.options[el.selectedIndex].innerHTML;
}

function removeAnnotations() {
    annotationLayer.destroyFeatures()
    annotationClassesAndAppliers.forEach(function(ca) {
        ca.unapplier.undoToRange(makeRange(document.body))
    })
    annotationChanges = 0
}

function removeAnnotation() {
    var selectionRange = getSelectionRange()
    if (overlapsAnnotation(selectionRange, true, annotationClasses))
        logMessage("Selection contains part of an annotation")
    else {
        annotationClassesAndAppliers.forEach(function(ca) {
            ca.unapplier.undoToSelection()
        })
    }
}

function spanClick(element) {
    //window.alert("Clicked inside article")
    var range = rangy.createRange()
    range.selectNodeContents(element)
    var sel = rangy.getSelection()
    sel.setSingleRange(range)
    var jsonfeats = getStoredMapFeatures(element)
    // alert("GeoJSON: " + jsonfeats)
    displayMapFeatures(jsonfeats)
}

function annotationFeatureChanged(event) {
    addMapFeaturesToSelection()
    // var bounds = event.feature.geometry.getBounds();
    // var answer = "bottom: " + bounds.bottom + "\n";
    // answer += "left: " + bounds.left + "\n";
    // answer += "right: " + bounds.right + "\n";
    // answer += "top: " + bounds.top + "\n";
    // alert("Feature modified: " + answer);
}

function annotationFeatureAdded(event) {
    console.log("annotationFeatureAdded " + event)
    annotationFeatureChanged(event)
}

function annotationFeatureModified(event) {
    console.log("annotationFeatureModified " + event)
    annotationFeatureChanged(event)
}

function annotationFeatureRemoved(event) {
    console.log("annotationFeatureRemoved " + event)
    annotationFeatureChanged(event)
}

function commonMapInit() {
    var extent = new OpenLayers.Bounds(
        -12003508, 3009847, -9005759, 6557774
    );

    Parse.initialize("Dxi3BvGT3mHiDC7B1YjeEuiUQKtWIeQNofT5FIIx", "QG352rxcZvLrYeV4jOCsIZvM8mIeQyhvHzDNINAb");

    VolTextObject = Parse.Object.extend("VolumeText");

    rangy.init();

    var table = $('#vol_table').DataTable()
    var rows = getVolTableRows(table)

    map = new OpenLayers.Map('map', {
        projection: new OpenLayers.Projection("EPSG:900913"),
        displayProjection: new OpenLayers.Projection("EPSG:4326"),
        controls: [
            new OpenLayers.Control.PanZoom(),
            new OpenLayers.Control.Navigation(),
            new OpenLayers.Control.MousePosition({prefix: 'Long: ', separator: '  Lat: ', numDigits: 3, emptyString:''})
        ]
    });

    var gphy = new OpenLayers.Layer.Google(
        "Google Streets"
    );

    var saveStrategy = new OpenLayers.Strategy.Save();

    annotationLayer = new OpenLayers.Layer.Vector("Annotations", {
        strategies: [new OpenLayers.Strategy.BBOX(), saveStrategy],
        projection: new OpenLayers.Projection("EPSG:4326"),
        protocol : new OpenLayers.Protocol.HTTP({
                url : 'polygons.geojson',
                format : new OpenLayers.Format.GeoJSON()
            })
    })

    map.addLayers([gphy, annotationLayer]);

    annotationLayer.events.on({
        featureadded: annotationFeatureAdded,
        featuremodified: annotationFeatureModified,
        featureremoved: annotationFeatureRemoved
    })

    var panel = new OpenLayers.Control.Panel({
        displayClass: 'customEditingToolbar',
        allowDepress: true
    });

    var draw = new OpenLayers.Control.DrawFeature(
        annotationLayer, OpenLayers.Handler.Polygon,
        {
            title: "Draw Feature",
            displayClass: "olControlDrawFeaturePolygon",
            multi: true
        }
    );

    var edit = new OpenLayers.Control.ModifyFeature(annotationLayer, {
        title: "Modify Feature",
        displayClass: "olControlModifyFeature"
    });

    var del = new DeleteFeature(annotationLayer, {title: "Delete Feature"});

    var save = new OpenLayers.Control.Button({
        title: "Save Changes",
        trigger: function() {
            if(edit.feature) {
                edit.selectControl.unselectAll();
            }
            saveStrategy.save();
            logMessage("Done Saving")
        },
        displayClass: "olControlSaveFeatures"
    });

    panel.addControls([save, del, edit, draw]);
    map.addControl(panel);
    map.zoomToExtent(extent, true);
}

// Set 4-space indentation for vi
// vi:sw=4
<|endoftext|>


<commit_before>var express = require('express'),
    http = require('http'),
    path = require('path'),
    git = require('git-rev'),
    spawn = require('child_process').spawn,
    crypto = require('crypto'),
    fs = require('fs');

var app = express(),
    tag;

app.configure(function(){
    app.set('port', process.env.PORT || 3000);
    app.use(express.logger());
    app.use(express.compress());
});

git.tag(function (str) {
    tag = str;
});

var bundleBuilder = function(req, res, min){
    var version = req.params.version || tag;
    var query = req.query;

    var filename;
    if (Object.keys(query).length > 0) {
        // User want a custom bundle
        var patterns = Object.keys(query).sort().join('&'),
            shasum = crypto.createHash('sha1');
        shasum.update(tag + '?' + patterns);

        var hex = shasum.digest('hex').substr(0,8);
        filename = 'patterns-' + version + '-' + hex + (min?'.min':'') + '.js';
    } else {
        filename = 'patterns-' + version + (min?'.min':'') + '.js';
    }

    // Generate args that are passed to jam
    var fullname = path.join(__dirname, 'bundles', filename);

    // The CLI interface is:
    // jam compile -i pat1 [-i pat2 [-i pat3]] -o outfile
    var args = [ 'compile' ].concat(Object.keys(query).sort())
        .join(' -i ').split(' ').concat(['-o', fullname, '-v']);

    if (!min)
        args.push('--no-minify');

    if (!fs.existsSync(fullname)) {
        // Not in cache, so generate
        var p = spawn('jam', args);

        p.on('exit', function(code) {
            if (code === 0) {
                return res.download(fullname);
            } else {
                res.send('Something went wrong build the package');
            }
        });
        return;
    }
    return res.download(fullname);
};

app.get('/getBundle', function(req, res){
    var minify = req.query.minify === 'minify';
    delete req.query.minify;
    bundleBuilder(req, res, minify);
});

app.get('/bundles/:name', function(req, res){
    var fullname = path.join(__dirname, 'bundles', req.params.name);
    return res.download(fullname);
});

app.use(express.static(path.join(__dirname)));

http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
});

<commit_msg>adjust port number<commit_after>var express = require('express'),
    http = require('http'),
    path = require('path'),
    git = require('git-rev'),
    spawn = require('child_process').spawn,
    crypto = require('crypto'),
    fs = require('fs');

var app = express(),
    tag;

app.configure(function(){
    app.set('port', process.env.PORT || 2652);
    app.use(express.logger());
    app.use(express.compress());
});

git.tag(function (str) {
    tag = str;
});

var bundleBuilder = function(req, res, min){
    var version = req.params.version || tag;
    var query = req.query;

    var filename;
    if (Object.keys(query).length > 0) {
        // User want a custom bundle
        var patterns = Object.keys(query).sort().join('&'),
            shasum = crypto.createHash('sha1');
        shasum.update(tag + '?' + patterns);

        var hex = shasum.digest('hex').substr(0,8);
        filename = 'patterns-' + version + '-' + hex + (min?'.min':'') + '.js';
    } else {
        filename = 'patterns-' + version + (min?'.min':'') + '.js';
    }

    // Generate args that are passed to jam
    var fullname = path.join(__dirname, 'bundles', filename);

    // The CLI interface is:
    // jam compile -i pat1 [-i pat2 [-i pat3]] -o outfile
    var args = [ 'compile' ].concat(Object.keys(query).sort())
        .join(' -i ').split(' ').concat(['-o', fullname, '-v']);

    if (!min)
        args.push('--no-minify');

    if (!fs.existsSync(fullname)) {
        // Not in cache, so generate
        var p = spawn('jam', args);

        p.on('exit', function(code) {
            if (code === 0) {
                return res.download(fullname);
            } else {
                res.send('Something went wrong build the package');
            }
        });
        return;
    }
    return res.download(fullname);
};

app.get('/getBundle', function(req, res){
    var minify = req.query.minify === 'minify';
    delete req.query.minify;
    bundleBuilder(req, res, minify);
});

app.get('/bundles/:name', function(req, res){
    var fullname = path.join(__dirname, 'bundles', req.params.name);
    return res.download(fullname);
});

app.use(express.static(path.join(__dirname)));

http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
});

<|endoftext|>


<commit_before>var queryUtils = require('../../utils/query-utils');
const AggregationMapConfig = require('../../models/aggregation/aggregation-mapconfig');
var SubstitutionTokens = require('../../utils/substitution-tokens');

// Instantiate a query with tokens for a given zoom level
function queryForZoom(sql, zoom, singleTile=false) {
    const tileRes = 256;
    const wmSize = 6378137.0*2*Math.PI;
    const nTiles = Math.pow(2, zoom);
    const tileSize = wmSize / nTiles;
    const resolution = tileSize / tileRes;
    const scaleDenominator = resolution / 0.00028;
    const x0 = -wmSize/2, y0 = -wmSize/2;
    let bbox = `ST_MakeEnvelope(${x0}, ${y0}, ${x0+wmSize}, ${y0+wmSize})`;
    if (singleTile) {
        bbox = `ST_MakeEnvelope(${x0}, ${y0}, ${x0 + tileSize}, ${y0 + tileSize})`;
    }
    return SubstitutionTokens.replace(sql, {
        bbox: bbox,
        scale_denominator: scaleDenominator,
        pixel_width: resolution,
        pixel_height: resolution
    });
}

function MapnikLayerStats () {
    this._types = {
        mapnik: true,
        cartodb: true
    };
}

MapnikLayerStats.prototype.is = function (type) {
    return this._types[type] ? this._types[type] : false;
};

function queryPromise(dbConnection, query, adaptResults, errorHandler) {
    return new Promise(function(resolve, reject) {
        dbConnection.query(query, function (err, res) {
            if (err) {
                if (errorHandler) {
                    resolve(errorHandler(err));
                }
                else {
                    reject(err);
                }
            }
            else {
                resolve(adaptResults(res));
            }
        });

    });
}

function columnAggregations(field) {
    if (field.type === 'number') {
        return ['min', 'max', 'avg', 'sum'];
    }
    if (field.type === 'date') { // TODO other types too?
        return ['min', 'max'];
    }
    return [];
}

function _getSQL(ctx, query, type='pre', zoom=0) {
    let sql;
    if (type === 'pre') {
        sql = ctx.preQuery;
    }
    else {
        sql = ctx.aggrQuery;
    }
    sql = queryForZoom(sql, zoom || 0);
    return query(sql);
}

function _estimatedFeatureCount(ctx) {
    return queryPromise(
        ctx.dbConnection,
        _getSQL(ctx, queryUtils.getQueryRowEstimation),
        res => ({ estimatedFeatureCount: res.rows[0].rows }),
        () => ({ estimatedFeatureCount: -1 })
    );
}

function _featureCount(ctx) {
    if (ctx.metaOptions.featureCount) {
        // TODO: if ctx.metaOptions.columnStats we can combine this with column stats query
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, queryUtils.getQueryActualRowCount),
            res => ({ featureCount: res.rows[0].rows })
        );
    }
    return Promise.resolve();
}

function _aggrFeatureCount(ctx) {
    if (ctx.metaOptions.hasOwnProperty('aggrFeatureCount')) {
        // We expect as zoom level as the value of aggrFeatureCount
        // TODO: it'd be nice to admit an array of zoom levels to
        // return metadata for multiple levels.
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, queryUtils.getQueryActualRowCount, 'post',  ctx.metaOptions.aggrFeatureCount),
            res => ({ aggrFeatureCount: res.rows[0].rows })
        );
    }
    return Promise.resolve();
}

function _geometryType(ctx) {
    if (ctx.metaOptions.geometryType) {
        const geometryColumn = AggregationMapConfig.getAggregationGeometryColumn();
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn)),
            res => ({ geometryType: res.rows[0].geom_type })
        );
    }
    return Promise.resolve();
}

function _columns(ctx) {
    if (ctx.metaOptions.columns || ctx.metaOptions.columnStats) {
        // note: post-aggregation columns are in layer.options.columns when aggregation is present
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0)),
            res => formatResultFields(ctx.dbConnection, res.fields)
        );
    }
    return Promise.resolve();
}

// combine a list of results merging the properties of all the objects
// undefined results are admitted and ignored
function mergeResults(results) {
    if (results) {
        if (results.length === 0) {
            return {};
        }
        return results.reduce((a, b) => {
            if (a === undefined) {
                return b;
            }
            if (b === undefined) {
                return a;
            }
            return Object.assign({}, a, b);
        });
    }
}

// deeper (1 level) combination of a list of objects:
// mergeColumns([{ col1: { a: 1 }, col2: { a: 2 } }, { col1: { b: 3 } }]) => { col1: { a: 1, b: 3 }, col2: { a: 2 } }
function mergeColumns(results) {
    if (results) {
        if (results.length === 0) {
            return {};
        }
        return results.reduce((a, b) => {
            let c = Object.assign({}, b || {}, a || {});
            Object.keys(c).forEach(key => {
                if (b.hasOwnProperty(key)) {
                    c[key] = Object.assign(c[key], b[key]);
                }
            });
            return c;
        });
    }
}

function _sample(ctx, numRows) {
    if (ctx.metaOptions.sample) {
        const sampleProb = Math.min(ctx.metaOptions.sample / numRows, 1);
        // We'll use a safety limit just in case numRows is a bad estimate
        const limit = Math.ceil(ctx.metaOptions.sample * 1.5);
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, sql => queryUtils.getQuerySample(sql, sampleProb, limit)),
            res => ({ sample: res.rows })
        );
    }
    return Promise.resolve();
}

function _columnStats(ctx, columns) {
    if (!columns) {
        return Promise.resolve();
    }
    if (ctx.metaOptions.columnStats) {
        let queries = [];
        let aggr = [];
        queries.push(new Promise(resolve => resolve(columns))); // add columns as first result
        Object.keys(columns).forEach(name => {
            aggr = aggr.concat(
                columnAggregations(columns[name])
                .map(fn => `${fn}(${name}) AS ${name}_${fn}`)
            );
            if (columns[name].type === 'string') {
                const topN = ctx.metaOptions.columnStats.topCategories || 1024;
                let includeNulls = true;
                if (ctx.metaOptions.columnStats.hasOwnProperty('includeNulls')) {
                    includeNulls = ctx.metaOptions.columnStats.includeNulls;
                }

                // TODO:  ctx.metaOptions.columnStats.maxCategories
                //        => use PG stats to dismiss columns with more distinct values
                queries.push(
                    queryPromise(
                        ctx.dbConnection,
                        _getSQL(ctx, sql => queryUtils.getQueryTopCategories(sql, name, topN, includeNulls)),
                        res => ({ [name]: { categories: res.rows } })
                    )
                );
            }
        });
        queries.push(
            queryPromise(
                ctx.dbConnection,
                _getSQL(ctx, sql => `SELECT ${aggr.join(',')} FROM (${sql}) AS __cdb_query`),
                res => {
                    let stats = {};
                    Object.keys(columns).forEach(name => {
                        stats[name] = {};
                        columnAggregations(columns[name]).forEach(fn => {
                            stats[name][fn] = res.rows[0][`${name}_${fn}`];
                        });
                    });
                    return stats;
                }
            )
        );
        return Promise.all(queries).then(results => ({ columns: mergeColumns(results) }));
    }
    return Promise.resolve({ columns });
}

// This is adapted from SQL API:
function fieldType(cname) {
    let tname;
    switch (true) {
        case /bool/.test(cname):
            tname = 'boolean';
            break;
        case /int|float|numeric/.test(cname):
            tname = 'number';
            break;
        case /text|char|unknown/.test(cname):
            tname = 'string';
            break;
        case /date|time/.test(cname):
            tname = 'date';
            break;
        default:
            tname = cname;
    }
    if ( tname && cname.match(/^_/) ) {
        tname += '[]';
    }
    return tname;
}

// columns are returned as an object { columnName1: { type1: ...}, ..}
// for consistency with SQL API
function formatResultFields(dbConnection, fields = []) {
    let nfields = {};
    for (let field of fields) {
      const cname = dbConnection.typeName(field.dataTypeID);
      let tname;
      if ( ! cname ) {
        tname = 'unknown(' + field.dataTypeID + ')';
      } else {
        tname = fieldType(cname);
      }
      nfields[field.name] = { type: tname };
    }
    return nfields;
}

MapnikLayerStats.prototype.getStats =
function (layer, dbConnection, callback) {
    let aggrQuery = layer.options.sql;
    let preQuery =  layer.options.sql_raw || aggrQuery;

    let ctx = {
        dbConnection,
        preQuery,
        aggrQuery,
        metaOptions: layer.options.metadata || {}
    };

    // TODO: could save some queries if queryUtils.getAggregationMetadata() has been used and kept somewhere
    // we would set queries.results.estimatedFeatureCount and queries.results.geometryType
    // (if metaOptions.geometryType) from it.

    // TODO: compute _sample with _featureCount when available

    Promise.all([
        _estimatedFeatureCount(ctx).then(
            ({ estimatedFeatureCount }) => _sample(ctx, estimatedFeatureCount)
                .then(sampleResults => mergeResults([sampleResults, { estimatedFeatureCount }]))
        ),
        _featureCount(ctx),
        _aggrFeatureCount(ctx),
        _geometryType(ctx),
        _columns(ctx).then(columns => _columnStats(ctx, columns))
    ]).then(results => {
        callback(null, mergeResults(results));
    }).catch(error => {
        callback(error);
    });
};

module.exports = MapnikLayerStats;
<commit_msg>Small change to use const<commit_after>var queryUtils = require('../../utils/query-utils');
const AggregationMapConfig = require('../../models/aggregation/aggregation-mapconfig');
var SubstitutionTokens = require('../../utils/substitution-tokens');

// Instantiate a query with tokens for a given zoom level
function queryForZoom(sql, zoom, singleTile=false) {
    const tileRes = 256;
    const wmSize = 6378137.0*2*Math.PI;
    const nTiles = Math.pow(2, zoom);
    const tileSize = wmSize / nTiles;
    const resolution = tileSize / tileRes;
    const scaleDenominator = resolution / 0.00028;
    const x0 = -wmSize/2, y0 = -wmSize/2;
    let bbox = `ST_MakeEnvelope(${x0}, ${y0}, ${x0+wmSize}, ${y0+wmSize})`;
    if (singleTile) {
        bbox = `ST_MakeEnvelope(${x0}, ${y0}, ${x0 + tileSize}, ${y0 + tileSize})`;
    }
    return SubstitutionTokens.replace(sql, {
        bbox: bbox,
        scale_denominator: scaleDenominator,
        pixel_width: resolution,
        pixel_height: resolution
    });
}

function MapnikLayerStats () {
    this._types = {
        mapnik: true,
        cartodb: true
    };
}

MapnikLayerStats.prototype.is = function (type) {
    return this._types[type] ? this._types[type] : false;
};

function queryPromise(dbConnection, query, adaptResults, errorHandler) {
    return new Promise(function(resolve, reject) {
        dbConnection.query(query, function (err, res) {
            if (err) {
                if (errorHandler) {
                    resolve(errorHandler(err));
                }
                else {
                    reject(err);
                }
            }
            else {
                resolve(adaptResults(res));
            }
        });

    });
}

function columnAggregations(field) {
    if (field.type === 'number') {
        return ['min', 'max', 'avg', 'sum'];
    }
    if (field.type === 'date') { // TODO other types too?
        return ['min', 'max'];
    }
    return [];
}

function _getSQL(ctx, query, type='pre', zoom=0) {
    let sql;
    if (type === 'pre') {
        sql = ctx.preQuery;
    }
    else {
        sql = ctx.aggrQuery;
    }
    sql = queryForZoom(sql, zoom || 0);
    return query(sql);
}

function _estimatedFeatureCount(ctx) {
    return queryPromise(
        ctx.dbConnection,
        _getSQL(ctx, queryUtils.getQueryRowEstimation),
        res => ({ estimatedFeatureCount: res.rows[0].rows }),
        () => ({ estimatedFeatureCount: -1 })
    );
}

function _featureCount(ctx) {
    if (ctx.metaOptions.featureCount) {
        // TODO: if ctx.metaOptions.columnStats we can combine this with column stats query
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, queryUtils.getQueryActualRowCount),
            res => ({ featureCount: res.rows[0].rows })
        );
    }
    return Promise.resolve();
}

function _aggrFeatureCount(ctx) {
    if (ctx.metaOptions.hasOwnProperty('aggrFeatureCount')) {
        // We expect as zoom level as the value of aggrFeatureCount
        // TODO: it'd be nice to admit an array of zoom levels to
        // return metadata for multiple levels.
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, queryUtils.getQueryActualRowCount, 'post',  ctx.metaOptions.aggrFeatureCount),
            res => ({ aggrFeatureCount: res.rows[0].rows })
        );
    }
    return Promise.resolve();
}

function _geometryType(ctx) {
    if (ctx.metaOptions.geometryType) {
        const geometryColumn = AggregationMapConfig.getAggregationGeometryColumn();
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn)),
            res => ({ geometryType: res.rows[0].geom_type })
        );
    }
    return Promise.resolve();
}

function _columns(ctx) {
    if (ctx.metaOptions.columns || ctx.metaOptions.columnStats) {
        // note: post-aggregation columns are in layer.options.columns when aggregation is present
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0)),
            res => formatResultFields(ctx.dbConnection, res.fields)
        );
    }
    return Promise.resolve();
}

// combine a list of results merging the properties of all the objects
// undefined results are admitted and ignored
function mergeResults(results) {
    if (results) {
        if (results.length === 0) {
            return {};
        }
        return results.reduce((a, b) => {
            if (a === undefined) {
                return b;
            }
            if (b === undefined) {
                return a;
            }
            return Object.assign({}, a, b);
        });
    }
}

// deeper (1 level) combination of a list of objects:
// mergeColumns([{ col1: { a: 1 }, col2: { a: 2 } }, { col1: { b: 3 } }]) => { col1: { a: 1, b: 3 }, col2: { a: 2 } }
function mergeColumns(results) {
    if (results) {
        if (results.length === 0) {
            return {};
        }
        return results.reduce((a, b) => {
            let c = Object.assign({}, b || {}, a || {});
            Object.keys(c).forEach(key => {
                if (b.hasOwnProperty(key)) {
                    c[key] = Object.assign(c[key], b[key]);
                }
            });
            return c;
        });
    }
}

function _sample(ctx, numRows) {
    if (ctx.metaOptions.sample) {
        const sampleProb = Math.min(ctx.metaOptions.sample / numRows, 1);
        // We'll use a safety limit just in case numRows is a bad estimate
        const limit = Math.ceil(ctx.metaOptions.sample * 1.5);
        return queryPromise(
            ctx.dbConnection,
            _getSQL(ctx, sql => queryUtils.getQuerySample(sql, sampleProb, limit)),
            res => ({ sample: res.rows })
        );
    }
    return Promise.resolve();
}

function _columnStats(ctx, columns) {
    if (!columns) {
        return Promise.resolve();
    }
    if (ctx.metaOptions.columnStats) {
        let queries = [];
        let aggr = [];
        queries.push(new Promise(resolve => resolve(columns))); // add columns as first result
        Object.keys(columns).forEach(name => {
            aggr = aggr.concat(
                columnAggregations(columns[name])
                .map(fn => `${fn}(${name}) AS ${name}_${fn}`)
            );
            if (columns[name].type === 'string') {
                const topN = ctx.metaOptions.columnStats.topCategories || 1024;
                const includeNulls = ctx.metaOptions.columnStats.hasOwnProperty('includeNulls') ?
                    ctx.metaOptions.columnStats.includeNulls :
                    true;

                // TODO:  ctx.metaOptions.columnStats.maxCategories
                //        => use PG stats to dismiss columns with more distinct values
                queries.push(
                    queryPromise(
                        ctx.dbConnection,
                        _getSQL(ctx, sql => queryUtils.getQueryTopCategories(sql, name, topN, includeNulls)),
                        res => ({ [name]: { categories: res.rows } })
                    )
                );
            }
        });
        queries.push(
            queryPromise(
                ctx.dbConnection,
                _getSQL(ctx, sql => `SELECT ${aggr.join(',')} FROM (${sql}) AS __cdb_query`),
                res => {
                    let stats = {};
                    Object.keys(columns).forEach(name => {
                        stats[name] = {};
                        columnAggregations(columns[name]).forEach(fn => {
                            stats[name][fn] = res.rows[0][`${name}_${fn}`];
                        });
                    });
                    return stats;
                }
            )
        );
        return Promise.all(queries).then(results => ({ columns: mergeColumns(results) }));
    }
    return Promise.resolve({ columns });
}

// This is adapted from SQL API:
function fieldType(cname) {
    let tname;
    switch (true) {
        case /bool/.test(cname):
            tname = 'boolean';
            break;
        case /int|float|numeric/.test(cname):
            tname = 'number';
            break;
        case /text|char|unknown/.test(cname):
            tname = 'string';
            break;
        case /date|time/.test(cname):
            tname = 'date';
            break;
        default:
            tname = cname;
    }
    if ( tname && cname.match(/^_/) ) {
        tname += '[]';
    }
    return tname;
}

// columns are returned as an object { columnName1: { type1: ...}, ..}
// for consistency with SQL API
function formatResultFields(dbConnection, fields = []) {
    let nfields = {};
    for (let field of fields) {
      const cname = dbConnection.typeName(field.dataTypeID);
      let tname;
      if ( ! cname ) {
        tname = 'unknown(' + field.dataTypeID + ')';
      } else {
        tname = fieldType(cname);
      }
      nfields[field.name] = { type: tname };
    }
    return nfields;
}

MapnikLayerStats.prototype.getStats =
function (layer, dbConnection, callback) {
    let aggrQuery = layer.options.sql;
    let preQuery =  layer.options.sql_raw || aggrQuery;

    let ctx = {
        dbConnection,
        preQuery,
        aggrQuery,
        metaOptions: layer.options.metadata || {}
    };

    // TODO: could save some queries if queryUtils.getAggregationMetadata() has been used and kept somewhere
    // we would set queries.results.estimatedFeatureCount and queries.results.geometryType
    // (if metaOptions.geometryType) from it.

    // TODO: compute _sample with _featureCount when available

    Promise.all([
        _estimatedFeatureCount(ctx).then(
            ({ estimatedFeatureCount }) => _sample(ctx, estimatedFeatureCount)
                .then(sampleResults => mergeResults([sampleResults, { estimatedFeatureCount }]))
        ),
        _featureCount(ctx),
        _aggrFeatureCount(ctx),
        _geometryType(ctx),
        _columns(ctx).then(columns => _columnStats(ctx, columns))
    ]).then(results => {
        callback(null, mergeResults(results));
    }).catch(error => {
        callback(error);
    });
};

module.exports = MapnikLayerStats;
<|endoftext|>


<commit_before>import React from 'react';
import { STATISTIC_ORDER } from 'Main/StatisticBox';
import ExpandableStatisticBox from 'Main/ExpandableStatisticBox';
import SpellIcon from 'common/SpellIcon';
import { formatNumber, formatPercentage } from 'common/format';

import SPELLS from 'common/SPELLS';
import Analyzer from 'Parser/Core/Analyzer';
import { ABILITIES_THAT_TRIGGER_MASTERY } from '../../Constants';

class MasteryBreakdown extends Analyzer {
  _tickMode = {};
  _healValByTargetId = {};
  _maxHealVal = {};
  _masteryActive = {};
  effectiveHealDist = {};
  effectiveOverhealDist = {};
  _eHDbyPlayer = {}; // for potential future features

  healing = 0;

  effectiveHealDistPerc = [];

  on_finished() {
    // There's likely a far better way to do this, but my 2AM brain couldn't find it
    let total = 0;
    delete this.effectiveHealDist[SPELLS.RENEW.id]; // see the comment at line ~150 for why this happens

    Object.keys(this.effectiveHealDist).forEach((spell) => {
      total += this.effectiveHealDist[spell];
    });

    const eHDPerc = {};
    Object.keys(this.effectiveHealDist).forEach((spell) => {
      eHDPerc[spell] = this.effectiveHealDist[spell] / total;
    });

    const eOHD = {};
    Object.keys(this.effectiveHealDist).forEach((spell) => {
      eOHD[spell] = this.effectiveOverhealDist[spell] / (this.effectiveHealDist[spell] + this.effectiveOverhealDist[spell]);
    });

    // Since JS objects lack order, we need to convert our dictionary to an array
    // to allow for it be ordered for display
    const eHDPArray = Object.keys(eHDPerc).map(spell => [spell, eHDPerc[spell], eOHD[spell]]);
    eHDPArray.sort((o1, o2) => o2[1] - o1[1]);

    this.effectiveHealDistPerc = eHDPArray;
  }

  on_byPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    const tId = event.targetID;
    if (spellId !== SPELLS.ECHO_OF_LIGHT.id) {
      return;
    }

    this._masteryActive[tId] = true;
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    const tId = event.targetID;
    if (spellId !== SPELLS.ECHO_OF_LIGHT.id) {
      return;
    }

    this._masteryActive[tId] = false;
    this._healValByTargetId[tId] = {};
    this._maxHealVal[tId] = {};
  }

  on_byPlayer_heal(event) {
    const spellId = event.ability.guid;
    const tId = event.targetID;
    if (spellId === SPELLS.ECHO_OF_LIGHT.id) {
      // logic for eol itself
      this.healing += (event.amount + (event.absorbed || 0));
      this.effectiveHealDist = this.effectiveHealDist || {};

      const percH = (event.amount + (event.absorbed || 0)) / (event.amount + (event.absorbed || 0) + (event.overheal || 0));
      const tickMode = this._tickMode[tId];

      if (this._healValByTargetId[tId] === undefined) {
        // If we have a spell that triggers Echo of Light, but is not known of in CONSTANTS.js, then we
        // will potentially end up with a situation where that EoL ticks but there is nothing stored in the
        // EoL data table. This fixes that issue.
        return;
      }

      Object.keys(this._healValByTargetId[tId]).forEach((spell) => {
        // For potential future Features //
        // if (spell in this._eHDbyPlayer[tId]) {
        //   this._eHDbyPlayer[tId][spell] += this._maxHealVal[tId][spell] * percOH / tickMode;
        // } else {
        //   this._eHDbyPlayer[tId][spell] = this._maxHealVal[tId][spell] * percOH / tickMode;
        // }
        // ----------------------------- //

        if (this.effectiveHealDist[spell] !== undefined) {
          this.effectiveHealDist[spell] += (this._maxHealVal[tId][spell]) * percH / tickMode;
          this.effectiveOverhealDist[spell] += (this._maxHealVal[tId][spell]) * (1 - percH) / tickMode;
        } else {
          this.effectiveHealDist[spell] = (this._maxHealVal[tId][spell]) * percH / tickMode;
          this.effectiveOverhealDist[spell] = (this._maxHealVal[tId][spell]) * (1 - percH) / tickMode;
        }

        this._healValByTargetId[tId][spell] -= (this._maxHealVal[tId][spell] / tickMode);

        // There's a rare issue that can cause the healVal allocation to dip to a negative value
        // I'm not sure of the cause but it seems like it is due to an incorrect tickMode
        // value. Likely something to do with events on the same timestamp (or just some bad
        // logic in this file) but the value is much more accurate with this tweak
        if (this._healValByTargetId[tId][spell] < 0) {
          this._healValByTargetId[tId][spell] = 0;
        }
      });
    } else {
      // logic for eol triggering spells
      if (!ABILITIES_THAT_TRIGGER_MASTERY.includes(spellId)) {
        return;
      }

      if (this._masteryActive[tId]) {
        this._tickMode[tId] = 3;
      } else {
        this._tickMode[tId] = 2;
      }

      if (this._healValByTargetId[tId] === undefined) {
        this._healValByTargetId[tId] = {};
      }

      if (this._maxHealVal[tId] === undefined) {
        this._maxHealVal[tId] = {};
      }

      let effectiveHealing = event.amount + (event.absorbed || 0) + (event.overheal || 0);
      if (spellId === SPELLS.RENEW.id) {
        // due to renew only applying off of initial tick and not from periodic ticks
        // and both of them having identical spell IDs, I chose to negate renew's mastery
        // value since the current meta leads us towards never hard-casting renew anyway
        // which means the mastery value for renew would be miniscule anyway. We uninclude
        // the value in output for this reason as well.

        // (but I want to include it anyway, that way I can get the "unknown spell" warning to
        //  work optimally and let users report unknown spells)

        effectiveHealing = 0;
      }

      if (this._healValByTargetId[tId][spellId] === undefined) {
        this._healValByTargetId[tId][spellId] = effectiveHealing;
      } else {
        this._healValByTargetId[tId][spellId] += effectiveHealing;
      }
      this._maxHealVal[tId][spellId] = this._healValByTargetId[tId][spellId];
    }
  }

  statistic() {
    const percOfTotalHealingDone = this.owner.getPercentageOfTotalHealingDone(this.healing);
    return (
      <ExpandableStatisticBox
        icon={<SpellIcon id={SPELLS.ECHO_OF_LIGHT.id} />}
        value={`${formatNumber(this.healing)}`}
        label={(
          <dfn data-tip={`Echo of Light healing breakdown. As our mastery is often very finicky, this could end up wrong in various situations. Please report any logs that seem strange to @enragednuke on the WoWAnalyzer discord.

            Please do note this is not 100% accurate. It is probably around 90% accurate.

            Also, a mastery value can be more than just "healing done times mastery percent" because Echo of Light is based off raw healing. If the heal itself overheals, but the mastery does not, it can surpass that assumed "limit". Don't use this as a reason for a "strange log" unless something is absurdly higher than its effective healing.`}
          >
            Echo of Light
          </dfn>
        )}
      >

          Values under 1% of total (and Renew) are omitted.

              Spell
              Amount
              % of Total
              % OH

            {
              this.effectiveHealDistPerc
                .filter(item => (percOfTotalHealingDone * item[1]) > 0.01)
                .map((item, index) => (

                    <SpellIcon id={item[0]} style={{ height: '2.4em' }} />
                    {formatNumber(this.healing * item[1])}
                    {formatPercentage(percOfTotalHealingDone * item[1])}%
                    {formatPercentage(item[2])}%

                ))
            }

      </ExpandableStatisticBox>
    );
  }

  statisticOrder = STATISTIC_ORDER.CORE(2);
}

export default MasteryBreakdown;
<commit_msg>add renew to the mastery breakdown<commit_after>import React from 'react';
import { STATISTIC_ORDER } from 'Main/StatisticBox';
import ExpandableStatisticBox from 'Main/ExpandableStatisticBox';
import SpellIcon from 'common/SpellIcon';
import { formatNumber, formatPercentage } from 'common/format';

import SPELLS from 'common/SPELLS';
import Analyzer from 'Parser/Core/Analyzer';
import { ABILITIES_THAT_TRIGGER_MASTERY } from '../../Constants';

class MasteryBreakdown extends Analyzer {
  _tickMode = {};
  _healValByTargetId = {};
  _maxHealVal = {};
  _masteryActive = {};
  effectiveHealDist = {};
  effectiveOverhealDist = {};
  _eHDbyPlayer = {}; // for potential future features

  healing = 0;

  effectiveHealDistPerc = [];

  on_finished() {
    // There's likely a far better way to do this, but my 2AM brain couldn't find it
    let total = 0;

    Object.keys(this.effectiveHealDist).forEach((spell) => {
      total += this.effectiveHealDist[spell];
    });

    const eHDPerc = {};
    Object.keys(this.effectiveHealDist).forEach((spell) => {
      eHDPerc[spell] = this.effectiveHealDist[spell] / total;
    });

    const eOHD = {};
    Object.keys(this.effectiveHealDist).forEach((spell) => {
      eOHD[spell] = this.effectiveOverhealDist[spell] / (this.effectiveHealDist[spell] + this.effectiveOverhealDist[spell]);
    });

    // Since JS objects lack order, we need to convert our dictionary to an array
    // to allow for it be ordered for display
    const eHDPArray = Object.keys(eHDPerc).map(spell => [spell, eHDPerc[spell], eOHD[spell]]);
    eHDPArray.sort((o1, o2) => o2[1] - o1[1]);

    this.effectiveHealDistPerc = eHDPArray;
  }

  on_byPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    const tId = event.targetID;
    if (spellId !== SPELLS.ECHO_OF_LIGHT.id) {
      return;
    }

    this._masteryActive[tId] = true;
  }

  on_byPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    const tId = event.targetID;
    if (spellId !== SPELLS.ECHO_OF_LIGHT.id) {
      return;
    }

    this._masteryActive[tId] = false;
    this._healValByTargetId[tId] = {};
    this._maxHealVal[tId] = {};
  }

  on_byPlayer_heal(event) {
    const spellId = event.ability.guid;
    const tId = event.targetID;
    if (spellId === SPELLS.ECHO_OF_LIGHT.id) {
      // logic for eol itself
      this.healing += (event.amount + (event.absorbed || 0));
      this.effectiveHealDist = this.effectiveHealDist || {};

      const percH = (event.amount + (event.absorbed || 0)) / (event.amount + (event.absorbed || 0) + (event.overheal || 0));
      const tickMode = this._tickMode[tId];

      if (this._healValByTargetId[tId] === undefined) {
        // If we have a spell that triggers Echo of Light, but is not known of in CONSTANTS.js, then we
        // will potentially end up with a situation where that EoL ticks but there is nothing stored in the
        // EoL data table. This fixes that issue.
        return;
      }

      Object.keys(this._healValByTargetId[tId]).forEach((spell) => {
        // For potential future Features //
        // if (spell in this._eHDbyPlayer[tId]) {
        //   this._eHDbyPlayer[tId][spell] += this._maxHealVal[tId][spell] * percOH / tickMode;
        // } else {
        //   this._eHDbyPlayer[tId][spell] = this._maxHealVal[tId][spell] * percOH / tickMode;
        // }
        // ----------------------------- //

        if (this.effectiveHealDist[spell] !== undefined) {
          this.effectiveHealDist[spell] += (this._maxHealVal[tId][spell]) * percH / tickMode;
          this.effectiveOverhealDist[spell] += (this._maxHealVal[tId][spell]) * (1 - percH) / tickMode;
        } else {
          this.effectiveHealDist[spell] = (this._maxHealVal[tId][spell]) * percH / tickMode;
          this.effectiveOverhealDist[spell] = (this._maxHealVal[tId][spell]) * (1 - percH) / tickMode;
        }

        this._healValByTargetId[tId][spell] -= (this._maxHealVal[tId][spell] / tickMode);

        // There's a rare issue that can cause the healVal allocation to dip to a negative value
        // I'm not sure of the cause but it seems like it is due to an incorrect tickMode
        // value. Likely something to do with events on the same timestamp (or just some bad
        // logic in this file) but the value is much more accurate with this tweak
        if (this._healValByTargetId[tId][spell] < 0) {
          this._healValByTargetId[tId][spell] = 0;
        }
      });
    } else {
      // logic for eol triggering spells
      if (!ABILITIES_THAT_TRIGGER_MASTERY.includes(spellId)) {
        return;
      }

      // filters out renew ticks as they don't apply mastery again
      if (spellId === SPELLS.RENEW.id && event.tick) {
        return;
      }

      if (this._masteryActive[tId]) {
        this._tickMode[tId] = 3;
      } else {
        this._tickMode[tId] = 2;
      }

      if (this._healValByTargetId[tId] === undefined) {
        this._healValByTargetId[tId] = {};
      }

      if (this._maxHealVal[tId] === undefined) {
        this._maxHealVal[tId] = {};
      }

      const effectiveHealing = event.amount + (event.absorbed || 0) + (event.overheal || 0);

      if (this._healValByTargetId[tId][spellId] === undefined) {
        this._healValByTargetId[tId][spellId] = effectiveHealing;
      } else {
        this._healValByTargetId[tId][spellId] += effectiveHealing;
      }
      this._maxHealVal[tId][spellId] = this._healValByTargetId[tId][spellId];
    }
  }

  statistic() {
    const percOfTotalHealingDone = this.owner.getPercentageOfTotalHealingDone(this.healing);
    return (
      <ExpandableStatisticBox
        icon={<SpellIcon id={SPELLS.ECHO_OF_LIGHT.id} />}
        value={`${formatNumber(this.healing)}`}
        label={(
          <dfn data-tip={`Echo of Light healing breakdown. As our mastery is often very finicky, this could end up wrong in various situations. Please report any logs that seem strange to @enragednuke on the WoWAnalyzer discord.

            Please do note this is not 100% accurate. It is probably around 90% accurate.

            Also, a mastery value can be more than just "healing done times mastery percent" because Echo of Light is based off raw healing. If the heal itself overheals, but the mastery does not, it can surpass that assumed "limit". Don't use this as a reason for a "strange log" unless something is absurdly higher than its effective healing.`}
          >
            Echo of Light
          </dfn>
        )}
      >

          Values under 1% of total are omitted.

              Spell
              Amount
              % of Total
              % OH

            {
              this.effectiveHealDistPerc
                .filter(item => (percOfTotalHealingDone * item[1]) > 0.01)
                .map((item, index) => (

                    <SpellIcon id={item[0]} style={{ height: '2.4em' }} />
                    {formatNumber(this.healing * item[1])}
                    {formatPercentage(percOfTotalHealingDone * item[1])}%
                    {formatPercentage(item[2])}%

                ))
            }

      </ExpandableStatisticBox>
    );
  }

  statisticOrder = STATISTIC_ORDER.CORE(2);
}

export default MasteryBreakdown;
<|endoftext|>


<commit_before>// Copyright (c) 2015 Uber Technologies, Inc.
//
// 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.

'use strict';

var errors = require('./errors.js');

var TOMBSTONE_TTL_OFFSET = 500;

module.exports = Operations;

function Operations(opts) {
    var self = this;

    self.timers = opts.timers;
    self.logger = opts.logger;
    self.random = opts.random;
    self.timeoutCheckInterval = opts.timeoutCheckInterval;
    self.timeoutFuzz = opts.timeoutFuzz;
    self.initTimeout = opts.initTimeout;
    self.connectionStalePeriod = opts.connectionStalePeriod;

    self.connection = opts.connection;
    self.startTime = self.timers.now();
    self.timer = null;
    self.destroyed = false;

    self.tombstones = {
        out: []
    };
    self.requests = {
        in: {},
        out: {}
    };
    self.pending = {
        in: 0,
        out: 0
    };
    self.lastTimeoutTime = 0;
}

function OperationTombstone(id, time, timeout) {
    var self = this;

    self.id = id;
    self.time = time;
    self.timeout = timeout;
}

Operations.prototype.startTimeoutTimer =
function startTimeoutTimer() {
    var self = this;

    self.timer = self.timers.setTimeout(
        onChannelTimeout, self._getTimeoutDelay()
    );

    function onChannelTimeout() {
        self._onTimeoutCheck();
    }
};

Operations.prototype.getRequests = function getRequests() {
    var self = this;

    return self.requests;
};

Operations.prototype.getPending = function getPending() {
    var self = this;

    return self.pending;
};

Operations.prototype.getOutReq = function getOutReq(id) {
    var self = this;

    return self.requests.out[id];
};

Operations.prototype.getInReq = function getInReq(id) {
    var self = this;

    return self.requests.in[id];
};

Operations.prototype.addOutReq = function addOutReq(req) {
    var self = this;

    self.requests.out[req.id] = req;
    self.pending.out++;

    return req;
};

Operations.prototype.addInReq = function addInReq(req) {
    var self = this;

    self.requests.in[req.id] = req;
    self.pending.in++;

    return req;
};

Operations.prototype.popOutReq = function popOutReq(id, context) {
    var self = this;

    var req = self.requests.out[id];
    if (!req) {
        var tombstones = self.tombstones.out;
        var isStale = false;

        for (var i = 0; i < tombstones.length; i++) {
            if (tombstones[i].id === id) {
                isStale = true;
                break;
            }
        }

        // If this id has been timed out then just return
        if (isStale) {
            return null;
        }

        // This could be because of a confused / corrupted server.
        self.logger.info('popOutReq received for unknown or lost id', {
            context: context,
            socketRemoteAddr: self.connection.socketRemoteAddr,
            direction: self.connection.direction
        });
        return null;
    }

    delete self.requests.out[id];

    var now = self.timers.now();
    var timeout = now + TOMBSTONE_TTL_OFFSET + req.ttl +
        self._getTimeoutFuzz();

    self.tombstones.out.push(new OperationTombstone(
        req.id, self.timers.now(), timeout
    ));
    self.pending.out--;

    return req;
};

Operations.prototype.popInReq = function popInReq(id) {
    var self = this;

    var req = self.requests.in[id];
    if (!req) {
        // TODO warn ?
        return null;
    }

    delete self.requests.in[id];
    self.pending.in--;

    return req;
};

Operations.prototype.removeReq = function removeReq(id) {
    var self = this;

    if (id in self.requests.in) {
        delete self.requests.in[id];
    } else if (id in self.requests.out) {
        delete self.requests.out[id];
    }
};

Operations.prototype.clear = function clear() {
    var self = this;

    self.pending.in = 0;
    self.pending.out = 0;
    self.tombstones.out = [];
};

Operations.prototype.destroy = function destroy() {
    var self = this;

    self.destroyed = true;

    if (self.timer) {
        self.timers.clearTimeout(self.timer);
        self.timer = null;
    }
};

// timeout check runs every timeoutCheckInterval +/- some random fuzz. Range is from
//   base - fuzz/2 to base + fuzz/2
Operations.prototype._getTimeoutDelay =
function _getTimeoutDelay() {
    var self = this;

    return self.timeoutCheckInterval + self._getTimeoutFuzz();
};

Operations.prototype._getTimeoutFuzz =
function _getTimeoutFuzz() {
    var self = this;

    var fuzz = self.timeoutFuzz;
    if (!fuzz) {
        return 0;
    }

    return Math.round(Math.floor(self.random() * fuzz)) - (fuzz / 2);
};

// If the connection has some success and some timeouts, we should probably leave it up,
// but if everything is timing out, then we should kill the connection.
Operations.prototype._onTimeoutCheck =
function _onTimeoutCheck() {
    var self = this;
    if (self.destroyed) {
        return;
    }

    var isInitialized = self.connection.remoteName;
    if (!isInitialized) {
        var elapsed = self.timers.now() - self.startTime;
        if (elapsed >= self.initTimeout) {
            self.connection.timedOutEvent
                .emit(self, errors.ConnectionTimeoutError({
                    start: self.startTime,
                    elapsed: elapsed,
                    timeout: self.initTimeout
                }));
            return;
        }
    }

    self._checkTimeout(self.requests.out, 'out');
    self._checkTimeout(self.requests.in, 'in');

    var now = self.timers.now();
    var tombstones = [];
    for (var i = 0; i < self.tombstones.out.length; i++) {
        var tombstone = self.tombstones.out[i];
        if (now < tombstone.timeout) {
            tombstones.push(tombstone);
        }
    }
    self.tombstones.out = tombstones;

    self.startTimeoutTimer();
};

Operations.prototype._checkTimeout =
function _checkTimeout(ops, direction) {
    var self = this;
    var opKeys = Object.keys(ops);
    for (var i = 0; i < opKeys.length; i++) {
        var id = opKeys[i];
        var req = ops[id];
        if (req === undefined) {
            self.logger.warn('unexpected undefined request', {
                direction: direction,
                id: id
            });
        } else if (req.timedOut) {
            self.logger.warn('lingering timed-out request', {
                direction: direction,
                id: id
            });

            if (direction === 'in') {
                self.popInReq(id);
            } else if (direction === 'out') {
                self.popOutReq(id);
            }
        } else if (req.checkTimeout()) {
            if (direction === 'out') {
                var now = self.timers.now();
                if (self.lastTimeoutTime &&
                    now > self.lastTimeoutTime + self.connectionStalePeriod
                ) {
                    var err = errors.ConnectionStaleTimeoutError({
                        lastTimeoutTime: self.lastTimeoutTime
                    });
                    self.connection.timedOutEvent
                        .emit(self, err);
                } else if (!self.lastTimeoutTime) {
                    self.lastTimeoutTime = self.timers.now();
                }

            }
            // else
            //     req.res.sendError // XXX may need to build
            if (direction === 'in') {
                self.popInReq(id);
            } else if (direction === 'out') {
                self.popOutReq(id);
            }
        }
    }
};
<commit_msg>ops: use Object.create(null)<commit_after>// Copyright (c) 2015 Uber Technologies, Inc.
//
// 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.

'use strict';

var errors = require('./errors.js');

var TOMBSTONE_TTL_OFFSET = 500;

module.exports = Operations;

function Operations(opts) {
    var self = this;

    self.timers = opts.timers;
    self.logger = opts.logger;
    self.random = opts.random;
    self.timeoutCheckInterval = opts.timeoutCheckInterval;
    self.timeoutFuzz = opts.timeoutFuzz;
    self.initTimeout = opts.initTimeout;
    self.connectionStalePeriod = opts.connectionStalePeriod;

    self.connection = opts.connection;
    self.startTime = self.timers.now();
    self.timer = null;
    self.destroyed = false;

    self.tombstones = {
        out: []
    };
    self.requests = {
        in: Object.create(null),
        out: Object.create(null)
    };
    self.pending = {
        in: 0,
        out: 0
    };
    self.lastTimeoutTime = 0;
}

function OperationTombstone(id, time, timeout) {
    var self = this;

    self.id = id;
    self.time = time;
    self.timeout = timeout;
}

Operations.prototype.startTimeoutTimer =
function startTimeoutTimer() {
    var self = this;

    self.timer = self.timers.setTimeout(
        onChannelTimeout, self._getTimeoutDelay()
    );

    function onChannelTimeout() {
        self._onTimeoutCheck();
    }
};

Operations.prototype.getRequests = function getRequests() {
    var self = this;

    return self.requests;
};

Operations.prototype.getPending = function getPending() {
    var self = this;

    return self.pending;
};

Operations.prototype.getOutReq = function getOutReq(id) {
    var self = this;

    return self.requests.out[id];
};

Operations.prototype.getInReq = function getInReq(id) {
    var self = this;

    return self.requests.in[id];
};

Operations.prototype.addOutReq = function addOutReq(req) {
    var self = this;

    self.requests.out[req.id] = req;
    self.pending.out++;

    return req;
};

Operations.prototype.addInReq = function addInReq(req) {
    var self = this;

    self.requests.in[req.id] = req;
    self.pending.in++;

    return req;
};

Operations.prototype.popOutReq = function popOutReq(id, context) {
    var self = this;

    var req = self.requests.out[id];
    if (!req) {
        var tombstones = self.tombstones.out;
        var isStale = false;

        for (var i = 0; i < tombstones.length; i++) {
            if (tombstones[i].id === id) {
                isStale = true;
                break;
            }
        }

        // If this id has been timed out then just return
        if (isStale) {
            return null;
        }

        // This could be because of a confused / corrupted server.
        self.logger.info('popOutReq received for unknown or lost id', {
            context: context,
            socketRemoteAddr: self.connection.socketRemoteAddr,
            direction: self.connection.direction
        });
        return null;
    }

    delete self.requests.out[id];

    var now = self.timers.now();
    var timeout = now + TOMBSTONE_TTL_OFFSET + req.ttl +
        self._getTimeoutFuzz();

    self.tombstones.out.push(new OperationTombstone(
        req.id, self.timers.now(), timeout
    ));
    self.pending.out--;

    return req;
};

Operations.prototype.popInReq = function popInReq(id) {
    var self = this;

    var req = self.requests.in[id];
    if (!req) {
        // TODO warn ?
        return null;
    }

    delete self.requests.in[id];
    self.pending.in--;

    return req;
};

Operations.prototype.removeReq = function removeReq(id) {
    var self = this;

    if (id in self.requests.in) {
        delete self.requests.in[id];
    } else if (id in self.requests.out) {
        delete self.requests.out[id];
    }
};

Operations.prototype.clear = function clear() {
    var self = this;

    self.pending.in = 0;
    self.pending.out = 0;
    self.tombstones.out = [];
};

Operations.prototype.destroy = function destroy() {
    var self = this;

    self.destroyed = true;

    if (self.timer) {
        self.timers.clearTimeout(self.timer);
        self.timer = null;
    }
};

// timeout check runs every timeoutCheckInterval +/- some random fuzz. Range is from
//   base - fuzz/2 to base + fuzz/2
Operations.prototype._getTimeoutDelay =
function _getTimeoutDelay() {
    var self = this;

    return self.timeoutCheckInterval + self._getTimeoutFuzz();
};

Operations.prototype._getTimeoutFuzz =
function _getTimeoutFuzz() {
    var self = this;

    var fuzz = self.timeoutFuzz;
    if (!fuzz) {
        return 0;
    }

    return Math.round(Math.floor(self.random() * fuzz)) - (fuzz / 2);
};

// If the connection has some success and some timeouts, we should probably leave it up,
// but if everything is timing out, then we should kill the connection.
Operations.prototype._onTimeoutCheck =
function _onTimeoutCheck() {
    var self = this;
    if (self.destroyed) {
        return;
    }

    var isInitialized = self.connection.remoteName;
    if (!isInitialized) {
        var elapsed = self.timers.now() - self.startTime;
        if (elapsed >= self.initTimeout) {
            self.connection.timedOutEvent
                .emit(self, errors.ConnectionTimeoutError({
                    start: self.startTime,
                    elapsed: elapsed,
                    timeout: self.initTimeout
                }));
            return;
        }
    }

    self._checkTimeout(self.requests.out, 'out');
    self._checkTimeout(self.requests.in, 'in');

    var now = self.timers.now();
    var tombstones = [];
    for (var i = 0; i < self.tombstones.out.length; i++) {
        var tombstone = self.tombstones.out[i];
        if (now < tombstone.timeout) {
            tombstones.push(tombstone);
        }
    }
    self.tombstones.out = tombstones;

    self.startTimeoutTimer();
};

Operations.prototype._checkTimeout =
function _checkTimeout(ops, direction) {
    var self = this;
    var opKeys = Object.keys(ops);
    for (var i = 0; i < opKeys.length; i++) {
        var id = opKeys[i];
        var req = ops[id];
        if (req === undefined) {
            self.logger.warn('unexpected undefined request', {
                direction: direction,
                id: id
            });
        } else if (req.timedOut) {
            self.logger.warn('lingering timed-out request', {
                direction: direction,
                id: id
            });

            if (direction === 'in') {
                self.popInReq(id);
            } else if (direction === 'out') {
                self.popOutReq(id);
            }
        } else if (req.checkTimeout()) {
            if (direction === 'out') {
                var now = self.timers.now();
                if (self.lastTimeoutTime &&
                    now > self.lastTimeoutTime + self.connectionStalePeriod
                ) {
                    var err = errors.ConnectionStaleTimeoutError({
                        lastTimeoutTime: self.lastTimeoutTime
                    });
                    self.connection.timedOutEvent
                        .emit(self, err);
                } else if (!self.lastTimeoutTime) {
                    self.lastTimeoutTime = self.timers.now();
                }

            }
            // else
            //     req.res.sendError // XXX may need to build
            if (direction === 'in') {
                self.popInReq(id);
            } else if (direction === 'out') {
                self.popOutReq(id);
            }
        }
    }
};
<|endoftext|>


<commit_before>angular.module('pascalprecht.translate')
/**
 * @ngdoc directive
 * @name pascalprecht.translate.directive:translate
 * @requires $filter
 * @requires $interpolate
 * @restrict A
 *
 * @description
 * Translates given translation id either through attribute or DOM content.
 * Internally it uses `translate` filter to translate translation id. It possible to
 * pass an optional `translate-values` object literal as string into translation id.
 *
 * @param {string=} translate Translation id which could be either string or interpolated string.
 * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object.
 *
 * @example
   <example module="ngView">
    <file name="index.html">

TRANSLATION_ID

{{translationId}}

WITH_VALUES

WITH_VALUES

    </file>
    <file name="script.js">
      angular.module('ngView', ['pascalprecht.translate'])

      .config(function ($translateProvider) {

        $translateProvider.translations({
          'TRANSLATION_ID': 'Hello there!',
          'WITH_VALUES': 'The following value is dynamic: {{value}}'
        });

      });

      angular.module('ngView').controller('TranslateCtrl', function ($scope) {
        $scope.translationId = 'TRANSLATION_ID';

        $scope.values = {
          value: 78
        };
      });
    </file>
    <file name="scenario.js">
      it('should translate', function () {
        inject(function ($rootScope, $compile) {
          $rootScope.translationId = 'TRANSLATION_ID';

          element = $compile('

')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');

          element = $compile('

')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');

          element = $compile('
TRANSLATION_ID
')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');

          element = $compile('
{{translationId}}
')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');
        });
      });
    </file>
   </example>
 */
.directive('translate', ['$filter', '$interpolate', function ($filter, $interpolate) {

  var translate = $filter('translate');

  return {
    restrict: 'AE',
    scope: true,
    link: function linkFn(scope, element, attr) {

      if (attr.translateInterpolation) {
        scope.interpolation = attr.translateInterpolation;
      }
      // Ensures any change of the attribute "translate" containing the id will
      // be re-stored to the scope's "translationId".
      // If the attribute has no content, the element's text value (white spaces trimmed off) will be used.
      attr.$observe('translate', function (translationId) {
        if (angular.equals(translationId , '') || translationId === undefined) {
          scope.translationId = $interpolate(element.text().replace(/^\s+|\s+$/g,''))(scope.$parent);
        } else {
          scope.translationId = translationId;
        }
      });

      attr.$observe('translateValues', function (interpolateParams) {
        scope.interpolateParams = interpolateParams;
      });

      // Ensures the text will be refreshed after the current language was changed
      // w/ $translate.uses(...)
      scope.$on('$translateChangeSuccess', function () {
        element.html(translate(scope.translationId, scope.interpolateParams, scope.interpolation));
      });

      // Ensures the text will be refreshed after either the scope's translationId
      // or the interpolated params have been changed.
      scope.$watch('translationId + interpolateParams', function (nValue) {
        if (nValue) {
          element.html(translate(scope.translationId, scope.interpolateParams, scope.interpolation));
        }
      });
    }
  };
}]);

<commit_msg>Parse given translate-values<commit_after>angular.module('pascalprecht.translate')
/**
 * @ngdoc directive
 * @name pascalprecht.translate.directive:translate
 * @requires $filter
 * @requires $interpolate
 * @restrict A
 *
 * @description
 * Translates given translation id either through attribute or DOM content.
 * Internally it uses `translate` filter to translate translation id. It possible to
 * pass an optional `translate-values` object literal as string into translation id.
 *
 * @param {string=} translate Translation id which could be either string or interpolated string.
 * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object.
 *
 * @example
   <example module="ngView">
    <file name="index.html">

TRANSLATION_ID

{{translationId}}

WITH_VALUES

WITH_VALUES

    </file>
    <file name="script.js">
      angular.module('ngView', ['pascalprecht.translate'])

      .config(function ($translateProvider) {

        $translateProvider.translations({
          'TRANSLATION_ID': 'Hello there!',
          'WITH_VALUES': 'The following value is dynamic: {{value}}'
        });

      });

      angular.module('ngView').controller('TranslateCtrl', function ($scope) {
        $scope.translationId = 'TRANSLATION_ID';

        $scope.values = {
          value: 78
        };
      });
    </file>
    <file name="scenario.js">
      it('should translate', function () {
        inject(function ($rootScope, $compile) {
          $rootScope.translationId = 'TRANSLATION_ID';

          element = $compile('

')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');

          element = $compile('

')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');

          element = $compile('
TRANSLATION_ID
')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');

          element = $compile('
{{translationId}}
')($rootScope);
          $rootScope.$digest();
          expect(element.text()).toBe('Hello there!');
        });
      });
    </file>
   </example>
 */
.directive('translate', ['$filter', '$interpolate', '$parse', function ($filter, $interpolate, $parse) {

  var translate = $filter('translate');

  return {
    restrict: 'AE',
    scope: true,
    link: function linkFn(scope, element, attr) {

      if (attr.translateInterpolation) {
        scope.interpolation = attr.translateInterpolation;
      }
      // Ensures any change of the attribute "translate" containing the id will
      // be re-stored to the scope's "translationId".
      // If the attribute has no content, the element's text value (white spaces trimmed off) will be used.
      attr.$observe('translate', function (translationId) {
        if (angular.equals(translationId , '') || translationId === undefined) {
          scope.translationId = $interpolate(element.text().replace(/^\s+|\s+$/g,''))(scope.$parent);
        } else {
          scope.translationId = translationId;
        }
      });

      attr.$observe('translateValues', function (interpolateParams) {
        scope.interpolateParams = $parse(interpolateParams)(scope.$parent);
      });

      // Ensures the text will be refreshed after the current language was changed
      // w/ $translate.uses(...)
      scope.$on('$translateChangeSuccess', function () {
        element.html(translate(scope.translationId, scope.interpolateParams, scope.interpolation));
      });

      // Ensures the text will be refreshed after either the scope's translationId
      // or the interpolated params have been changed.
      scope.$watch('translationId + interpolateParams', function (nValue) {
        if (nValue) {
          element.html(translate(scope.translationId, scope.interpolateParams, scope.interpolation));
        }
      });
    }
  };
}]);

<|endoftext|>


<commit_before>function typof (v) {
  const s = Object.prototype.toString.call(v)
  return s.substring(8, s.length - 1)
}

export default function formatLog (...args) {
  const msgs = args.map((v) => {
    const type = Object.prototype.toString.call(v)
    if (type.toLowerCase() === '[object object]') {
      try {
        v = '---BEGIN:JSON---' + JSON.stringify(v) + '---END:JSON---'
      } catch (e) {
        v = '[object object]'
      }
    } else {
      if (v === null) {
        v = '---NULL---'
      } else if (v === undefined) {
        v = '---UNDEFINED---'
      } else {
        const vType = typof(v).toUpperCase()
        if (vType === 'NUMBER' || vType === 'BOOLEAN') {
          v = '---BEGIN:' + vType + '---' + v + '---END:' + vType + '---'
        } else {
          v = String(v)
        }
      }
    }
    return v
  })
  let msg = ''
  if (msgs.length > 1) {
    const lastMsg = msgs.pop()
    msg = msgs.join('---COMMA---')
    if (lastMsg.indexOf(' at ') === 0) {
      msg += lastMsg
    } else {
      msg += '---COMMA---' + lastMsg
    }
  } else {
    msg = msgs[0]
  }
  return msg
}
<commit_msg>fixt(cli): format-log.js(es6=>es5)<commit_after>function typof (v) {
  var s = Object.prototype.toString.call(v)
  return s.substring(8, s.length - 1)
}

export default function formatLog () {
  for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
    args[_key] = arguments[_key]
  }

  var msgs = args.map(function (v) {
    var type = Object.prototype.toString.call(v)

    if (type.toLowerCase() === '[object object]') {
      try {
        v = '---BEGIN:JSON---' + JSON.stringify(v) + '---END:JSON---'
      } catch (e) {
        v = '[object object]'
      }
    } else {
      if (v === null) {
        v = '---NULL---'
      } else if (v === undefined) {
        v = '---UNDEFINED---'
      } else {
        var vType = typof(v).toUpperCase()

        if (vType === 'NUMBER' || vType === 'BOOLEAN') {
          v = '---BEGIN:' + vType + '---' + v + '---END:' + vType + '---'
        } else {
          v = String(v)
        }
      }
    }

    return v
  })
  var msg = ''

  if (msgs.length > 1) {
    var lastMsg = msgs.pop()
    msg = msgs.join('---COMMA---')

    if (lastMsg.indexOf(' at ') === 0) {
      msg += lastMsg
    } else {
      msg += '---COMMA---' + lastMsg
    }
  } else {
    msg = msgs[0]
  }

  return msg
}
<|endoftext|>