<commit_before>HTMLWidgets.widget({
    name: 'rcytoscapejs',
    type: 'output',

    initialize: function (el, width, height) {
        return {
            // TODO: add instance fields as required
        }
    },

    resize: function (el, width, height, instance) {
        if (instance.cy)
            instance.cy.resize();
    },

    renderValue: function (el, x, instance) {
        //console.log(x.nodeEntries);
        //console.log(x.edgeEntries);

        //var nodetest = JSON.parse(x.nodeEntries);
        //var edgetest = JSON.parse(x.edgeEntries);

        //console.log(nodetest);
        //console.log(edgetest);

        var defaults = ({
          zoomFactor: 0.05, // zoom factor per zoom tick
          zoomDelay: 45, // how many ms between zoom ticks
          minZoom: 0.1, // min zoom level
          maxZoom: 10, // max zoom level
          fitPadding: 50, // padding when fitting
          panSpeed: 10, // how many ms in between pan ticks
          panDistance: 10, // max pan distance per tick
          panDragAreaSize: 75, // the length of the pan drag box in which the vector for panning is calculated (bigger = finer control of pan speed and direction)
          panMinPercentSpeed: 0.25, // the slowest speed we can pan by (as a percent of panSpeed)
          panInactiveArea: 8, // radius of inactive area in pan drag box
          panIndicatorMinOpacity: 0.5, // min opacity of pan indicator (the draggable nib); scales from this to 1.0
          autodisableForMobile: true, // disable the panzoom completely for mobile (since we don't really need it with gestures like pinch to zoom)

          // icon class names
          sliderHandleIcon: 'fa fa-minus',
          zoomInIcon: 'fa fa-plus',
          zoomOutIcon: 'fa fa-minus',
          resetIcon: 'fa fa-expand'
        });

        var positionMap = {};

        //add position information to data for preset layout
        for (var i = 0; i < x.nodeEntries.length ; i++){
            var xPos = x.nodeEntries[i].data.x;
            var yPos = x.nodeEntries[i].data.y;
            positionMap[x.nodeEntries[i].data.id] = {'x':parseFloat(xPos), 'y':parseFloat(yPos)};
        }

        instance.cy = new cytoscape({
            container: el,
            style: cytoscape.stylesheet()
                .selector('node')
                .css({
                    'color': 'data(nodeLabelColor)',
                    'content': 'data(name)',
                    'text-valign': 'center',
                    'shape': 'data(shape)',
                    'text-outline-color': 'data(color)',
                    'background-color': 'data(color)',
                    'width': 'data(width)',
                    'height': 'data(height)'
                })
                .selector('edge')
                .css({
                    'line-color': 'data(color)',
                    'source-arrow-color': 'data(color)',
                    'target-arrow-color': 'data(color)',
                    'source-arrow-shape': 'data(edgeSourceShape)',
                    'target-arrow-shape': 'data(edgeTargetShape)',
                    'width': 'data(width)'
                })
                .selector(':selected')
                .css({
                    'background-color': '#FF00FF',
                    'line-color': 'black',
                    'target-arrow-color': 'black',
                    'source-arrow-color': 'black'
                })
                .selector('.highlighted')
                .css({
                    'background-color': '#FF00FF',
                    'line-color': '#FF00FF',
                    'target-arrow-color': '#FF00FF',
                    'transition-property': 'background-color, line-color, target-arrow-color',
                    'transition-duration': '0.5s'
                })
                .selector('.faded')
                .css({
                    'opacity': 0.25,
                    'text-opacity': 0
                }),

            elements: {
                nodes: x.nodeEntries,
                //nodes: [{ data: { id:'509209821', name:'509209821', color:'#888888', shape:'ellipse', href:''} }, { data: { id:'531376085', name:'531376085', color:'#888888', shape:'ellipse', href:''} }],
                edges: x.edgeEntries
                //edges: [{ data: { source:'509209821', target:'531376085', color:'#888888', edgeSourceShape:'none', edgeTargetShape:'triangle'} }]

            },
            layout: {
                name: x.layout,
                ungrabifyWhileSimulating: true,
                positions: positionMap
            },
            ready: function () {
                window.cy = this;

                if(x.showPanzoom) {
                  cy.panzoom(defaults);
                }

                cy.boxSelectionEnabled(true);
                cy.userZoomingEnabled(true);
                cy.on('tap', 'node', function (event) {
                    var nodeHighlighted = this.hasClass("highlighted");
                    console.log(nodeHighlighted);
                    var nodes = this.closedNeighborhood().connectedNodes();
                    //console.log(nodes);
                    console.log("A:" + el.id);
                    console.log("ID:" + this._private.data.id);
                    Shiny.onInputChange("clickedNode", this._private.data.id);
                    console.log("break");

                    if (nodes.length === 0) {
                        this.toggleClass("highlighted");
                    }

                    if (nodeHighlighted) {
                        for (var i = 0; i < nodes.length; i++) {
                            if (nodes[i].hasClass("highlighted")) {
                                nodes[i].toggleClass("highlighted");
                            }
                        }
                    } else {
                        for (var i = 0; i < nodes.length; i++) {
                            if (!nodes[i].hasClass("highlighted")) {
                                nodes[i].toggleClass("highlighted");
                            }
                        }
                    }

                    var globalnodes = instance.cy.nodes();
                    var selected = [];
                    for (var i = 0; i < globalnodes.length; i++) {
                        if (globalnodes[i].hasClass("highlighted")) {
                            selected.push(globalnodes[i]._private.ids);
                        }
                    }

                    //console.log(globalnodes);
                    //console.log(selected);

                    var keys = [];
                    for (var i = 0; i < selected.length; i++) {
                        var kk = selected[i];
                        for (var k in kk) keys.push(k);
                    }
                    console.log(keys);
                    Shiny.onInputChange("connectedNodes", keys);
                });

                cy.on('tap', 'node', function (event) {
                    var node = this;
                    $(".qtip").remove();
                    //console.log(event);

                    var name = node._private.data.name;
                    var href = node._private.data.href;

                    var target = event.cyTarget;
                    var sourceName = target.data("id");
                    var targetName = target.data("href");
                    console.log(sourceName);
                    //console.log(targetName);

                    //var x = event.cyRenderedPosition.x;
                    //var y = event.cyRenderedPosition.y;
                    //var x = event.cyPosition.x;
                    //var y = event.cyPosition.y;
                    //console.log("x="+x+" Y="+y);

                    cy.getElementById(node.id()).qtip({
                        content: {
                            text: function (event, api) {
                              // Retrieve content from custom attribute of the $('.selector') elements.
                              return href;
                            }
                        },
                        show: {
                            ready: true
                        },
                        position: {
                            my: 'top center',
                            at: 'bottom center',
                            adjust: {
                              cyViewport: true
                            },
                            effect: false
                        },
                        hide: {
                            fixed: true,
                            event: false,
                            inactive: 1000
                        },
                        style: {
                            classes: 'qtip-bootstrap',
                            tip: {
                              width: 16,
                              height: 8
                            }
                        }
                    });
                });
            }
        });
    }
});
<commit_msg>Pulled in changes to inst/htmlwidgets/rcytoscapejs.js from networkR<commit_after>HTMLWidgets.widget({
    name: 'rcytoscapejs',
    type: 'output',

    initialize: function (el, width, height) {
        return {
            // TODO: add instance fields as required
        }
    },

    resize: function (el, width, height, instance) {
        if (instance.cy)
            instance.cy.resize();
    },

    renderValue: function (el, x, instance) {
        //console.log(x.nodeEntries);
        //console.log(x.edgeEntries);

        //var nodetest = JSON.parse(x.nodeEntries);
        //var edgetest = JSON.parse(x.edgeEntries);

        //console.log(nodetest);
        //console.log(edgetest);

        var defaults = ({
          zoomFactor: 0.05, // zoom factor per zoom tick
          zoomDelay: 45, // how many ms between zoom ticks
          minZoom: 0.1, // min zoom level
          maxZoom: 10, // max zoom level
          fitPadding: 50, // padding when fitting
          panSpeed: 10, // how many ms in between pan ticks
          panDistance: 10, // max pan distance per tick
          panDragAreaSize: 75, // the length of the pan drag box in which the vector for panning is calculated (bigger = finer control of pan speed and direction)
          panMinPercentSpeed: 0.25, // the slowest speed we can pan by (as a percent of panSpeed)
          panInactiveArea: 8, // radius of inactive area in pan drag box
          panIndicatorMinOpacity: 0.5, // min opacity of pan indicator (the draggable nib); scales from this to 1.0
          autodisableForMobile: true, // disable the panzoom completely for mobile (since we don't really need it with gestures like pinch to zoom)

          // icon class names
          sliderHandleIcon: 'fa fa-minus',
          zoomInIcon: 'fa fa-plus',
          zoomOutIcon: 'fa fa-minus',
          resetIcon: 'fa fa-expand'
        });

        var positionMap = {};

        //add position information to data for preset layout
        for (var i = 0; i < x.nodeEntries.length ; i++){
            var xPos = x.nodeEntries[i].data.x;
            var yPos = x.nodeEntries[i].data.y;
            positionMap[x.nodeEntries[i].data.id] = {'x':parseFloat(xPos), 'y':parseFloat(yPos)};
        }

        instance.cy = new cytoscape({
            container: el,
            style: cytoscape.stylesheet()
                .selector('node')
                .css({
                    'color': 'data(nodeLabelColor)',
                    'content': 'data(name)',
                    'text-valign': 'center',
                    'shape': 'data(shape)',
                    'text-outline-color': 'data(color)',
                    'background-color': 'data(color)',
                    'width': 'data(width)',
                    'height': 'data(height)',
                    'font-size': '36px',
                    'min-zoomed-font-size': '7px'
                })
                .selector('edge')
                .css({

                    'line-color': 'data(color)',
                    'width': 'data(width)',
                    'curve-style': 'haystack',
                    'haystack-radius': '0'
                })
                .selector(':selected')
                .css({
                    //'background-color': '#FF00FF',
                    'border-style': 'solid',
                    'border-width': '30px',
                    'border-color': '#FFFF00'
                })
                .selector('.highlighted')
                .css({
                    'background-color': '#FF00FF',
                    'line-color': '#FF00FF',
                    'target-arrow-color': '#FF00FF',
                    'transition-property': 'background-color, line-color, target-arrow-color',
                    'transition-duration': '0.5s'
                })
                .selector('.faded')
                .css({
                    'opacity': 0.25,
                    'text-opacity': 0
                }),

            elements: {
                nodes: x.nodeEntries,
                //nodes: [{ data: { id:'509209821', name:'509209821', color:'#888888', shape:'ellipse', href:''} }, { data: { id:'531376085', name:'531376085', color:'#888888', shape:'ellipse', href:''} }],
                edges: x.edgeEntries
                //edges: [{ data: { source:'509209821', target:'531376085', color:'#888888', edgeSourceShape:'none', edgeTargetShape:'triangle'} }]

            },
            layout: {
                name: x.layout,
                ungrabifyWhileSimulating: true,
                positions: positionMap
            },

            ready: function () {
                window.cy = this;

                if(x.showPanzoom) {
                  cy.panzoom(defaults);
                }

                cy.boxSelectionEnabled(true);
                cy.userZoomingEnabled(true);
                cy.on('tap', 'node', function (event) {
                    /*
                    var nodeHighlighted = this.hasClass("highlighted");
                    console.log(nodeHighlighted);
                    console.log("A:" + el.id);
                    console.log("ID:" + this._private.data.id);
                    console.log("break");

                    //var nodes = this.closedNeighborhood().connectedNodes();
                    var nodes = [];
                    //console.log(nodes);

                    if (nodes.length === 0) {
                        if (nodeHighlighted) {
                          console.log("Node is highlighted");
                          this.toggleClass("highlighted", false);
                        } else {
                          console.log("Node is not highlighted");
                          this.toggleClass("highlighted", true);
                        }

                    }

                    if (nodeHighlighted) {
                        for (var i = 0; i < nodes.length; i++) {
                            if (nodes[i].hasClass("highlighted")) {
                                nodes[i].toggleClass("highlighted");
                            }
                        }
                    } else {
                        for (var i = 0; i < nodes.length; i++) {
                            if (!nodes[i].hasClass("highlighted")) {
                                nodes[i].toggleClass("highlighted");
                            }
                        }
                    }

                    var globalnodes = instance.cy.nodes();
                    var selected = [];
                    for (var i = 0; i < globalnodes.length; i++) {
                        if (globalnodes[i].hasClass("highlighted")) {
                            selected.push(globalnodes[i]._private.ids);
                        }
                    }

                    //console.log(globalnodes);
                    //console.log(selected);

                    var keys = [];
                    for (var i = 0; i < selected.length; i++) {
                        var kk = selected[i];
                        for (var k in kk) keys.push(k);
                    }
                    console.log(keys);

                    Shiny.onInputChange("connectedNodes", keys);
                    Shiny.onInputChange("clickedNode",
                                   cy.$(".highlighted, :selected").map(
                                     function(node) {return node._private.data.id}));
                    */
                });

                /*cy.on('select', function (event) {
                  // var clicked_nodes = cy.$(".highlighted").map(function(node) {return node._private.data.id});
                  //alert(JSON.stringify(Object.keys(event.cyTarget)));
                  //alert(JSON.stringify(event.cyTarget.length));
                  alert(JSON.stringify(event.cyTarget._private.data));

                  //Shiny.onInputChange("clickedNode",  clicked_nodes);
                })*/
                window.old_clicked_nodes = [];
                window.setInterval(function() {
                  var clicked_nodes = cy.$(".highlighted, :selected").map(function(node) {return node._private.data.id});
                  if (clicked_nodes != window.old_clicked_nodes) {
                    Shiny.onInputChange("clickedNode",  clicked_nodes);
                    window.old_clicked_nodes = clicked_nodes;
                  }
                }, 400.0);

                cy.on('tap', 'node', function (event) {

                    var node = this;
                    $(".qtip").remove();
                    //console.log(event);

                    var name = node._private.data.name;
                    var href = node._private.data.href;

                    var target = event.cyTarget;
                    var sourceName = target.data("id");
                    var targetName = target.data("href");
                    console.log(sourceName);
                    //console.log(targetName);

                    //var x = event.cyRenderedPosition.x;
                    //var y = event.cyRenderedPosition.y;
                    //var x = event.cyPosition.x;
                    //var y = event.cyPosition.y;
                    //console.log("x="+x+" Y="+y);

                    cy.getElementById(node.id()).qtip({
                        content: {
                            text: function (event, api) {
                              // Retrieve content from custom attribute of the $('.selector') elements.
                              return href;
                            }
                        },
                        show: {
                            ready: true
                        },
                        position: {
                            my: 'top center',
                            at: 'bottom center',
                            adjust: {
                              cyViewport: true
                            },
                            effect: false
                        },
                        hide: {
                            fixed: true,
                            event: false
                            //delay: 10000
                            //inactive: 1000
                        },
                        style: {
                            classes: 'qtip-bootstrap',
                            tip: {
                              width: 16,
                              height: 8
                            }
                        }
                    });
                });
                if (x.layout != "preset"){
                  var node_positions = cy.$("node").map(
                    function(node) {
                        var position = node.position();
                        position["name"] = node._private.data.short_name;
                        return position;
                    });
                  Shiny.onInputChange("nodeLayout", node_positions);
                }
            }
        });
    }
});
<|endoftext|>


<commit_before><commit_msg>Editor: Use networkFirst in sw.js<commit_after><|endoftext|>


<commit_before>import SPELLS from 'common/SPELLS';
import ISSUE_IMPORTANCE from 'Parser/Core/ISSUE_IMPORTANCE';
import CoreAbilities from 'Parser/Core/Modules/Abilities';

class Abilities extends CoreAbilities {
  spellbook() {
    const combatant = this.combatants.selected;
    return [
      {
        spell: SPELLS.ICEBOUND_FORTITUDE,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 180,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.50,
          extraSuggestion: 'Defensive CDs like this are meant to be used smartly. Use it to smooth regular damage intake or to take the edge of big attacks.',
          importance: ISSUE_IMPORTANCE.MINOR,
        },
      },
      {
        spell: SPELLS.VAMPIRIC_BLOOD,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 90,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.50,
          extraSuggestion: 'Defensive CDs like this are meant to be used smartly. Use it to smooth regular damage intake or to take the edge of big attacks.',
          importance: ISSUE_IMPORTANCE.MINOR,
        },
      },

      {
        spell: SPELLS.BLOOD_MIRROR_TALENT,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 120,
        enabled: combatant.hasTalent(SPELLS.BLOOD_MIRROR_TALENT.id),
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.75,
          extraSuggestion: 'Mostly a DPS CD. Use it to reflect large damage back to the boss. It can be used defensively to reduce 20% damage taken for its duration.',
        },
      },

      {
        spell: SPELLS.BLOOD_BOIL,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
        cooldown: haste => 7.5 / (1 + haste),
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.85,
          extraSuggestion: 'Should be casting it so you have at least one recharging.',
        },
      },
      {
        spell: SPELLS.CONSUMPTION,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
        cooldown: 45,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.90,
          extraSuggestion: 'Should be casting this on CD for the dps unless your saving the leach for something or saving it for a pack of adds.',
        },
      },

      {
        spell: SPELLS.DANCING_RUNE_WEAPON,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 180,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.90,
          extraSuggestion: 'Should be used as an opener and used on CD for the dps boost.',
        },
      },

      {
        spell: SPELLS.BLOODDRINKER_TALENT,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
        cooldown: 30,
        enabled: combatant.hasTalent(SPELLS.BLOODDRINKER_TALENT.id),
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.90,
          extraSuggestion: 'Mostly used as a dps CD. Should be almost casted on CD. Good to use when your running to the boss or cant melee them.',
        },
      },

      {
        spell: SPELLS.ARCANE_TORRENT_MANA,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 90,
        isUndetectable: true,
        castEfficiency: {
          suggestion: true,
        },
      },
    ];
  }
}

export default Abilities;
<commit_msg>Added DS<commit_after>import SPELLS from 'common/SPELLS';
import ISSUE_IMPORTANCE from 'Parser/Core/ISSUE_IMPORTANCE';
import CoreAbilities from 'Parser/Core/Modules/Abilities';

class Abilities extends CoreAbilities {
  spellbook() {
    const combatant = this.combatants.selected;
    return [
      {
        spell: SPELLS.ICEBOUND_FORTITUDE,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 180,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.50,
          extraSuggestion: 'Defensive CDs like this are meant to be used smartly. Use it to smooth regular damage intake or to take the edge of big attacks.',
          importance: ISSUE_IMPORTANCE.MINOR,
        },
      },
      {
        spell: SPELLS.VAMPIRIC_BLOOD,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 90,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.50,
          extraSuggestion: 'Defensive CDs like this are meant to be used smartly. Use it to smooth regular damage intake or to take the edge of big attacks.',
          importance: ISSUE_IMPORTANCE.MINOR,
        },
      },

      {
        spell: SPELLS.BLOOD_MIRROR_TALENT,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 120,
        enabled: combatant.hasTalent(SPELLS.BLOOD_MIRROR_TALENT.id),
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.75,
          extraSuggestion: 'Mostly a DPS CD. Use it to reflect large damage back to the boss. It can be used defensively to reduce 20% damage taken for its duration.',
        },
      },

      {
        spell: SPELLS.BLOOD_BOIL,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
        cooldown: haste => 7.5 / (1 + haste),
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.85,
          extraSuggestion: 'Should be casting it so you have at least one recharging.',
        },
      },
      {
        spell: SPELLS.CONSUMPTION,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
        cooldown: 45,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.90,
          extraSuggestion: 'Should be casting this on CD for the dps unless your saving the leach for something or saving it for a pack of adds.',
        },
      },

      {
        spell: SPELLS.DANCING_RUNE_WEAPON,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 180,
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.90,
          extraSuggestion: 'Should be used as an opener and used on CD for the dps boost.',
        },
      },

      {
        spell: SPELLS.BLOODDRINKER_TALENT,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
        cooldown: 30,
        enabled: combatant.hasTalent(SPELLS.BLOODDRINKER_TALENT.id),
        castEfficiency: {
          suggestion: true,
          recommendedEfficiency: 0.90,
          extraSuggestion: 'Mostly used as a dps CD. Should be almost casted on CD. Good to use when your running to the boss or cant melee them.',
        },
      },

      {
        spell: SPELLS.ARCANE_TORRENT_MANA,
        category: Abilities.SPELL_CATEGORIES.COOLDOWNS,
        cooldown: 90,
        isUndetectable: true,
        castEfficiency: {
          suggestion: true,
        },
      },
      {
        spell: SPELLS.DEATH_STRIKE,
        category: Abilities.SPELL_CATEGORIES.ROTATIONAL,
      },
    ];
  }
}

export default Abilities;
<|endoftext|>


<commit_before>/* coding: utf-8
 *
 * maposmatic, the web front-end of the MapOSMatic city map generation system
 * Copyright (C) 2009 David Decotigny
 * Copyright (C) 2009 Frédéric Lehobey
 * Copyright (C) 2009 David Mentré
 * Copyright (C) 2009 Maxime Petazzoni
 * Copyright (C) 2009 Thomas Petazzoni
 * Copyright (C) 2009 Gaël Utard
 * Copyright (C) 2009 Étienne Loks
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * See the file COPYING for details.
 */

/* OSM slippy map management. */

/* Maximum length of the bounding box to be rendered. This length is
 * checked in both directions (longitude and latitude).
 * Note: if you change this you should probably change
 * BBOX_MAXIMUM_LENGTH_IN_METERS in settings_local.py too. */
const BBOX_MAXIMUM_LENGTH_IN_KM = 20;

var map = null;
var update_lock = 0;
var epsg_display_projection = new OpenLayers.Projection('EPSG:4326');
var epsg_projection = new OpenLayers.Projection('EPSG:900913');
var bbox_style = {fill: true, fillColor: "#FFFFFF", fillOpacity: 0.5,
    stroke: true, strokeOpacity: 0.8, strokeColor: "#FF0000", strokeWidth: 2};

function getUpperLeftLat() { return document.getElementById('lat_upper_left'); }
function getUpperLeftLon() { return document.getElementById('lon_upper_left'); }
function getBottomRightLat() { return document.getElementById('lat_bottom_right'); }
function getBottomRightLon() { return document.getElementById('lon_bottom_right'); }
function getAreaUpperLeftLat() { return document.getElementById('area_lat_upper_left'); }
function getAreaUpperLeftLon() { return document.getElementById('area_lon_upper_left'); }
function getAreaBottomRightLat() { return document.getElementById('area_lat_bottom_right'); }
function getAreaBottomRightLon() { return document.getElementById('area_lon_bottom_right'); }

/* Update form fields on bbox drawing. */
function updateFormBbox(bounds)
{
    bounds = bounds.transform(epsg_projection, epsg_display_projection);

    getUpperLeftLat().value = bounds.top.toFixed(4);
    getUpperLeftLon().value = bounds.left.toFixed(4);
    getBottomRightLat().value = bounds.bottom.toFixed(4);
    getBottomRightLon().value = bounds.right.toFixed(4);

    upper_left   = new OpenLayers.LonLat(bounds.left, bounds.top);
    upper_right  = new OpenLayers.LonLat(bounds.right, bounds.top);
    bottom_right = new OpenLayers.LonLat(bounds.right, bounds.bottom);

    bbox_width  = OpenLayers.Util.distVincenty(upper_left, upper_right)
    bbox_height = OpenLayers.Util.distVincenty(upper_right, bottom_right)

    if (bbox_width > BBOX_MAXIMUM_LENGTH_IN_KM ||
        bbox_height > BBOX_MAXIMUM_LENGTH_IN_KM)
    {
        document.getElementById('bboxtoolarge').style.display = 'block'
        document.getElementById('id_go_next_btn').disabled = true
    }
    else
    {
        document.getElementById('bboxtoolarge').style.display = 'none'
        document.getElementById('id_go_next_btn').disabled = false
    }
}

/* Update the map on form field modification. */
function updateMap(vectorLayer)
{
    if (getUpperLeftLon().value!="" && getUpperLeftLat().value!="" &&
        getBottomRightLon().value!="" && getBottomRightLat().value!="")
    {
        var bbox_bounds = new OpenLayers.Bounds(
            getUpperLeftLon().value, getUpperLeftLat().value,
            getBottomRightLon().value, getBottomRightLat().value);
        bbox_bounds.transform(epsg_display_projection, epsg_projection);
        var feature = new OpenLayers.Feature.Vector(
            bbox_bounds.toGeometry(), {}, bbox_style);
        vectorLayer.addFeatures(feature);
        closest = true
    }
    else
    {
        closest = false
    }

    var bounds = new OpenLayers.Bounds(getAreaUpperLeftLon().value,
                                       getAreaUpperLeftLat().value,
                                       getAreaBottomRightLon().value,
                                       getAreaBottomRightLat().value);
    bounds.transform(epsg_display_projection, epsg_projection);

    update_lock = 1;
    map.zoomToExtent(bounds, closest);
    update_lock = 0;
}

/** Map Zoom/Move events callback: update form fields on zoom action. */
function updateFormArea()
{
    if (update_lock)
      return;

    var bounds = map.getExtent();
    bounds = bounds.transform(epsg_projection, epsg_display_projection);
    getAreaUpperLeftLat().value = bounds.top.toFixed(4);
    getAreaUpperLeftLon().value = bounds.left.toFixed(4);
    getAreaBottomRightLat().value = bounds.bottom.toFixed(4);
    getAreaBottomRightLon().value = bounds.right.toFixed(4);
}

/* Main initialisation function. Must be called before the map is manipulated. */
function mapInit()
{
    map = new OpenLayers.Map ('map', {
        controls:[new OpenLayers.Control.Navigation(),
                  new OpenLayers.Control.PanZoomBar(),
                  new OpenLayers.Control.Attribution()],
        maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),
        numZoomLevels: 18,
        maxResolution: 156543.0399,
        projection: epsg_projection,
        displayProjection: epsg_display_projection
    } );

    layerTilesMapnik = new OpenLayers.Layer.OSM.Mapnik("Mapnik");
    map.addLayer(layerTilesMapnik);

    var vectorLayer = new OpenLayers.Layer.Vector("Overlay");
    map.addLayer(vectorLayer);

    var selectControl = new OpenLayers.Control();
    OpenLayers.Util.extend(selectControl, {
        draw: function() {
            this.box = new OpenLayers.Handler.Box(selectControl,
                {'done': this.notice}, {keyMask: OpenLayers.Handler.MOD_CTRL});
            this.box.activate();
        },

        notice: function(pxbounds) {
            ltpixel = map.getLonLatFromPixel(
                new OpenLayers.Pixel(pxbounds.left, pxbounds.top));
            rbpixel = map.getLonLatFromPixel(
                new OpenLayers.Pixel(pxbounds.right, pxbounds.bottom));
            if (ltpixel.equals(rbpixel))
                return;
            bounds = new OpenLayers.Bounds();
            bounds.extend(ltpixel);
            bounds.extend(rbpixel);
            var feature = new OpenLayers.Feature.Vector(
                bounds.toGeometry(), {}, bbox_style);
            vectorLayer.destroyFeatures()
            vectorLayer.addFeatures(feature);
            updateFormBbox(bounds);
        }
    });
    map.addControl(selectControl);

    map.events.register('zoomend', map, updateFormArea);
    map.events.register('moveend', map, updateFormArea);
    updateMap(vectorLayer);
}

function setFormActivation(active) {
  if (active) {
    $('#map_language').show();
    $('#id_go_next_btn').show()
      .removeAttr('disabled');
  } else {
    $('#map_language').hide();
    $('#id_go_next_btn').hide()
      .attr('disabled', 'disabled');
  }
}

/** Language list management. */
var languages;

function resetLanguages() {
  $('#id_map_language').html(languages);
  $('#id_map_language').children('option').each(function() {
    if ($(this).val() == 'C')
      $(this).attr('selected', 'selected');
  });
}

function preselectLanguage(country) {
  var seen = false;

  $('#id_map_language').html(languages);
  $('#id_map_language').children('option').each(function() {
    if (! ($(this).val().match('.._' + country.toUpperCase() + '\..*') != null
           || $(this).val() == 'C'))
      $(this).remove();
    else if (!seen) {
      $(this).attr('selected', 'selected');
      seen = true;
    }
  });
}

/** Auto-suggest feature. */
function suggest(input, results, osm_id, button, options) {
  var $input = $(input).attr('autocomplete', 'off');
  var $results = $(results);
  var $osm_id = $(osm_id);
  var $button = $(button);
  var timeout = false;
  var shown = false;

  closeSuggest(true);

  // Setup the keyup event.
  $input.keyup(processKey);

  // Disable form validation via the Enter key
  $input.keypress(function(e) { if (e.keyCode == 13) return false; });

  function appendValidResult(item) {
    var id = 'rad_' + item.country_code + '_' + item.ocitysmap_params['id'];
    $results.append('
'
       + item.display_name + '
');

    var e = $('#' + id)
    e.bind('click', function(e) { setResult($(this)); });
    e.bind('mouseover', function(e) { setSelectedResultTo($(this)); });
  }

  /* Empty and close the suggestion box. */
  function closeSuggest(hide) {
    $results.empty();

    if (hide)
      $results.hide();
    else
      $results.show();

    shown = !hide;
  }

  /* Handle the JSON result. */
  function handleNominatimResults(data, textResult) {
    var unusable_token = false;
    $(input).css('cursor', 'text');
    closeSuggest(false);

    if (!data.length) {
      $results.append('
' + $('#noresultsinfo').html() + '
');
      return;
    }

    $.each(data, function(i, item) {
      if (typeof item.ocitysmap_params != 'undefined' &&
          item.ocitysmap_params['admin_level'] == 8) {
        appendValidResult(item);
      } else {
        $results.append('
'
          + item.display_name + '
');
        unusable_token = true;
      }
    });

    if (unusable_token)
      $results.append('
' + $('#noadminlimitinfo').html() + '
');
  }

  function doQuery() {
    if (!$input.val().length) {
      closeSuggest(true);
      return;
    }
    $(input).css('cursor', 'wait');
    $.getJSON("/nominatim/", { q: $input.val() }, handleNominatimResults);
  }

  function processKey(e) {
    if (e.keyCode != 13) {
      clearResult();
    } else {
      if ($osm_id.val() != '')
        $button.click();
    }

    switch (e.keyCode) {
      case 27:  // ESC
        closeSuggest(true);
        break;
      case 9:   // TAB
      case 13:  // OK
        var elt = getCurrentResult();
        if (elt)
          setResult(elt);
        return false;
        break;
      case 38:  // UP
        if (!shown)
          doQuery();
        prevResult();
        break;
      case 40:  // DOWN
        if (!shown)
          doQuery();
        nextResult();
        break;
      default:
        if (timeout)
          clearTimeout(timeout);
        timeout = setTimeout(doQuery, options.timeout);
      }
  }

  /* Returns the currently selected result. */
  function getCurrentResult() {
    var children = $results.children('li.' + options.selectedClass);
    if (children.length)
      return children;
    return false;
  }

  /* Set the form to the given result. */
  function setResult(elt) {
    var temp = elt.attr('id').split('_');

    preselectLanguage(temp[1]);
    $osm_id.val(temp[2]);
    $input.val(elt.html());

    closeSuggest(true);
    setFormActivation(true);
  }

  function clearResult() {
    $osm_id.val('');
    setFormActivation(false);
  }

  /** Functions to manipulate the current selection. */

  /* Set the currently selected item in the drop-down list. */
  function setSelectedResultTo(elt) {
    $results.children('li').removeClass(options.selectedClass);
    if (elt)
      elt.addClass(options.selectedClass);
  }

  /* Move to the previous valid result. */
  function prevResult() {
    var current = getCurrentResult();
    var new_result;

    if (current) {
      var prev_valid = current.siblings('li.suggestok');
      if (prev_valid.length)
        new_result = prev_valid;
    } else {
      new_result = $results.children('li.suggestok:last');
    }

    setSelectedResultTo(new_result);
  }

  /* Move to the next valid result. */
  function nextResult() {
    var current = getCurrentResult();
    var new_result;

    if (current) {
      var next_valid = current.siblings('li.suggestok');
      if (next_valid.length)
        new_result = next_valid;
    } else {
      new_result = $results.children('li.suggestok:first');
    }

    setSelectedResultTo(new_result);
  }
}

/** Page initialization. */
$(document).ready(function() {
  languages = $('#id_map_language').html();

  function switchToAdminMode() {
    $('#mapform tbody').children('tr.bybbox').hide();
    $('#mapform tbody').children('tr.byadmin').show();
    setFormActivation(false);
  }

  function switchToBBoxMode() {
    $('#mapform tbody').children('tr.byadmin').hide();
    $('#mapform tbody').children('tr.bybbox').show();
    setFormActivation(true);
    $('#id_administrative_city').empty();
    $('#id_administrative_osmid').empty();
    if (map == null)
      mapInit();
    resetLanguages();
  }

  if ($('#id_mode_1').is(':checked'))
    switchToBBoxMode();
  else
    switchToAdminMode();

  $('#id_mode_0').bind('click', function(e) { switchToAdminMode(); });
  $('#id_mode_1').bind('click', function(e) { switchToBBoxMode(); });

  suggest('#id_administrative_city', '#suggest',
          '#id_administrative_osmid', '#id_go_next_btn',
          { selectedClass: 'selected',
            timeout: 150
          });
});
<commit_msg>Javascript IE compatibility fix<commit_after>/* coding: utf-8
 *
 * maposmatic, the web front-end of the MapOSMatic city map generation system
 * Copyright (C) 2009 David Decotigny
 * Copyright (C) 2009 Frédéric Lehobey
 * Copyright (C) 2009 David Mentré
 * Copyright (C) 2009 Maxime Petazzoni
 * Copyright (C) 2009 Thomas Petazzoni
 * Copyright (C) 2009 Gaël Utard
 * Copyright (C) 2009 Étienne Loks
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * See the file COPYING for details.
 */

/* OSM slippy map management. */

/* Maximum length of the bounding box to be rendered. This length is
 * checked in both directions (longitude and latitude).
 * Note: if you change this you should probably change
 * BBOX_MAXIMUM_LENGTH_IN_METERS in settings_local.py too.
 *
 * Note: this should be a 'const', but apparently Internet Explorer (even
 * recent versions like 8.x) still chokes on the const keyword.
 * */
var BBOX_MAXIMUM_LENGTH_IN_KM = 20;

var map = null;
var update_lock = 0;
var epsg_display_projection = new OpenLayers.Projection('EPSG:4326');
var epsg_projection = new OpenLayers.Projection('EPSG:900913');
var bbox_style = {fill: true, fillColor: "#FFFFFF", fillOpacity: 0.5,
    stroke: true, strokeOpacity: 0.8, strokeColor: "#FF0000", strokeWidth: 2};

function getUpperLeftLat() { return document.getElementById('lat_upper_left'); }
function getUpperLeftLon() { return document.getElementById('lon_upper_left'); }
function getBottomRightLat() { return document.getElementById('lat_bottom_right'); }
function getBottomRightLon() { return document.getElementById('lon_bottom_right'); }
function getAreaUpperLeftLat() { return document.getElementById('area_lat_upper_left'); }
function getAreaUpperLeftLon() { return document.getElementById('area_lon_upper_left'); }
function getAreaBottomRightLat() { return document.getElementById('area_lat_bottom_right'); }
function getAreaBottomRightLon() { return document.getElementById('area_lon_bottom_right'); }

/* Update form fields on bbox drawing. */
function updateFormBbox(bounds)
{
    bounds = bounds.transform(epsg_projection, epsg_display_projection);

    getUpperLeftLat().value = bounds.top.toFixed(4);
    getUpperLeftLon().value = bounds.left.toFixed(4);
    getBottomRightLat().value = bounds.bottom.toFixed(4);
    getBottomRightLon().value = bounds.right.toFixed(4);

    upper_left   = new OpenLayers.LonLat(bounds.left, bounds.top);
    upper_right  = new OpenLayers.LonLat(bounds.right, bounds.top);
    bottom_right = new OpenLayers.LonLat(bounds.right, bounds.bottom);

    bbox_width  = OpenLayers.Util.distVincenty(upper_left, upper_right)
    bbox_height = OpenLayers.Util.distVincenty(upper_right, bottom_right)

    if (bbox_width > BBOX_MAXIMUM_LENGTH_IN_KM ||
        bbox_height > BBOX_MAXIMUM_LENGTH_IN_KM)
    {
        document.getElementById('bboxtoolarge').style.display = 'block'
        document.getElementById('id_go_next_btn').disabled = true
    }
    else
    {
        document.getElementById('bboxtoolarge').style.display = 'none'
        document.getElementById('id_go_next_btn').disabled = false
    }
}

/* Update the map on form field modification. */
function updateMap(vectorLayer)
{
    if (getUpperLeftLon().value!="" && getUpperLeftLat().value!="" &&
        getBottomRightLon().value!="" && getBottomRightLat().value!="")
    {
        var bbox_bounds = new OpenLayers.Bounds(
            getUpperLeftLon().value, getUpperLeftLat().value,
            getBottomRightLon().value, getBottomRightLat().value);
        bbox_bounds.transform(epsg_display_projection, epsg_projection);
        var feature = new OpenLayers.Feature.Vector(
            bbox_bounds.toGeometry(), {}, bbox_style);
        vectorLayer.addFeatures(feature);
        closest = true
    }
    else
    {
        closest = false
    }

    var bounds = new OpenLayers.Bounds(getAreaUpperLeftLon().value,
                                       getAreaUpperLeftLat().value,
                                       getAreaBottomRightLon().value,
                                       getAreaBottomRightLat().value);
    bounds.transform(epsg_display_projection, epsg_projection);

    update_lock = 1;
    map.zoomToExtent(bounds, closest);
    update_lock = 0;
}

/** Map Zoom/Move events callback: update form fields on zoom action. */
function updateFormArea()
{
    if (update_lock)
      return;

    var bounds = map.getExtent();
    bounds = bounds.transform(epsg_projection, epsg_display_projection);
    getAreaUpperLeftLat().value = bounds.top.toFixed(4);
    getAreaUpperLeftLon().value = bounds.left.toFixed(4);
    getAreaBottomRightLat().value = bounds.bottom.toFixed(4);
    getAreaBottomRightLon().value = bounds.right.toFixed(4);
}

/* Main initialisation function. Must be called before the map is manipulated. */
function mapInit()
{
    map = new OpenLayers.Map ('map', {
        controls:[new OpenLayers.Control.Navigation(),
                  new OpenLayers.Control.PanZoomBar(),
                  new OpenLayers.Control.Attribution()],
        maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),
        numZoomLevels: 18,
        maxResolution: 156543.0399,
        projection: epsg_projection,
        displayProjection: epsg_display_projection
    } );

    layerTilesMapnik = new OpenLayers.Layer.OSM.Mapnik("Mapnik");
    map.addLayer(layerTilesMapnik);

    var vectorLayer = new OpenLayers.Layer.Vector("Overlay");
    map.addLayer(vectorLayer);

    var selectControl = new OpenLayers.Control();
    OpenLayers.Util.extend(selectControl, {
        draw: function() {
            this.box = new OpenLayers.Handler.Box(selectControl,
                {'done': this.notice}, {keyMask: OpenLayers.Handler.MOD_CTRL});
            this.box.activate();
        },

        notice: function(pxbounds) {
            ltpixel = map.getLonLatFromPixel(
                new OpenLayers.Pixel(pxbounds.left, pxbounds.top));
            rbpixel = map.getLonLatFromPixel(
                new OpenLayers.Pixel(pxbounds.right, pxbounds.bottom));
            if (ltpixel.equals(rbpixel))
                return;
            bounds = new OpenLayers.Bounds();
            bounds.extend(ltpixel);
            bounds.extend(rbpixel);
            var feature = new OpenLayers.Feature.Vector(
                bounds.toGeometry(), {}, bbox_style);
            vectorLayer.destroyFeatures()
            vectorLayer.addFeatures(feature);
            updateFormBbox(bounds);
        }
    });
    map.addControl(selectControl);

    map.events.register('zoomend', map, updateFormArea);
    map.events.register('moveend', map, updateFormArea);
    updateMap(vectorLayer);
}

function setFormActivation(active) {
  if (active) {
    $('#map_language').show();
    $('#id_go_next_btn').show()
      .removeAttr('disabled');
  } else {
    $('#map_language').hide();
    $('#id_go_next_btn').hide()
      .attr('disabled', 'disabled');
  }
}

/** Language list management. */
var languages;

function resetLanguages() {
  $('#id_map_language').html(languages);
  $('#id_map_language').children('option').each(function() {
    if ($(this).val() == 'C')
      $(this).attr('selected', 'selected');
  });
}

function preselectLanguage(country) {
  var seen = false;

  $('#id_map_language').html(languages);
  $('#id_map_language').children('option').each(function() {
    if (! ($(this).val().match('.._' + country.toUpperCase() + '\..*') != null
           || $(this).val() == 'C'))
      $(this).remove();
    else if (!seen) {
      $(this).attr('selected', 'selected');
      seen = true;
    }
  });
}

/** Auto-suggest feature. */
function suggest(input, results, osm_id, button, options) {
  var $input = $(input).attr('autocomplete', 'off');
  var $results = $(results);
  var $osm_id = $(osm_id);
  var $button = $(button);
  var timeout = false;
  var shown = false;

  closeSuggest(true);

  // Setup the keyup event.
  $input.keyup(processKey);

  // Disable form validation via the Enter key
  $input.keypress(function(e) { if (e.keyCode == 13) return false; });

  function appendValidResult(item) {
    var id = 'rad_' + item.country_code + '_' + item.ocitysmap_params['id'];
    $results.append('
'
       + item.display_name + '
');

    var e = $('#' + id)
    e.bind('click', function(e) { setResult($(this)); });
    e.bind('mouseover', function(e) { setSelectedResultTo($(this)); });
  }

  /* Empty and close the suggestion box. */
  function closeSuggest(hide) {
    $results.empty();

    if (hide)
      $results.hide();
    else
      $results.show();

    shown = !hide;
  }

  /* Handle the JSON result. */
  function handleNominatimResults(data, textResult) {
    var unusable_token = false;
    $(input).css('cursor', 'text');
    closeSuggest(false);

    if (!data.length) {
      $results.append('
' + $('#noresultsinfo').html() + '
');
      return;
    }

    $.each(data, function(i, item) {
      if (typeof item.ocitysmap_params != 'undefined' &&
          item.ocitysmap_params['admin_level'] == 8) {
        appendValidResult(item);
      } else {
        $results.append('
'
          + item.display_name + '
');
        unusable_token = true;
      }
    });

    if (unusable_token)
      $results.append('
' + $('#noadminlimitinfo').html() + '
');
  }

  function doQuery() {
    if (!$input.val().length) {
      closeSuggest(true);
      return;
    }
    $(input).css('cursor', 'wait');
    $.getJSON("/nominatim/", { q: $input.val() }, handleNominatimResults);
  }

  function processKey(e) {
    if (e.keyCode != 13) {
      clearResult();
    } else {
      if ($osm_id.val() != '')
        $button.click();
    }

    switch (e.keyCode) {
      case 27:  // ESC
        closeSuggest(true);
        break;
      case 9:   // TAB
      case 13:  // OK
        var elt = getCurrentResult();
        if (elt)
          setResult(elt);
        return false;
        break;
      case 38:  // UP
        if (!shown)
          doQuery();
        prevResult();
        break;
      case 40:  // DOWN
        if (!shown)
          doQuery();
        nextResult();
        break;
      default:
        if (timeout)
          clearTimeout(timeout);
        timeout = setTimeout(doQuery, options.timeout);
      }
  }

  /* Returns the currently selected result. */
  function getCurrentResult() {
    var children = $results.children('li.' + options.selectedClass);
    if (children.length)
      return children;
    return false;
  }

  /* Set the form to the given result. */
  function setResult(elt) {
    var temp = elt.attr('id').split('_');

    preselectLanguage(temp[1]);
    $osm_id.val(temp[2]);
    $input.val(elt.html());

    closeSuggest(true);
    setFormActivation(true);
  }

  function clearResult() {
    $osm_id.val('');
    setFormActivation(false);
  }

  /** Functions to manipulate the current selection. */

  /* Set the currently selected item in the drop-down list. */
  function setSelectedResultTo(elt) {
    $results.children('li').removeClass(options.selectedClass);
    if (elt)
      elt.addClass(options.selectedClass);
  }

  /* Move to the previous valid result. */
  function prevResult() {
    var current = getCurrentResult();
    var new_result;

    if (current) {
      var prev_valid = current.siblings('li.suggestok');
      if (prev_valid.length)
        new_result = prev_valid;
    } else {
      new_result = $results.children('li.suggestok:last');
    }

    setSelectedResultTo(new_result);
  }

  /* Move to the next valid result. */
  function nextResult() {
    var current = getCurrentResult();
    var new_result;

    if (current) {
      var next_valid = current.siblings('li.suggestok');
      if (next_valid.length)
        new_result = next_valid;
    } else {
      new_result = $results.children('li.suggestok:first');
    }

    setSelectedResultTo(new_result);
  }
}

/** Page initialization. */
$(document).ready(function() {
  languages = $('#id_map_language').html();

  function switchToAdminMode() {
    $('#mapform tbody').children('tr.bybbox').hide();
    $('#mapform tbody').children('tr.byadmin').show();
    setFormActivation(false);
  }

  function switchToBBoxMode() {
    $('#mapform tbody').children('tr.byadmin').hide();
    $('#mapform tbody').children('tr.bybbox').show();
    setFormActivation(true);
    $('#id_administrative_city').empty();
    $('#id_administrative_osmid').empty();
    if (map == null)
      mapInit();
    resetLanguages();
  }

  if ($('#id_mode_1').is(':checked'))
    switchToBBoxMode();
  else
    switchToAdminMode();

  $('#id_mode_0').bind('click', function(e) { switchToAdminMode(); });
  $('#id_mode_1').bind('click', function(e) { switchToBBoxMode(); });

  suggest('#id_administrative_city', '#suggest',
          '#id_administrative_osmid', '#id_go_next_btn',
          { selectedClass: 'selected',
            timeout: 150
          });
});
<|endoftext|>


<commit_before>// The lack of a single-column primary key on this table turns out to be a real drag,
// because Bookshelf requires one for many purposes, so we have to drop down closer
// to raw SQL to work around that.

module.exports = bookshelf.Model.extend({
  tableName: 'users',

  memberships: function() {
    return this.hasMany(Membership, 'users_id');
  },

  communities: function() {
    return this.belongsToMany(Community, 'users_community', 'users_id', 'community_id');
  },

  linkedAccounts: function() {
    return this.hasMany(LinkedAccount);
  },

  setModerator: function(community) {
    return bookshelf.knex('users_community').where({
      users_id: this.id,
      community_id: (typeof community === 'object' ? community.id : community)
    }).update({role: 1});
  },

  joinCommunity: function(community) {
    return bookshelf.knex('users_community').insert({
      users_id: this.id,
      community_id: (typeof community === 'object' ? community.id : community)
    });
  }

}, {

  withId: function(id) {
    return User.where({id: id}).fetch();
  },

  named: function(name) {
    return User.where({name: name}).fetch();
  }

});<commit_msg>set role to 0 when joining community<commit_after>// The lack of a single-column primary key on this table turns out to be a real drag,
// because Bookshelf requires one for many purposes, so we have to drop down closer
// to raw SQL to work around that.

module.exports = bookshelf.Model.extend({
  tableName: 'users',

  memberships: function() {
    return this.hasMany(Membership, 'users_id');
  },

  communities: function() {
    return this.belongsToMany(Community, 'users_community', 'users_id', 'community_id');
  },

  linkedAccounts: function() {
    return this.hasMany(LinkedAccount);
  },

  setModerator: function(community) {
    return bookshelf.knex('users_community').where({
      users_id: this.id,
      community_id: (typeof community === 'object' ? community.id : community)
    }).update({role: 1});
  },

  joinCommunity: function(community) {
    return bookshelf.knex('users_community').insert({
      users_id: this.id,
      community_id: (typeof community === 'object' ? community.id : community),
      role: 0
    });
  }

}, {

  withId: function(id) {
    return User.where({id: id}).fetch();
  },

  named: function(name) {
    return User.where({name: name}).fetch();
  }

});<|endoftext|>


<commit_before>import { Class, clone, extend } from 'orbit/lib/objects';
import { uuid } from 'orbit/lib/uuid';
import { OperationNotAllowed } from './lib/exceptions';
import Evented from 'orbit/evented';

/**
 `Schema` defines the models allowed in a source, including their keys,
 attributes and relationships. A single schema may be shared across multiple
 sources.

 Schemas are defined with an initial set of options, passed in as a constructor
 argument:

 ``` javascript
  var schema = new Schema({
    models: {
      planet: {
        attributes: {
          name: {type: 'string'},
          classification: {type: 'string'}
        },
        links: {
          moons: {type: 'hasMany', model: 'moon', inverse: 'planet'}
        }
      },
      moon: {
        attributes: {
          name: {type: 'string'}
        },
        links: {
          planet: {type: 'hasOne', model: 'planet', inverse: 'moons'}
        }
      }
    }
  });
 ```

 Models should be keyed by their singular name, and should be defined as an
 object that contains `attributes` and/or `links`.

 Models can be registered after a schema's been initialized with
 `registerModel`.

 ## Fields

 There are three broad categories of fields available for models: keys,
 attributes, and links.

 Within each category, fields may be declared along with options appropriate
 for the category. Common field options include:

 * `type` - a classification, often category-specific, that defines a field's
   purpose and/or contents.

 * `defaultValue` - a value or function that returns a value, to be set on
   record initialization when a field's value is `undefined`.

 Default fields for models can be specified in a `modelDefaults` object. A
 single primary key field, `id`, is defined by default (see below).

 ### Keys

 Keys uniquely identify a record of a particular model type.

 Keys may only be of type `"string"`, which is also the default and therefore
 unnecessary to declare.

 Every model must define a single "primary key", which will be used throughout
 Orbit to identify records of that type uniquely. This should be indicated with
 the field option `primaryKey: true`.

 By default, `modelDefaults` define a primary key `id` to be used for all
 models:

 ```
  {
    keys: {
      'id': {primaryKey: true, defaultValue: uuid}
    }
  }
 ```

 The default primary key has a v4 UUID generator assigned as its `defaultValue`.
 Because of this, these keys can be used within Orbit and on remote servers with
 an extremely low probability of a conflict.

 When working with remote servers that do not support UUID primary keys, it's
 necessary to correlate Orbit IDs with IDs that are generated remotely. In order
 to support this scenario, one or more "secondary keys" may also be defined for
 a model.

 Let's say that you want to track Orbit's primary key locally as a UUID named
 `__id` and also define a remote key named `id`. You could define `modelDefaults`
 in your schema as follows:

 ```
  var schema = new Schema({
    modelDefaults: {
      keys: {
        '__id': {primaryKey: true, defaultValue: uuid},
        'id': {}
      }
    }
  });
 ```

 The `id` field above is considered a secondary key because `primaryKey` is
 `false` by default.

 When any secondary keys are defined, the schema will maintain a mapping of
 secondary to primary key values that can be shared by all sources. This
 centralized mapping assumes that key values will never change once set, which
 is a realistic assumption for distributed systems.

 ### Attributes

 Any properties that define a model's data, with the exception of links to other
 models, should be defined as "attributes".

 Attributes may be defined by their `type`, such as `"string"` or `"date"`,
 which can be used to define their purpose and contents. An attribute's type may
 also be used to determine how it should be normalized and serialized.

 ### Links

 Links are properties that define relationships between models. Two types of
 links are currently allowed:

 * `hasOne` - for to-one relationships
 * `hasMany` - for to-many relationships

 Links must define the related `model` and may optionally define their
 `inverse`, which should correspond to the name of a link on the related model.
 Inverse links should be defined when links must be kept synchronized, so that
 adding or removing a link on the primary model results in a corresponding
 change on the inverse model.

 Here's an example of a schema definition that includes links with inverses:

 ```
  var schema = new Schema({
    models: {
      planet: {
        links: {
          moons: {type: 'hasMany', model: 'moon', inverse: 'planet'}
        }
      },
      moon: {
        links: {
          planet: {type: 'hasOne', model: 'planet', inverse: 'moons'}
        }
      }
    }
  });
 ```

 To-many links may be defined with a special attribute, `actsAsSet`, to indicate
 that they act as a set that should be changed together. Sources should respect
 this attribute when processing changes.

 ```
  var schema = new Schema({
    models: {
      planet: {
        links: {
          moons: {type: 'hasMany', model: 'moon', inverse: 'planet',
                  actsAsSet: true}
        }
      },
      moon: {
        links: {
          planet: {type: 'hasOne', model: 'planet', inverse: 'moons'}
        }
      }
    }
  });
 ```

 ## Model Defaults

 The `modelDefaults` object defines a default model schema for ALL models in the
 schema. This is useful for defining the default ID attribute and any other
 attributes or links that are present across models in the schema.

 As discussed above, `modelDefaults` defines a single primary key by default.
 `modelDefaults` can be overridden to include any number of attributes and links.
 For instance:

 ```
  var schema = new Schema({
    modelDefaults: {
      keys: {
        __id: {primaryKey: true, defaultValue: uuid}
      },
      attributes: {
        createdAt: {type: 'date', defaultValue: currentTime}
      }
    }
  });
 ```

 The default fields can be overridden in or removed from any particular model
 definition. To remove any key, attribute or link definition inherited from
 `modelDefaults` simply define the field with a falsey value (`undefined`,
 `null`, or `false`).

 For example, the following schema removes `createdAt` from the `planet` model:

 ```
  var schema = new Schema({
    modelDefaults: {
      keys: {
        __id: {primaryKey: true, defaultValue: uuid}
      },
      attributes: {
        createdAt: {type: 'date', defaultValue: currentTime}
      }
    },
    models: {
      planet: {
        attributes: {
          name: {type: 'string'},
          createdAt: undefined
        }
      }
    }
  });
 ```

 @class Schema
 @namespace OC
 @param {Object}   [options]
 @param {Object}   [options.modelDefaults] defaults for model schemas
 @param {Function} [options.pluralize] Function used to pluralize names
 @param {Function} [options.singularize] Function used to singularize names
 @param {Object}   [options.models] schemas for individual models supported by this schema
 @constructor
 */
var Schema = Class.extend({
  init: function(options) {
    options = options || {};
    // model defaults
    if (options.modelDefaults) {
      this.modelDefaults = options.modelDefaults;
    } else {
      this.modelDefaults = {
        keys: {
          'id': {primaryKey: true, defaultValue: uuid}
        }
      };
    }
    // inflection
    if (options.pluralize) {
      this.pluralize = options.pluralize;
    }
    if (options.singularize) {
      this.singularize = options.singularize;
    }

    Evented.extend(this);

    // register provided model schema
    this.models = {};
    if (options.models) {
      for (var model in options.models) {
        if (options.models.hasOwnProperty(model)) {
          this.registerModel(model, options.models[model]);
        }
      }
    }
  },

  /**
   Registers a model's schema definition.

   Emits the `modelRegistered` event upon completion.

   @param {String} model      name of the model
   @param {Object} definition model schema definition
   */
  registerModel: function(model, definition) {
    var modelSchema = this._mergeModelSchemas({}, this.modelDefaults, definition);

    // process key definitions
    for (var name in modelSchema.keys) {
      var key = modelSchema.keys[name];

      key.name = name;

      if (key.primaryKey) {
        if (modelSchema.primaryKey) {
          throw new OperationNotAllowed('Schema can only define one primaryKey per model');
        }
        modelSchema.primaryKey = key;

      } else {
        key.primaryKey = false;

        key.secondaryToPrimaryKeyMap = {};
        key.primaryToSecondaryKeyMap = {};

        modelSchema.secondaryKeys = modelSchema.secondaryKeys || {};
        modelSchema.secondaryKeys[name] = key;
      }

      key.type = key.type || 'string';
      if (key.type !== 'string') {
        throw new OperationNotAllowed('Model keys must be of type `"string"`');
      }
    }

    // ensure every model has a valid primary key
    if (!modelSchema.primaryKey || typeof modelSchema.primaryKey.defaultValue !== 'function') {
      throw new OperationNotAllowed('Model schema ID defaultValue must be a function');
    }

    this.models[model] = modelSchema;

    this.emit('modelRegistered', model);
  },

  normalize: function(model, data) {
    if (data.__normalized) return data;

    var record = data; // TODO? clone(data);

    // set flag
    record.__normalized = true;

    // init backward links
    record.__rev = record.__rev || {};

    // init forward links
    record.__rel = record.__rel || {};

    // init meta info
    record.__meta = record.__meta || {};

    this.initDefaults(model, record);

    return record;
  },

  initDefaults: function(model, record) {
    if (!record.__normalized) {
      throw new OperationNotAllowed('Schema.initDefaults requires a normalized record');
    }

    var modelSchema = this.models[model],
        keys = modelSchema.keys,
        attributes = modelSchema.attributes,
        links = modelSchema.links;

    // init primary key - potentially setting the primary key from secondary keys if necessary
    this._initPrimaryKey(modelSchema, record);

    // init default key values
    for (var key in keys) {
      if (record[key] === undefined) {
        record[key] = this._defaultValue(record, keys[key].defaultValue, null);
      }
    }

    // init default attribute values
    if (attributes) {
      for (var attribute in attributes) {
        if (record[attribute] === undefined) {
          record[attribute] = this._defaultValue(record, attributes[attribute].defaultValue, null);
        }
      }
    }

    // init default link values
    if (links) {
      for (var link in links) {
        if (record.__rel[link] === undefined) {
          record.__rel[link] = this._defaultValue(record,
                                                  links[link].defaultValue,
                                                  links[link].type === 'hasMany' ? {} : null);
        }
      }
    }

    this._mapKeys(modelSchema, record);
  },

  primaryToSecondaryKey: function(model, secondaryKeyName, primaryKeyValue, autoGenerate) {
    var modelSchema = this.models[model];
    var secondaryKey = modelSchema.keys[secondaryKeyName];

    var value = secondaryKey.primaryToSecondaryKeyMap[primaryKeyValue];

    // auto-generate secondary key if necessary, requested, and possible
    if (value === undefined && autoGenerate && secondaryKey.defaultValue) {
      value = secondaryKey.defaultValue();
      this._registerKeyMapping(secondaryKey, primaryKeyValue, value);
    }

    return value;
  },

  secondaryToPrimaryKey: function(model, secondaryKeyName, secondaryKeyValue, autoGenerate) {
    var modelSchema = this.models[model];
    var secondaryKey = modelSchema.keys[secondaryKeyName];

    var value = secondaryKey.secondaryToPrimaryKeyMap[secondaryKeyValue];

    // auto-generate primary key if necessary, requested, and possible
    if (value === undefined && autoGenerate && modelSchema.primaryKey.defaultValue) {
      value = modelSchema.primaryKey.defaultValue();
      this._registerKeyMapping(secondaryKey, value, secondaryKeyValue);
    }

    return value;
  },

  /**
   Given a data object structured according to this schema, register all of its
   primary and secondary key mappings. This data object may contain any number
   of records and types.

   @param {Object} data - data structured according to this schema
   */
  registerAllKeys: function(data) {
    if (data) {
      Object.keys(data).forEach(function(type) {
        var modelSchema = this.models[type];

        if (modelSchema && modelSchema.secondaryKeys) {
          var records = data[type];

          Object.keys(records).forEach(function(id) {
            var record = records[id];
            var altId;

            Object.keys(modelSchema.secondaryKeys).forEach(function(secondaryKey) {
              altId = record[secondaryKey];
              if (altId !== undefined && altId !== null) {
                var secondaryKeyDef = modelSchema.secondaryKeys[secondaryKey];
                this._registerKeyMapping(secondaryKeyDef, id, altId);
              }
            }, this);
          }, this);
        }
      }, this);
    }
  },

  /**
   A naive pluralization method.

   Override with a more robust general purpose inflector or provide an
   inflector tailored to the vocabularly of your application.

   @param  {String} word
   @return {String} plural form of `word`
   */
  pluralize: function(word) {
    return word + 's';
  },

  /**
   A naive singularization method.

   Override with a more robust general purpose inflector or provide an
   inflector tailored to the vocabularly of your application.

   @param  {String} word
   @return {String} singular form of `word`
   */
  singularize: function(word) {
    if (word.lastIndexOf('s') === word.length - 1) {
      return word.substr(0, word.length - 1);
    } else {
      return word;
    }
  },

  _defaultValue: function(record, value, defaultValue) {
    if (value === undefined) {
      return defaultValue;

    } else if (typeof value === 'function') {
      return value.call(record);

    } else {
      return value;
    }
  },

  _initPrimaryKey: function(modelSchema, record) {
    var pk = modelSchema.primaryKey.name;
    var id = record[pk];

    // init primary key from secondary keys
    if (!id && modelSchema.secondaryKeys) {
      var keyNames = Object.keys(modelSchema.secondaryKeys);
      for (var i=0, l = keyNames.length; i <l ; i++){
        var key = modelSchema.keys[keyNames[i]];
        var value = record[key.name];
        if (value) {
          id = key.secondaryToPrimaryKeyMap[value];
          if (id) {
            record[pk] = id;
            return;
          }
        }
      }
    }
  },

  _mapKeys: function(modelSchema, record) {
    var id = record[modelSchema.primaryKey.name];

    if (modelSchema.secondaryKeys) {
      Object.keys(modelSchema.secondaryKeys).forEach(function(name) {
        var value = record[name];
        if (value) {
          var key = modelSchema.secondaryKeys[name];
          this._registerKeyMapping(key, id, value);
        }
      }, this);
    }
  },

  _registerKeyMapping: function(secondaryKeyDef, primaryValue, secondaryValue) {
    secondaryKeyDef.primaryToSecondaryKeyMap[primaryValue] = secondaryValue;
    secondaryKeyDef.secondaryToPrimaryKeyMap[secondaryValue] = primaryValue;
  },

  _mergeModelSchemas: function(base) {
    var sources = Array.prototype.slice.call(arguments, 1);

    // ensure model schema has categories set
    base.keys = base.keys || {};
    base.attributes = base.attributes || {};
    base.links = base.links || {};

    sources.forEach(function(source) {
      source = clone(source);
      this._mergeModelFields(base.keys, source.keys);
      this._mergeModelFields(base.attributes, source.attributes);
      this._mergeModelFields(base.links, source.links);
    }, this);

    return base;
  },

  _mergeModelFields: function(base, source) {
    if (source) {
      Object.keys(source).forEach(function(field) {
        if (source.hasOwnProperty(field)) {
          var fieldDef = source[field];
          if (fieldDef) {
            base[field] = fieldDef;
          } else {
            // fields defined as falsey should be removed
            delete base[field];
          }
        }
      });
    }
  }
});

export default Schema;
<commit_msg>Document Schema#normalize.<commit_after>import { Class, clone, extend } from 'orbit/lib/objects';
import { uuid } from 'orbit/lib/uuid';
import { OperationNotAllowed } from './lib/exceptions';
import Evented from 'orbit/evented';

/**
 `Schema` defines the models allowed in a source, including their keys,
 attributes and relationships. A single schema may be shared across multiple
 sources.

 Schemas are defined with an initial set of options, passed in as a constructor
 argument:

 ``` javascript
  var schema = new Schema({
    models: {
      planet: {
        attributes: {
          name: {type: 'string'},
          classification: {type: 'string'}
        },
        links: {
          moons: {type: 'hasMany', model: 'moon', inverse: 'planet'}
        }
      },
      moon: {
        attributes: {
          name: {type: 'string'}
        },
        links: {
          planet: {type: 'hasOne', model: 'planet', inverse: 'moons'}
        }
      }
    }
  });
 ```

 Models should be keyed by their singular name, and should be defined as an
 object that contains `attributes` and/or `links`.

 Models can be registered after a schema's been initialized with
 `registerModel`.

 ## Fields

 There are three broad categories of fields available for models: keys,
 attributes, and links.

 Within each category, fields may be declared along with options appropriate
 for the category. Common field options include:

 * `type` - a classification, often category-specific, that defines a field's
   purpose and/or contents.

 * `defaultValue` - a value or function that returns a value, to be set on
   record initialization when a field's value is `undefined`.

 Default fields for models can be specified in a `modelDefaults` object. A
 single primary key field, `id`, is defined by default (see below).

 ### Keys

 Keys uniquely identify a record of a particular model type.

 Keys may only be of type `"string"`, which is also the default and therefore
 unnecessary to declare.

 Every model must define a single "primary key", which will be used throughout
 Orbit to identify records of that type uniquely. This should be indicated with
 the field option `primaryKey: true`.

 By default, `modelDefaults` define a primary key `id` to be used for all
 models:

 ```
  {
    keys: {
      'id': {primaryKey: true, defaultValue: uuid}
    }
  }
 ```

 The default primary key has a v4 UUID generator assigned as its `defaultValue`.
 Because of this, these keys can be used within Orbit and on remote servers with
 an extremely low probability of a conflict.

 When working with remote servers that do not support UUID primary keys, it's
 necessary to correlate Orbit IDs with IDs that are generated remotely. In order
 to support this scenario, one or more "secondary keys" may also be defined for
 a model.

 Let's say that you want to track Orbit's primary key locally as a UUID named
 `__id` and also define a remote key named `id`. You could define `modelDefaults`
 in your schema as follows:

 ```
  var schema = new Schema({
    modelDefaults: {
      keys: {
        '__id': {primaryKey: true, defaultValue: uuid},
        'id': {}
      }
    }
  });
 ```

 The `id` field above is considered a secondary key because `primaryKey` is
 `false` by default.

 When any secondary keys are defined, the schema will maintain a mapping of
 secondary to primary key values that can be shared by all sources. This
 centralized mapping assumes that key values will never change once set, which
 is a realistic assumption for distributed systems.

 ### Attributes

 Any properties that define a model's data, with the exception of links to other
 models, should be defined as "attributes".

 Attributes may be defined by their `type`, such as `"string"` or `"date"`,
 which can be used to define their purpose and contents. An attribute's type may
 also be used to determine how it should be normalized and serialized.

 ### Links

 Links are properties that define relationships between models. Two types of
 links are currently allowed:

 * `hasOne` - for to-one relationships
 * `hasMany` - for to-many relationships

 Links must define the related `model` and may optionally define their
 `inverse`, which should correspond to the name of a link on the related model.
 Inverse links should be defined when links must be kept synchronized, so that
 adding or removing a link on the primary model results in a corresponding
 change on the inverse model.

 Here's an example of a schema definition that includes links with inverses:

 ```
  var schema = new Schema({
    models: {
      planet: {
        links: {
          moons: {type: 'hasMany', model: 'moon', inverse: 'planet'}
        }
      },
      moon: {
        links: {
          planet: {type: 'hasOne', model: 'planet', inverse: 'moons'}
        }
      }
    }
  });
 ```

 To-many links may be defined with a special attribute, `actsAsSet`, to indicate
 that they act as a set that should be changed together. Sources should respect
 this attribute when processing changes.

 ```
  var schema = new Schema({
    models: {
      planet: {
        links: {
          moons: {type: 'hasMany', model: 'moon', inverse: 'planet',
                  actsAsSet: true}
        }
      },
      moon: {
        links: {
          planet: {type: 'hasOne', model: 'planet', inverse: 'moons'}
        }
      }
    }
  });
 ```

 ## Model Defaults

 The `modelDefaults` object defines a default model schema for ALL models in the
 schema. This is useful for defining the default ID attribute and any other
 attributes or links that are present across models in the schema.

 As discussed above, `modelDefaults` defines a single primary key by default.
 `modelDefaults` can be overridden to include any number of attributes and links.
 For instance:

 ```
  var schema = new Schema({
    modelDefaults: {
      keys: {
        __id: {primaryKey: true, defaultValue: uuid}
      },
      attributes: {
        createdAt: {type: 'date', defaultValue: currentTime}
      }
    }
  });
 ```

 The default fields can be overridden in or removed from any particular model
 definition. To remove any key, attribute or link definition inherited from
 `modelDefaults` simply define the field with a falsey value (`undefined`,
 `null`, or `false`).

 For example, the following schema removes `createdAt` from the `planet` model:

 ```
  var schema = new Schema({
    modelDefaults: {
      keys: {
        __id: {primaryKey: true, defaultValue: uuid}
      },
      attributes: {
        createdAt: {type: 'date', defaultValue: currentTime}
      }
    },
    models: {
      planet: {
        attributes: {
          name: {type: 'string'},
          createdAt: undefined
        }
      }
    }
  });
 ```

 @class Schema
 @namespace OC
 @param {Object}   [options]
 @param {Object}   [options.modelDefaults] defaults for model schemas
 @param {Function} [options.pluralize] Function used to pluralize names
 @param {Function} [options.singularize] Function used to singularize names
 @param {Object}   [options.models] schemas for individual models supported by this schema
 @constructor
 */
var Schema = Class.extend({
  init: function(options) {
    options = options || {};
    // model defaults
    if (options.modelDefaults) {
      this.modelDefaults = options.modelDefaults;
    } else {
      this.modelDefaults = {
        keys: {
          'id': {primaryKey: true, defaultValue: uuid}
        }
      };
    }
    // inflection
    if (options.pluralize) {
      this.pluralize = options.pluralize;
    }
    if (options.singularize) {
      this.singularize = options.singularize;
    }

    Evented.extend(this);

    // register provided model schema
    this.models = {};
    if (options.models) {
      for (var model in options.models) {
        if (options.models.hasOwnProperty(model)) {
          this.registerModel(model, options.models[model]);
        }
      }
    }
  },

  /**
   Registers a model's schema definition.

   Emits the `modelRegistered` event upon completion.

   @param {String} model      name of the model
   @param {Object} definition model schema definition
   */
  registerModel: function(model, definition) {
    var modelSchema = this._mergeModelSchemas({}, this.modelDefaults, definition);

    // process key definitions
    for (var name in modelSchema.keys) {
      var key = modelSchema.keys[name];

      key.name = name;

      if (key.primaryKey) {
        if (modelSchema.primaryKey) {
          throw new OperationNotAllowed('Schema can only define one primaryKey per model');
        }
        modelSchema.primaryKey = key;

      } else {
        key.primaryKey = false;

        key.secondaryToPrimaryKeyMap = {};
        key.primaryToSecondaryKeyMap = {};

        modelSchema.secondaryKeys = modelSchema.secondaryKeys || {};
        modelSchema.secondaryKeys[name] = key;
      }

      key.type = key.type || 'string';
      if (key.type !== 'string') {
        throw new OperationNotAllowed('Model keys must be of type `"string"`');
      }
    }

    // ensure every model has a valid primary key
    if (!modelSchema.primaryKey || typeof modelSchema.primaryKey.defaultValue !== 'function') {
      throw new OperationNotAllowed('Model schema ID defaultValue must be a function');
    }

    this.models[model] = modelSchema;

    this.emit('modelRegistered', model);
  },

  /**
   Normalizes a record according to its type and corresponding schema
   definition.

   A record's primary key, links, and meta data will all be initialized.

   A record can only be normalized once. A flag is set on the record
   (`__normalized`) to prevent "re-normalization".

   @param  {String} model
   @param  {Object} data
   @return {Object} normalized version of `data`
   */
  normalize: function(model, data) {
    if (data.__normalized) return data;

    var record = data;

    // set flag
    record.__normalized = true;

    // init backward links
    record.__rev = record.__rev || {};

    // init forward links
    record.__rel = record.__rel || {};

    // init meta info
    record.__meta = record.__meta || {};

    this.initDefaults(model, record);

    return record;
  },

  initDefaults: function(model, record) {
    if (!record.__normalized) {
      throw new OperationNotAllowed('Schema.initDefaults requires a normalized record');
    }

    var modelSchema = this.models[model],
        keys = modelSchema.keys,
        attributes = modelSchema.attributes,
        links = modelSchema.links;

    // init primary key - potentially setting the primary key from secondary keys if necessary
    this._initPrimaryKey(modelSchema, record);

    // init default key values
    for (var key in keys) {
      if (record[key] === undefined) {
        record[key] = this._defaultValue(record, keys[key].defaultValue, null);
      }
    }

    // init default attribute values
    if (attributes) {
      for (var attribute in attributes) {
        if (record[attribute] === undefined) {
          record[attribute] = this._defaultValue(record, attributes[attribute].defaultValue, null);
        }
      }
    }

    // init default link values
    if (links) {
      for (var link in links) {
        if (record.__rel[link] === undefined) {
          record.__rel[link] = this._defaultValue(record,
                                                  links[link].defaultValue,
                                                  links[link].type === 'hasMany' ? {} : null);
        }
      }
    }

    this._mapKeys(modelSchema, record);
  },

  primaryToSecondaryKey: function(model, secondaryKeyName, primaryKeyValue, autoGenerate) {
    var modelSchema = this.models[model];
    var secondaryKey = modelSchema.keys[secondaryKeyName];

    var value = secondaryKey.primaryToSecondaryKeyMap[primaryKeyValue];

    // auto-generate secondary key if necessary, requested, and possible
    if (value === undefined && autoGenerate && secondaryKey.defaultValue) {
      value = secondaryKey.defaultValue();
      this._registerKeyMapping(secondaryKey, primaryKeyValue, value);
    }

    return value;
  },

  secondaryToPrimaryKey: function(model, secondaryKeyName, secondaryKeyValue, autoGenerate) {
    var modelSchema = this.models[model];
    var secondaryKey = modelSchema.keys[secondaryKeyName];

    var value = secondaryKey.secondaryToPrimaryKeyMap[secondaryKeyValue];

    // auto-generate primary key if necessary, requested, and possible
    if (value === undefined && autoGenerate && modelSchema.primaryKey.defaultValue) {
      value = modelSchema.primaryKey.defaultValue();
      this._registerKeyMapping(secondaryKey, value, secondaryKeyValue);
    }

    return value;
  },

  /**
   Given a data object structured according to this schema, register all of its
   primary and secondary key mappings. This data object may contain any number
   of records and types.

   @param {Object} data - data structured according to this schema
   */
  registerAllKeys: function(data) {
    if (data) {
      Object.keys(data).forEach(function(type) {
        var modelSchema = this.models[type];

        if (modelSchema && modelSchema.secondaryKeys) {
          var records = data[type];

          Object.keys(records).forEach(function(id) {
            var record = records[id];
            var altId;

            Object.keys(modelSchema.secondaryKeys).forEach(function(secondaryKey) {
              altId = record[secondaryKey];
              if (altId !== undefined && altId !== null) {
                var secondaryKeyDef = modelSchema.secondaryKeys[secondaryKey];
                this._registerKeyMapping(secondaryKeyDef, id, altId);
              }
            }, this);
          }, this);
        }
      }, this);
    }
  },

  /**
   A naive pluralization method.

   Override with a more robust general purpose inflector or provide an
   inflector tailored to the vocabularly of your application.

   @param  {String} word
   @return {String} plural form of `word`
   */
  pluralize: function(word) {
    return word + 's';
  },

  /**
   A naive singularization method.

   Override with a more robust general purpose inflector or provide an
   inflector tailored to the vocabularly of your application.

   @param  {String} word
   @return {String} singular form of `word`
   */
  singularize: function(word) {
    if (word.lastIndexOf('s') === word.length - 1) {
      return word.substr(0, word.length - 1);
    } else {
      return word;
    }
  },

  _defaultValue: function(record, value, defaultValue) {
    if (value === undefined) {
      return defaultValue;

    } else if (typeof value === 'function') {
      return value.call(record);

    } else {
      return value;
    }
  },

  _initPrimaryKey: function(modelSchema, record) {
    var pk = modelSchema.primaryKey.name;
    var id = record[pk];

    // init primary key from secondary keys
    if (!id && modelSchema.secondaryKeys) {
      var keyNames = Object.keys(modelSchema.secondaryKeys);
      for (var i=0, l = keyNames.length; i <l ; i++){
        var key = modelSchema.keys[keyNames[i]];
        var value = record[key.name];
        if (value) {
          id = key.secondaryToPrimaryKeyMap[value];
          if (id) {
            record[pk] = id;
            return;
          }
        }
      }
    }
  },

  _mapKeys: function(modelSchema, record) {
    var id = record[modelSchema.primaryKey.name];

    if (modelSchema.secondaryKeys) {
      Object.keys(modelSchema.secondaryKeys).forEach(function(name) {
        var value = record[name];
        if (value) {
          var key = modelSchema.secondaryKeys[name];
          this._registerKeyMapping(key, id, value);
        }
      }, this);
    }
  },

  _registerKeyMapping: function(secondaryKeyDef, primaryValue, secondaryValue) {
    secondaryKeyDef.primaryToSecondaryKeyMap[primaryValue] = secondaryValue;
    secondaryKeyDef.secondaryToPrimaryKeyMap[secondaryValue] = primaryValue;
  },

  _mergeModelSchemas: function(base) {
    var sources = Array.prototype.slice.call(arguments, 1);

    // ensure model schema has categories set
    base.keys = base.keys || {};
    base.attributes = base.attributes || {};
    base.links = base.links || {};

    sources.forEach(function(source) {
      source = clone(source);
      this._mergeModelFields(base.keys, source.keys);
      this._mergeModelFields(base.attributes, source.attributes);
      this._mergeModelFields(base.links, source.links);
    }, this);

    return base;
  },

  _mergeModelFields: function(base, source) {
    if (source) {
      Object.keys(source).forEach(function(field) {
        if (source.hasOwnProperty(field)) {
          var fieldDef = source[field];
          if (fieldDef) {
            base[field] = fieldDef;
          } else {
            // fields defined as falsey should be removed
            delete base[field];
          }
        }
      });
    }
  }
});

export default Schema;
<|endoftext|>