<commit_before>/*
 * Mensuration Widget
 * Logic code from Ian Wilson's (Esri PS) Image Observables demo
 * Transformed into a widget (mostly AMD) for the Basic Viewer by James Tedrick
 *
 * Declaration syntax:
 * require(['dtc/Mensuration], function(Mensuration){
 * 	new Mensuration({map: <esri.Map>, imageService: <esri.layers.ArcGISImageServiceLayer>}, <string|domNode>domNode)
 * });
 *
 * Resources used by this widget:
 * 		-templates/Mensuration.html
 * 		-css/Mensuration.css
 * 		-assets/BaseTop.png
 * 		-assets/BaseTopShadow.png
 * 		-assets/TopTopShadow.png
 *
 */

define(["dojo/_base/declare", "dojo/_base/connect", "dojo/_base/array", "dojo/_base/lang", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dijit/_WidgetsInTemplateMixin", "dojo/text!./templates/Mensuration.html", "dijit/form/Select", "dojo/dom", "dojo/dom-attr", "dojo/dom-construct", "dijit/registry"], function(declare, connect, array, lang, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, dijitTemplate, Select, dom, domAttr, domConstruct, registry) {
	return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
		declaredClass : "dtc.Mensuration",
		templateString : dijitTemplate,

		options : {
			map : null,
			imageService : null
		},
		//imageService: esri.layers.ArcGISImageServiceLayer
		imageService : null,
		//drawToolbar: esri.toolbars.Draw
		drawToolbar : null,
		//measureMethod: string -> esriMensurationHeightFromBaseAndTop|esriMensurationHeightFromBaseAndTopShadow|esriMensurationHeightFromTopAndTopShadow
		measureMethod : null,
		//measureLine: esri.geometry.Polyline (in this application, it will always be a 2 point line)
		measureLine : null,
		//units: string (Linear units from esri.Units)
		units : null,
		//_MENSURATIONOPTIONS: The types of mensuration operations that can be done.  Populates the buttons during startup
		_MENSURATIONOPTIONS : [{
			method : "esriMensurationHeightFromBaseAndTop",
			title : "Measure from the base of the building to the top of the building",
			iconClass : "iconBaseTop"
		}, {
			method : "esriMensurationHeightFromBaseAndTopShadow",
			title : "Measure from the base of the building to the shadow of the top of the building",
			iconClass : "iconBaseTopShadow"
		}, {
			method : "esriMensurationHeightFromTopAndTopShadow",
			title : "Measure from the top of the building to the shadow of the top of the building",
			iconClass : "iconTopTopShadow"
		}],
		//_UNITS: the types of valid (i.e., linear) units. Populates this.unitsSelect during startup
		_UNITS : [{
			value : "esriInches",
			label : "Inches"
		}, {
			value : "esriFeet",
			label : "Feet"
		}, {
			value : "esriYards",
			label : "Yards"
		}, {
			value : "esriMiles",
			label : "Miles"
		}, {
			value : "esriMillimeters",
			label : "Millimeters"
		}, {
			value : "esriCentimeters",
			label : "Centimeters"
		}, {
			value : "esriMeters",
			label : "Meters",
			selected : true
		}, {
			value : "esriKilometers",
			label : "Kilometers"
		}, {
			value : "esriDecimeters",
			label : "Decimeters"
		}],

		constructor : function(options, srcRefNode) {
			declare.safeMixin(this.options, options);

			this.domNode = srcRefNode;
			this.map = this.options.map;
			this.imageService = this.options.imageService;

		},

		startup : function() {
			var _self = this;

			if (!_self.map) {
				console.log('map required');
				_self.destroy();
			}

			if (this.map.loaded) {
				_self._init();
			} else {
				connect.connect(_self.map, "onLoad", function() {
					_self._init()
				});
			}
		},

		//Initialization code here (called by startup)
		_init : function() {
			var _self = this;
			//Load the stylesheet
			domConstruct.create("link", {
				rel : "stylesheet",
				type : "text/css",
				href : "dtc/css/mensuration.css"
			}, document.getElementsByTagName('head')[0]);

			//Instantiate the draw toolbar and have it perform a measurement when the line is completed
			//I'm getting a multiple define error when I try to load via AMD (5/10/13)
			_self.drawToolbar = new esri.toolbars.Draw(this.map, {
				tooltipOffset : 50
			});
			connect.connect(_self.drawToolbar, "onDrawEnd", lang.hitch(_self, _self.measureBuilding));

			//Add the Units to the Select and set the default
			_self.unitsSelect.addOption(_self._UNITS);
			_self.units = 'esriMeters';

			//When we change the units, update the units and fire off a new mensuration request to update the result
			_self.unitsSelect.on("change", function(newValue) {
				_self.units = newValue;
				_self.measureBuilding(null);
			});

			//Buttons - using the _MENSURATIONOPTIONS to simplify construction code with a for loop
			require(["dijit/form/Button"], function(Button) {
				array.forEach(_self._MENSURATIONOPTIONS, function(option) {
					new Button({
						onClick : lang.hitch(_self, function() {
							_self.drawBuildingHeight(option.method);
						}),
						title : option.title,
						iconClass : option.iconClass
					}).placeAt(_self.buttonContainer);
				});
			});
		},

		//Custom setter for the imageService variable to determine whether the service supports mensuration
		_setImageServiceAttr : function(/*string (ID)*/serviceID) {
			var _self = this;
			//Get the service from the map and find out if it's mensurable
			if (serviceID) {
				var service = this.map.getLayer(serviceID);
				if (service.loaded === true) {
					_self._buttonSet(service);
				} else {
					service.on("load", function() {
						_self._buttonSet(service);
					});
				}
			}
			//Set the service
			this._set('imageService', service);
		},
		//Corresponding getter so that the get returns the id, which is what set expects
		//In nearly every case you should be able to widget.set('attribute', widget.get('attribute'))
		_getImageServiceAttr : function() {
			return (this.imageService.id)
		},

		_buttonSet : function(service) {
			var isNotMensurable = true;
			if (service.hasOwnProperty('capabilities')) {
				isNotMensurable = (service.capabilities.indexOf('Mensuration') < 0);
			}

			//If the service doesn't support mensuration, disable the buttons
			var buttons = registry.findWidgets(this.buttonContainer);
			array.forEach(buttons, function(button) {
				button.set('disabled', isNotMensurable);
			});
		},

		drawBuildingHeight : function(method) {
			this.resetHeights();
			this.measureMethod = method;
			this.measureLine = null;
			this.map.setMapCursor('crosshair');
			this.drawToolbar.activate(esri.toolbars.Draw.LINE);
		},

		measureBuilding : function(result) {
			console.log('measureBuilding');
			var _self = this;
			this.drawToolbar.deactivate();
			var imageServiceLayer = this.imageService.url;
			if (result) {
				_self.measureLine = result;
			}
			if (!_self.measureLine) {
				return;
			}
			var fromPoint = _self.measureLine.getPoint(0, 0);
			var toPoint = _self.measureLine.getPoint(0, 1);
			domAttr.set('output.info', "innerHTML", 'Measuring building heights...');
			require(["dojo/json", "esri/request"], function(JSON, esriRequest) {
				var contentObj = {
					fromGeometry : JSON.stringify(fromPoint.toJson()),
					toGeometry : JSON.stringify(toPoint.toJson()),
					geometryType : 'esriGeometryPoint',
					measureOperation : _self.measureMethod,
					linearUnit : _self.units,
					mosaicRule : '',
					pixelSize : '',
					f : 'json'
				};
				new esriRequest({
					url : lang.replace("{0}/{1}", [imageServiceLayer, 'measure']),
					content : contentObj,
					callbackParamName : "callback",
					load : lang.hitch(_self, _self.onResult),
					error : lang.hitch(_self, _self.onResultError)
				});
			});
		},

		onResult : function(response) {
			domAttr.set('output.info', "innerHTML", '');
			// ANALYSIS RESULTS - MEASUREMENTS //
			var measurement = response.height;
			//console.log(measurement);
			// NUMBER FORMATTING //
			var numFormat = {
				places : 2
			};
			// UPDATE MEASUREMENT UI //
			require(['dojo/number'], function(number) {
				domAttr.set('output.MensurationHeight', "innerHTML", number.format(Math.abs(measurement.value), numFormat));
				domAttr.set('output.HeightUncertainty', "innerHTML", number.format(measurement.uncertainty, numFormat));
				domAttr.set('output.LinearUnit', "innerHTML", measurement.unit.replace("esri", ""));
			});
			// DISPLAY MEASUREMENT GRAPHICS //
			//addMeasurementGraphic(userLine, measurement);
			this.map.setMapCursor('default');

		},
		onResultError : function(error) {
			domAttr.set('output.info', "innerHTML", error.message);
			this.map.setMapCursor('default');
		},

		resetHeights : function() {
			// RESET MEASUREMENT UI //
			domAttr.set('output.MensurationHeight', "innerHTML", '0.00');
			domAttr.set('output.HeightUncertainty', "innerHTML", '0.00');
			domAttr.set('output.LinearUnit', "innerHTML", '');
			domAttr.set('output.info', "innerHTML", '');
		}
	});

});
<commit_msg>Update checking mechanism to process null correctly<commit_after>/*
 * Mensuration Widget
 * Logic code from Ian Wilson's (Esri PS) Image Observables demo
 * Transformed into a widget (mostly AMD) for the Basic Viewer by James Tedrick
 *
 * Declaration syntax:
 * require(['dtc/Mensuration], function(Mensuration){
 * 	new Mensuration({map: <esri.Map>, imageService: <esri.layers.ArcGISImageServiceLayer>}, <string|domNode>domNode)
 * });
 *
 * Resources used by this widget:
 * 		-templates/Mensuration.html
 * 		-css/Mensuration.css
 * 		-assets/BaseTop.png
 * 		-assets/BaseTopShadow.png
 * 		-assets/TopTopShadow.png
 *
 */

define(["dojo/_base/declare", "dojo/_base/connect", "dojo/_base/array", "dojo/_base/lang", "dijit/_WidgetBase", "dijit/_TemplatedMixin", "dijit/_WidgetsInTemplateMixin", "dojo/text!./templates/Mensuration.html", "dijit/form/Select", "dojo/dom", "dojo/dom-attr", "dojo/dom-construct", "dijit/registry"], function(declare, connect, array, lang, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, dijitTemplate, Select, dom, domAttr, domConstruct, registry) {
	return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
		declaredClass : "dtc.Mensuration",
		templateString : dijitTemplate,

		options : {
			map : null,
			imageService : null
		},
		//imageService: esri.layers.ArcGISImageServiceLayer
		imageService : null,
		//drawToolbar: esri.toolbars.Draw
		drawToolbar : null,
		//measureMethod: string -> esriMensurationHeightFromBaseAndTop|esriMensurationHeightFromBaseAndTopShadow|esriMensurationHeightFromTopAndTopShadow
		measureMethod : null,
		//measureLine: esri.geometry.Polyline (in this application, it will always be a 2 point line)
		measureLine : null,
		//units: string (Linear units from esri.Units)
		units : null,
		//_MENSURATIONOPTIONS: The types of mensuration operations that can be done.  Populates the buttons during startup
		_MENSURATIONOPTIONS : [{
			method : "esriMensurationHeightFromBaseAndTop",
			title : "Measure from the base of the building to the top of the building",
			iconClass : "iconBaseTop"
		}, {
			method : "esriMensurationHeightFromBaseAndTopShadow",
			title : "Measure from the base of the building to the shadow of the top of the building",
			iconClass : "iconBaseTopShadow"
		}, {
			method : "esriMensurationHeightFromTopAndTopShadow",
			title : "Measure from the top of the building to the shadow of the top of the building",
			iconClass : "iconTopTopShadow"
		}],
		//_UNITS: the types of valid (i.e., linear) units. Populates this.unitsSelect during startup
		_UNITS : [{
			value : "esriInches",
			label : "Inches"
		}, {
			value : "esriFeet",
			label : "Feet"
		}, {
			value : "esriYards",
			label : "Yards"
		}, {
			value : "esriMiles",
			label : "Miles"
		}, {
			value : "esriMillimeters",
			label : "Millimeters"
		}, {
			value : "esriCentimeters",
			label : "Centimeters"
		}, {
			value : "esriMeters",
			label : "Meters",
			selected : true
		}, {
			value : "esriKilometers",
			label : "Kilometers"
		}, {
			value : "esriDecimeters",
			label : "Decimeters"
		}],

		constructor : function(options, srcRefNode) {
			declare.safeMixin(this.options, options);

			this.domNode = srcRefNode;
			this.map = this.options.map;
			this.imageService = this.options.imageService;

		},

		startup : function() {
			var _self = this;

			if (!_self.map) {
				console.log('map required');
				_self.destroy();
			}

			if (this.map.loaded) {
				_self._init();
			} else {
				connect.connect(_self.map, "onLoad", function() {
					_self._init()
				});
			}
		},

		//Initialization code here (called by startup)
		_init : function() {
			var _self = this;
			//Load the stylesheet
			domConstruct.create("link", {
				rel : "stylesheet",
				type : "text/css",
				href : "dtc/css/mensuration.css"
			}, document.getElementsByTagName('head')[0]);

			//Instantiate the draw toolbar and have it perform a measurement when the line is completed
			//I'm getting a multiple define error when I try to load via AMD (5/10/13)
			_self.drawToolbar = new esri.toolbars.Draw(this.map, {
				tooltipOffset : 50
			});
			connect.connect(_self.drawToolbar, "onDrawEnd", lang.hitch(_self, _self.measureBuilding));

			//Add the Units to the Select and set the default
			_self.unitsSelect.addOption(_self._UNITS);
			_self.units = 'esriMeters';

			//When we change the units, update the units and fire off a new mensuration request to update the result
			_self.unitsSelect.on("change", function(newValue) {
				_self.units = newValue;
				_self.measureBuilding(null);
			});

			//Buttons - using the _MENSURATIONOPTIONS to simplify construction code with a for loop
			require(["dijit/form/Button"], function(Button) {
				array.forEach(_self._MENSURATIONOPTIONS, function(option) {
					new Button({
						onClick : lang.hitch(_self, function() {
							_self.drawBuildingHeight(option.method);
						}),
						title : option.title,
						iconClass : option.iconClass
					}).placeAt(_self.buttonContainer);
				});
			});
		},

		//Custom setter for the imageService variable to determine whether the service supports mensuration
		_setImageServiceAttr : function(/*string (ID)*/serviceID) {
			var _self = this;
			var service = null;
			//Get the service from the map and find out if it's mensurable
			if (serviceID) {
				service = this.map.getLayer(serviceID);
				if (service.loaded === true) {
					_self._buttonSet(service);
				} else {
					service.on("load", function() {
						_self._buttonSet(service);
					});
				}
			}
			//Set the service
			this._set('imageService', service);
		},
		//Corresponding getter so that the get returns the id, which is what set expects
		//In nearly every case you should be able to widget.set('attribute', widget.get('attribute'))
		_getImageServiceAttr : function() {
			return (this.imageService.id)
		},

		_buttonSet : function(service) {
			var isNotMensurable = true;
			if (service.hasOwnProperty('capabilities')) {
				isNotMensurable = (service.capabilities.indexOf('Mensuration') < 0);
			}

			//If the service doesn't support mensuration, disable the buttons
			var buttons = registry.findWidgets(this.buttonContainer);
			array.forEach(buttons, function(button) {
				button.set('disabled', isNotMensurable);
			});
		},

		drawBuildingHeight : function(method) {
			this.resetHeights();
			this.measureMethod = method;
			this.measureLine = null;
			this.map.setMapCursor('crosshair');
			this.drawToolbar.activate(esri.toolbars.Draw.LINE);
		},

		measureBuilding : function(result) {
			console.log('measureBuilding');
			var _self = this;
			this.drawToolbar.deactivate();
			var imageServiceLayer = this.imageService.url;
			if (result) {
				_self.measureLine = result;
			}
			if (!_self.measureLine) {
				return;
			}
			var fromPoint = _self.measureLine.getPoint(0, 0);
			var toPoint = _self.measureLine.getPoint(0, 1);
			domAttr.set('output.info', "innerHTML", 'Measuring building heights...');
			require(["dojo/json", "esri/request"], function(JSON, esriRequest) {
				var contentObj = {
					fromGeometry : JSON.stringify(fromPoint.toJson()),
					toGeometry : JSON.stringify(toPoint.toJson()),
					geometryType : 'esriGeometryPoint',
					measureOperation : _self.measureMethod,
					linearUnit : _self.units,
					mosaicRule : '',
					pixelSize : '',
					f : 'json'
				};
				new esriRequest({
					url : lang.replace("{0}/{1}", [imageServiceLayer, 'measure']),
					content : contentObj,
					callbackParamName : "callback",
					load : lang.hitch(_self, _self.onResult),
					error : lang.hitch(_self, _self.onResultError)
				});
			});
		},

		onResult : function(response) {
			domAttr.set('output.info', "innerHTML", '');
			// ANALYSIS RESULTS - MEASUREMENTS //
			var measurement = response.height;
			//console.log(measurement);
			// NUMBER FORMATTING //
			var numFormat = {
				places : 2
			};
			// UPDATE MEASUREMENT UI //
			require(['dojo/number'], function(number) {
				domAttr.set('output.MensurationHeight', "innerHTML", number.format(Math.abs(measurement.value), numFormat));
				domAttr.set('output.HeightUncertainty', "innerHTML", number.format(measurement.uncertainty, numFormat));
				domAttr.set('output.LinearUnit', "innerHTML", measurement.unit.replace("esri", ""));
			});
			// DISPLAY MEASUREMENT GRAPHICS //
			//addMeasurementGraphic(userLine, measurement);
			this.map.setMapCursor('default');

		},
		onResultError : function(error) {
			domAttr.set('output.info', "innerHTML", error.message);
			this.map.setMapCursor('default');
		},

		resetHeights : function() {
			// RESET MEASUREMENT UI //
			domAttr.set('output.MensurationHeight', "innerHTML", '0.00');
			domAttr.set('output.HeightUncertainty', "innerHTML", '0.00');
			domAttr.set('output.LinearUnit', "innerHTML", '');
			domAttr.set('output.info', "innerHTML", '');
		}
	});

});
<|endoftext|>


<commit_before>function GraphRender(nodes, distances, links) {
	// nodes and links for this graph render
	this.nodes 		= nodes;
	this.distances 	= distances;
	this.links 		= links;
	this.singleLinks = [];
	this.doubleLinks = [];
	this.centerNode = 100;
	// defined width & height of svg
	this.w = $(window).width();
	this.h = $(window).height();

	// temp
	this.ready = false;
	this.canTick = true;
	this.radScale = false;
	this.existOverlap = false;

	// set the svg for resizing and use the inner for drawing
	this.svg = d3.select("#d3").select("svg");
	this.inner = d3.select("#d3").select("svg").select("#inner");

    // scale the nodes logarithmically based on half the shortest link
    // to assure no overlapping nodes distance
    this.drawCircles = function() {
        var maxRadius = 0,
            minLength = this.distances[0].sp * 100;

        // check each link for overlapping nodes and update minLength, maxRadius
        for (i = 0; i < this.distances.length; i++) {
            var len = this.distances[i].sp * 100;
            if (len < minLength) {
                minLength = len;
            }

            var source = null,
                target = null;
            // map the link ends to the right nodes
            for (j = 0; j < this.nodes.length; j++) {
                if (this.distances[i].source.id == this.nodes[j].id) {
                    source = this.nodes[j];
                }
                if (this.distances[i].target.id == this.nodes[j].id) {
                    target = this.nodes[j];
                }
            };

            if (source && target) {
                var sr    = source.sum_degree/10,
                    tr    = target.sum_degree/10,
                    radii = sr + tr;

                // check for overlap and bigger than existing maxRadius
                if (radii > len && radii > maxRadius) {
                    maxRadius = Math.max(sr, tr);
					this.existOverlap = true;
                }
            }
        };

        // scale the nodes logarithmically, needing 1 as the base case to avoid
        // pesky NaN due to log(), with rangeRound making integer output
        var radScale = d3.scale.log()
                .domain([1, maxRadius])
                .rangeRound([1, minLength/2]);
		this.radScale = radScale;

        // draw circles
        var center = this.centerNode;
        this.circle.enter().append("circle")
            .attr("r", function(d) {
                // only scale if there exists overlap in the graph
                if (maxRadius > 0) {
                    return radScale(d.sum_degree)+'px';
                }
                else {
                    return d.sum_degree+'px';
                }
            })
            .attr("opacity", 0.5)
            .attr("cx", function(d) {return d.x;})
            .attr("cy", function(d) {return d.y;})
            .attr("title", function(d) {return 'UserId='+d.id+'UserName='+d.name;})
            .attr("class", function(d) {return (d.id==center) ?'center' :'' ;})
            .append("svg:title").text(function(d) { return 'UserId='+d.id+'UserName='+d.name; });

		//this.clearDataNodes();
    };

	this.writeName = function() {
		// A copy of the text with a thick white stroke for legibility.
		this.text.enter().append("text")
			.attr("x", 8)
			.attr("y", ".31em")
			.attr("class", "shadow")
			.text(function(d) { return d.name; });

		this.text.enter().append("text")
			.attr("x", 8)
			.attr("y", ".31em")
			.text(function(d) { return d.name; });
	}

	this.drawPaths = function() {
		// define defs area to initialize all arrows
		var defs = this.inner.append("defs").selectAll("marker");
		// arrows for single links
		defs.data(this.singleLinks).enter().append("marker")
			.attr("id", function(d) {return "marker-"+d.source.id+"-"+d.target.id; })
			.attr("viewBox", "0 0 15 15")
			.attr("refX", function(d) { return 15; })
			.attr("refY", 5)
			.attr("markerWidth", 15)
			.attr("markerHeight", 10)
			.attr("orient", "auto")
			.append("path").attr("d", "M 0 0 L 15 5 L 0 10 z");
			//.attr("d", "M 0 0 L 100 100 M 0 100 L 100 0");
		// arrows for target->source in double links
		defs.data([]);
		defs.data(this.doubleLinks).enter().append("marker")
			.attr("id", function(d) {return "marker-"+d.target.id+"-"+d.source.id; })
			.attr("viewBox", "0 0 15 15")
			.attr("refX", function(d) { return 0; })
			.attr("refY", 5)
			.attr("markerWidth", 15)
			.attr("markerHeight", 10)
			.attr("orient", "auto")
			.append("path").attr("d", "M 15 0 L 0 5 L 15 10 z");
		// arrows for source->target in double links
		defs.data([]);
		defs.data(this.doubleLinks).enter().append("marker")
			.attr("id", function(d) {return "marker-"+d.source.id+"-"+d.target.id; })
			.attr("viewBox", "0 0 15 15")
			.attr("refX", function(d) { return 15; })
			.attr("refY", 5)
			.attr("markerWidth", 15)
			.attr("markerHeight", 10)
			.attr("orient", "auto")
			.append("path").attr("d", "M 0 0 L 15 5 L 0 10 z");
			//.append("path").attr("d", "M0,-5L10,0L0,5");

		// draw single paths
		this.singlePath.enter().append("path")
			.attr("class", function(d) { return "link"; })
			.attr("marker-end", function(d) { return "url(#marker-"+d.source.id+"-"+d.target.id+")"; })
			.attr("title", "xyz - abc")
			.style("stroke-width", function(d) { return (d.inf/2.0)+'px'; });
		// draw double paths
		this.doublePath.enter().append("path")
			.attr("class", function(d) { return "link"; })
			.attr("marker-start", function(d) { return "url(#marker-"+d.target.id+"-"+d.source.id+")"; })
			.attr("marker-end", function(d) { return "url(#marker-"+d.source.id+"-"+d.target.id+")"; })
			.style("stroke-width", function(d) { return (d.inf/2.0)+'px'; });

		//this.clearDataLinks();
	}

	this.draw = function() {
		var arrNodes = [];
		for (i=0; i<this.nodes.length; i++)
			arrNodes[i] = this.nodes[i].sum_degree;
		for (i=0; i<arrNodes.length-1; i++)
			for (j=i+1; j<arrNodes.length; j++)
				if ( arrNodes[j] > arrNodes[i] ) {
					temp = arrNodes[i];
					arrNodes[i] = arrNodes[j];
					arrNodes[j] = temp;
				}
		for (i=0; i<arrNodes.length; i++) {
			console.log(i + ': ' + arrNodes[i]);
		}

		// init force graph
		this.ready = false;
		this.force = d3.layout.force()
			.nodes(this.nodes)
			.links(this.distances)
			.linkDistance(function(d) { return d.sp*100; })
			.charge(-100)          // pos for node attraction, neg for repulsion
			.size([this.w, this.h])
			.start();

		// only render paths with sum influences > 0
		// to make sure, we copy only link whose sum_inf>0 to another array, so d3 can still this.links array with all of the links
		var singles = new Array();	// 1-way relationships
		var doubles = new Array();	// 2-way relationships
		for (i=0; i<this.links.length; i++) {
			// current influences
			inf_1to2 = this.links[i].i12;
			inf_2to1 = this.links[i].i21;
			source = this.links[i].source;
			target = this.links[i].target;
			path = new Object();
			path.inf = inf_1to2 + inf_2to1;
			// if this is a single relationship
			if ( Math.max(inf_1to2, inf_2to1)==(inf_1to2+inf_2to1) ) {
				path.source = (inf_1to2!=0) ?this.nodes[source] :this.nodes[target] ;
				path.target = (path.source.id==this.nodes[source].id) ?this.nodes[target] :this.nodes[source] ;
				singles.push(path);
			} else {
				path.source = this.nodes[source];
				path.target = this.nodes[target];
				doubles.push(path);
			}
		}
		this.singleLinks = singles;
		this.doubleLinks = doubles;
		this.singlePath = this.inner.selectAll("path")
			.data(this.singleLinks);
		this.doublePath = this.inner.selectAll("path")
			.data(this.doubleLinks);

		// init circles as nodes
		this.circle = this.inner.selectAll("circle")
			.data(this.nodes);

		// init texts
		this.text = this.inner.selectAll("text")
			.data(this.nodes);

		// handle events for graph (only for graph)
		return new GraphEvent(this);
	};

	this.data = function(nodes, distances, links) {
		this.nodes 		= nodes;
		this.distances 	= distances;
		this.links 		= links;
	};

	this.setCenterNode = function(id) {
		this.centerNode = id;
	};

	this.hide = function() {
		this.circle.remove();
		this.line.remove();
	};
	this.show = function() {
		this.drawCircles();
		this.drawLines();
	};

	this.empty = function() {
		// empty nodes and links
		this.nodes = [];
		this.links = [];
		this.singleLinks = [];
		this.doubleLinks = [];
		this.centerNode = 0;
		// empty the force graph
		this.force = d3.layout.force()
			.nodes([])
			.links([]);
		// empty actual circles and paths
		this.circle = this.inner.selectAll("circle").data([]);
		this.circle.exit().remove();
		this.path = this.inner.selectAll("path").data([]);
		this.path.exit().remove();
		// empty defs (arrows declaration)
		var defs = this.inner.select("defs").remove();
	};

	this.clearDataNodes = function() {
		// empty nodes and links
		this.nodes = [];
		// empty the force graph
		this.force = d3.layout.force().nodes([]);
		// empty actual circles
		this.circle = this.inner.selectAll("circle").data([]);
		console.log('data nodes cleared.');
	}
	this.clearDataLinks = function() {
		// empty nodes and links
		this.links = [];
		this.singleLinks = [];
		this.doubleLinks = [];
		// empty the force graph
		this.force = d3.layout.force().links([]);
		// empty actual circles and paths
		this.path = this.inner.selectAll("path").data([]);
		console.log('data links cleared.');
	}
}
<commit_msg>fix small issue: switching between overlap and non-overlap graphs should change the $existOverlap variable accordingly (true/false)<commit_after>function GraphRender(nodes, distances, links) {
	// nodes and links for this graph render
	this.nodes 		= nodes;
	this.distances 	= distances;
	this.links 		= links;
	this.singleLinks = [];
	this.doubleLinks = [];
	this.centerNode = 100;
	// defined width & height of svg
	this.w = $(window).width();
	this.h = $(window).height();

	// temp
	this.ready = false;
	this.canTick = true;
	this.radScale = false;
	this.existOverlap = false;

	// set the svg for resizing and use the inner for drawing
	this.svg = d3.select("#d3").select("svg");
	this.inner = d3.select("#d3").select("svg").select("#inner");

    // scale the nodes logarithmically based on half the shortest link
    // to assure no overlapping nodes distance
    this.drawCircles = function() {
        var maxRadius = 0,
            minLength = this.distances[0].sp * 100;

		// by default there is no overlap
		this.existOverlap = false;
        // check each link for overlapping nodes and update minLength, maxRadius
        for (i = 0; i < this.distances.length; i++) {
            var len = this.distances[i].sp * 100;
            if (len < minLength) {
                minLength = len;
            }

            var source = null,
                target = null;
            // map the link ends to the right nodes
            for (j = 0; j < this.nodes.length; j++) {
                if (this.distances[i].source.id == this.nodes[j].id) {
                    source = this.nodes[j];
                }
                if (this.distances[i].target.id == this.nodes[j].id) {
                    target = this.nodes[j];
                }
            };

            if (source && target) {
                var sr    = source.sum_degree/10,
                    tr    = target.sum_degree/10,
                    radii = sr + tr;

                // check for overlap and bigger than existing maxRadius
                if (radii > len && radii > maxRadius) {
                    maxRadius = Math.max(sr, tr);
					this.existOverlap = true;
                }
            }
        };

        // scale the nodes logarithmically, needing 1 as the base case to avoid
        // pesky NaN due to log(), with rangeRound making integer output
        var radScale = d3.scale.log()
                .domain([1, maxRadius])
                .rangeRound([1, minLength/2]);
		this.radScale = radScale;

        // draw circles
        var center = this.centerNode;
        this.circle.enter().append("circle")
            .attr("r", function(d) {
                // only scale if there exists overlap in the graph
                if (maxRadius > 0) {
                    return radScale(d.sum_degree)+'px';
                }
                else {
                    return d.sum_degree+'px';
                }
            })
            .attr("opacity", 0.5)
            .attr("cx", function(d) {return d.x;})
            .attr("cy", function(d) {return d.y;})
            .attr("title", function(d) {return 'UserId='+d.id+'UserName='+d.name;})
            .attr("class", function(d) {return (d.id==center) ?'center' :'' ;})
            .append("svg:title").text(function(d) { return 'UserId='+d.id+'UserName='+d.name; });

		//this.clearDataNodes();
    };

	this.writeName = function() {
		// A copy of the text with a thick white stroke for legibility.
		this.text.enter().append("text")
			.attr("x", 8)
			.attr("y", ".31em")
			.attr("class", "shadow")
			.text(function(d) { return d.name; });

		this.text.enter().append("text")
			.attr("x", 8)
			.attr("y", ".31em")
			.text(function(d) { return d.name; });
	}

	this.drawPaths = function() {
		// define defs area to initialize all arrows
		var defs = this.inner.append("defs").selectAll("marker");
		// arrows for single links
		defs.data(this.singleLinks).enter().append("marker")
			.attr("id", function(d) {return "marker-"+d.source.id+"-"+d.target.id; })
			.attr("viewBox", "0 0 15 15")
			.attr("refX", function(d) { return 15; })
			.attr("refY", 5)
			.attr("markerWidth", 15)
			.attr("markerHeight", 10)
			.attr("orient", "auto")
			.append("path").attr("d", "M 0 0 L 15 5 L 0 10 z");
			//.attr("d", "M 0 0 L 100 100 M 0 100 L 100 0");
		// arrows for target->source in double links
		defs.data([]);
		defs.data(this.doubleLinks).enter().append("marker")
			.attr("id", function(d) {return "marker-"+d.target.id+"-"+d.source.id; })
			.attr("viewBox", "0 0 15 15")
			.attr("refX", function(d) { return 0; })
			.attr("refY", 5)
			.attr("markerWidth", 15)
			.attr("markerHeight", 10)
			.attr("orient", "auto")
			.append("path").attr("d", "M 15 0 L 0 5 L 15 10 z");
		// arrows for source->target in double links
		defs.data([]);
		defs.data(this.doubleLinks).enter().append("marker")
			.attr("id", function(d) {return "marker-"+d.source.id+"-"+d.target.id; })
			.attr("viewBox", "0 0 15 15")
			.attr("refX", function(d) { return 15; })
			.attr("refY", 5)
			.attr("markerWidth", 15)
			.attr("markerHeight", 10)
			.attr("orient", "auto")
			.append("path").attr("d", "M 0 0 L 15 5 L 0 10 z");
			//.append("path").attr("d", "M0,-5L10,0L0,5");

		// draw single paths
		this.singlePath.enter().append("path")
			.attr("class", function(d) { return "link"; })
			.attr("marker-end", function(d) { return "url(#marker-"+d.source.id+"-"+d.target.id+")"; })
			.attr("title", "xyz - abc")
			.style("stroke-width", function(d) { return (d.inf/2.0)+'px'; });
		// draw double paths
		this.doublePath.enter().append("path")
			.attr("class", function(d) { return "link"; })
			.attr("marker-start", function(d) { return "url(#marker-"+d.target.id+"-"+d.source.id+")"; })
			.attr("marker-end", function(d) { return "url(#marker-"+d.source.id+"-"+d.target.id+")"; })
			.style("stroke-width", function(d) { return (d.inf/2.0)+'px'; });

		//this.clearDataLinks();
	}

	this.draw = function() {
		/*
		var arrNodes = [];
		for (i=0; i<this.nodes.length; i++)
			arrNodes[i] = this.nodes[i].sum_degree;
		for (i=0; i<arrNodes.length-1; i++)
			for (j=i+1; j<arrNodes.length; j++)
				if ( arrNodes[j] > arrNodes[i] ) {
					temp = arrNodes[i];
					arrNodes[i] = arrNodes[j];
					arrNodes[j] = temp;
				}
		for (i=0; i<arrNodes.length; i++) {
			console.log(i + ': ' + arrNodes[i]);
		}
		*/

		// init force graph
		this.ready = false;
		this.force = d3.layout.force()
			.nodes(this.nodes)
			.links(this.distances)
			.linkDistance(function(d) { return d.sp*100; })
			.charge(-100)          // pos for node attraction, neg for repulsion
			.size([this.w, this.h])
			.start();

		// only render paths with sum influences > 0
		// to make sure, we copy only link whose sum_inf>0 to another array, so d3 can still this.links array with all of the links
		var singles = new Array();	// 1-way relationships
		var doubles = new Array();	// 2-way relationships
		for (i=0; i<this.links.length; i++) {
			// current influences
			inf_1to2 = this.links[i].i12;
			inf_2to1 = this.links[i].i21;
			source = this.links[i].source;
			target = this.links[i].target;
			path = new Object();
			path.inf = inf_1to2 + inf_2to1;
			// if this is a single relationship
			if ( Math.max(inf_1to2, inf_2to1)==(inf_1to2+inf_2to1) ) {
				path.source = (inf_1to2!=0) ?this.nodes[source] :this.nodes[target] ;
				path.target = (path.source.id==this.nodes[source].id) ?this.nodes[target] :this.nodes[source] ;
				singles.push(path);
			} else {
				path.source = this.nodes[source];
				path.target = this.nodes[target];
				doubles.push(path);
			}
		}
		this.singleLinks = singles;
		this.doubleLinks = doubles;
		this.singlePath = this.inner.selectAll("path")
			.data(this.singleLinks);
		this.doublePath = this.inner.selectAll("path")
			.data(this.doubleLinks);

		// init circles as nodes
		this.circle = this.inner.selectAll("circle")
			.data(this.nodes);

		// init texts
		this.text = this.inner.selectAll("text")
			.data(this.nodes);

		// handle events for graph (only for graph)
		return new GraphEvent(this);
	};

	this.data = function(nodes, distances, links) {
		this.nodes 		= nodes;
		this.distances 	= distances;
		this.links 		= links;
	};

	this.setCenterNode = function(id) {
		this.centerNode = id;
	};

	this.hide = function() {
		this.circle.remove();
		this.line.remove();
	};
	this.show = function() {
		this.drawCircles();
		this.drawLines();
	};

	this.empty = function() {
		// empty nodes and links
		this.nodes = [];
		this.links = [];
		this.singleLinks = [];
		this.doubleLinks = [];
		this.centerNode = 0;
		// empty the force graph
		this.force = d3.layout.force()
			.nodes([])
			.links([]);
		// empty actual circles and paths
		this.circle = this.inner.selectAll("circle").data([]);
		this.circle.exit().remove();
		this.path = this.inner.selectAll("path").data([]);
		this.path.exit().remove();
		// empty defs (arrows declaration)
		var defs = this.inner.select("defs").remove();
	};

	this.clearDataNodes = function() {
		// empty nodes and links
		this.nodes = [];
		// empty the force graph
		this.force = d3.layout.force().nodes([]);
		// empty actual circles
		this.circle = this.inner.selectAll("circle").data([]);
		console.log('data nodes cleared.');
	}
	this.clearDataLinks = function() {
		// empty nodes and links
		this.links = [];
		this.singleLinks = [];
		this.doubleLinks = [];
		// empty the force graph
		this.force = d3.layout.force().links([]);
		// empty actual circles and paths
		this.path = this.inner.selectAll("path").data([]);
		console.log('data links cleared.');
	}
}
<|endoftext|>


<commit_before>  (function( $ ) {
    $.widget( "custom.combobox", {
      options:{
          size:""
      },
      _create: function() {
        this.wrapper = $( "<span>" )
          .addClass( "custom-combobox" )
          .insertAfter( this.element );

        this.element.hide();
        this._createAutocomplete();
        this._createShowAllButton();
      },

      _createAutocomplete: function() {
        var selected = this.element.children( ":selected" ),
          value = selected.val() ? selected.text() : "";

        var size=this.options.size;
        if (size == "auto") {
          size=0;
          this.element.children("option").each(function(){
            if(!size) size = this.text.length;
            else size = Math.max(size, this.text.length);
          });
        }
        this.input = $( "<input>" )
          .appendTo( this.wrapper )
          .val( value )
          .attr( "title", "" )
          .attr("size", size)
          .addClass( "custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left" )
          .autocomplete({
            delay: 0,
            minLength: 0,
            source: $.proxy( this, "_source" )
          })
          .tooltip({
            tooltipClass: "ui-state-highlight"
          });

        this._on( this.input, {
          autocompleteselect: function( event, ui ) {
            ui.item.option.selected = true;
            this._trigger( "select", event, {
              item: ui.item.option
            });
            this.element.change();
          },

          autocompletechange: "_removeIfInvalid"
        });
      },

      _createShowAllButton: function() {
        var input = this.input,
          wasOpen = false;

        $( "<a>" )
          .attr( "tabIndex", -1 )
          .attr( "title", "Show All Items" )
          .tooltip()
          .appendTo( this.wrapper )
          .button({
            icons: {
              primary: "ui-icon-triangle-1-s"
            },
            text: false
          })
          .removeClass( "ui-corner-all" )
          .addClass( "custom-combobox-toggle ui-corner-right" )
          .mousedown(function() {
            wasOpen = input.autocomplete( "widget" ).is( ":visible" );
          })
          .click(function() {
            input.focus();

            // Close if already visible
            if ( wasOpen ) {
              return;
            }

            // Pass empty string as value to search for, displaying all results
            input.autocomplete( "search", "" );
          });
      },

      _source: function( request, response ) {
        var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
        response( this.element.children( "option" ).map(function() {
          var text = $( this ).text();
          if ( this.value && ( !request.term || matcher.test(text) ) )
            return {
              label: text,
              value: text,
              option: this
            };
        }) );
      },

      _removeIfInvalid: function( event, ui ) {

        // Selected an item, nothing to do
        if ( ui.item ) {
          this.element.change();
          return;
        }

        // Search for a match (case-insensitive)
        var value = this.input.val(),
          valueLowerCase = value.toLowerCase(),
          valid = false;
        var element = this.element;
        this.element.children( "option" ).each(function() {
          if ( $( this ).text().toLowerCase() === valueLowerCase ) {
            this.selected = valid = true;
            element.change();
            return false;
          }
        });

        // Found a match, nothing to do
        if ( valid ) {
          return;
        }

        // Remove invalid value
        this.input
          .val( "" )
          .attr( "title", value + " didn't match any item" )
          .tooltip( "open" );
        this.element.val( "" );
        this.element.change()
        this._delay(function() {
          this.input.tooltip( "close" ).attr( "title", "" );
        }, 2500 );
        this.input.autocomplete( "instance" ).term = "";
      },

      _destroy: function() {
        this.wrapper.remove();
        this.element.show();
      }
    });
  })( jQuery );
<commit_msg>blur combobox textbox on select<commit_after>  (function( $ ) {
    $.widget( "custom.combobox", {
      options:{
          size:"",
          blurOnSelect:true,
      },
      _create: function() {
        this.wrapper = $( "<span>" )
          .addClass( "custom-combobox" )
          .insertAfter( this.element );

        this.element.hide();
        this._createAutocomplete();
        this._createShowAllButton();
      },

      _createAutocomplete: function() {
        var selected = this.element.children( ":selected" ),
          value = selected.val() ? selected.text() : "";

        var size=this.options.size;
        if (size == "auto") {
          size=0;
          this.element.children("option").each(function(){
            if(!size) size = this.text.length;
            else size = Math.max(size, this.text.length);
          });
        }
        this.input = $( "<input>" )
          .appendTo( this.wrapper )
          .val( value )
          .attr( "title", "" )
          .attr("size", size)
          .addClass( "custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left" )
          .autocomplete({
            delay: 0,
            minLength: 0,
            source: $.proxy( this, "_source" )
          })
          .tooltip({
            tooltipClass: "ui-state-highlight"
          });

        this._on( this.input, {
          autocompleteselect: function( event, ui ) {
            ui.item.option.selected = true;
            this._trigger( "select", event, {
              item: ui.item.option
            });
            if (this.options.blurOnSelect) {
              this.input.val(ui.item.label);
              this.input.blur();
            }
            this.element.change();
          },

          autocompletechange: "_removeIfInvalid"
        });
      },

      _createShowAllButton: function() {
        var input = this.input,
          wasOpen = false;

        $( "<a>" )
          .attr( "tabIndex", -1 )
          .attr( "title", "Show All Items" )
          .tooltip()
          .appendTo( this.wrapper )
          .button({
            icons: {
              primary: "ui-icon-triangle-1-s"
            },
            text: false
          })
          .removeClass( "ui-corner-all" )
          .addClass( "custom-combobox-toggle ui-corner-right" )
          .mousedown(function() {
            wasOpen = input.autocomplete( "widget" ).is( ":visible" );
          })
          .click(function() {
            input.focus();

            // Close if already visible
            if ( wasOpen ) {
              return;
            }

            // Pass empty string as value to search for, displaying all results
            input.autocomplete( "search", "" );
          });
      },

      _source: function( request, response ) {
        var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
        response( this.element.children( "option" ).map(function() {
          var text = $( this ).text();
          if ( this.value && ( !request.term || matcher.test(text) ) )
            return {
              label: text,
              value: text,
              option: this
            };
        }) );
      },

      _removeIfInvalid: function( event, ui ) {

        // Selected an item, nothing to do
        if ( ui.item ) {
          this.element.change();
          return;
        }

        // Search for a match (case-insensitive)
        var value = this.input.val(),
          valueLowerCase = value.toLowerCase(),
          valid = false;
        var element = this.element;
        this.element.children( "option" ).each(function() {
          if ( $( this ).text().toLowerCase() === valueLowerCase ) {
            this.selected = valid = true;
            element.change();
            return false;
          }
        });

        // Found a match, nothing to do
        if ( valid ) {
          return;
        }

        // Remove invalid value
        this.input
          .val( "" )
          .attr( "title", value + " didn't match any item" )
          .tooltip( "open" );
        this.element.val( "" );
        this.element.change()
        this._delay(function() {
          this.input.tooltip( "close" ).attr( "title", "" );
        }, 2500 );
        this.input.autocomplete( "instance" ).term = "";
      },

      _destroy: function() {
        this.wrapper.remove();
        this.element.show();
      }
    });
  })( jQuery );
<|endoftext|>


<commit_before>WSData = {
    _book_data: null,
    _course_data: {},
    _uwemail_data: null,
    _hfs_data: null,
    _library_data: null,
    _tuition_data: null,
    _grade_data: {},
    _notice_data: null,
    _instructor_data: {},
    _link_data: null,
    _oquarter_data: null,
    _category_link_data: {},

    book_data: function() {
        return WSData._book_data;
    },

    normalized_course_data: function(term) {
        var course_data;
        if (term) {
            course_data = WSData.course_data_for_term(term);
        } else {
            course_data = WSData.current_course_data();
        }
        if (course_data) {
            WSData._normalize_instructors(course_data);
        }
        return course_data;
    },

    current_course_data: function() {
        return WSData._course_data["current"];
    },

    course_data: function() {
        if (window.console) {
            console.warn("Use WSData.current_course_data");
        }
        return WSData.current_course_data();
    },

    course_data_for_term: function(term) {
        return WSData._course_data[term];
    },

    grade_data_for_term: function(term) {
        if (!term) { term = ''; }
        return WSData._grade_data[term];
    },

    hfs_data: function() {
        return WSData._hfs_data;
    },

    instructor_data: function(regid) {
        return WSData._instructor_data[regid];
    },

    library_data: function() {
        return WSData._library_data;
    },

    link_data: function() {
        return WSData._link_data;
    },

    notice_data: function() {
        return WSData._notice_data;
    },

    oquarter_data: function() {
        return WSData._oquarter_data;
    },

    category_link_data: function(category) {
        return WSData._category_link_data[category];
    },

    tuition_data: function() {
        return WSData._tuition_data;
    },

    uwemail_data: function() {
        return WSData._uwemail_data;
    },

    fetch_book_data: function(callback, term, err_callback, args) {
        if (WSData._book_data === null) {
            $.ajax({
                url: "/mobile/api/v1/book/" + term,
                dataType: "JSON",

                type: "GET",
                accepts: {html: "text/html"},
                success: function(results) {
                    WSData._book_data = results;
                    callback.apply(null, args);
                },
                error: function(xhr, status, error) {
                        WSData._book_data = null;
                        if (err_callback !== undefined){
                            err_callback.apply(null, args);
                        }
                    }
                });
            }
            else {
                window.setTimeout(function() {
                    callback.apply(null, args);
                }, 0);
            }
        },

    fetch_current_course_data: function(callback, args) {
        return WSData.fetch_course_data_for_term("current", callback, args);
    },

    fetch_course_data_for_term: function(term, callback, args) {
        if (!WSData._course_data[term]) {
            $.ajax({
                url: "/mobile/api/v1/schedule/"+term,
                dataType: "JSON",

                type: "GET",
                accepts: {html: "text/html"},
                success: function(results) {
                    // MUWM-549 and MUWM-552
                    var sections = results.sections;
                    var section_count = sections.length;
                    for (var index = 0; index < section_count; index++) {
                        section = sections[index];

                        var canvas_url = section["canvas_url"];
                        if (canvas_url) {
                            if (section["class_website_url"] == canvas_url) {
                                section["class_website_url"] = null;
                            }
                            var matches = canvas_url.match(/\/([0-9]+)$/);
                            var canvas_id = matches[1];
                            var alternate_url = "https://uw.instructure.com/courses/"+canvas_id;

                            if (section["class_website_url"] == alternate_url) {
                                section["class_website_url"] = null;
                            }
                        }
                    }
                    WSData._course_data[term] = results;
                    if (callback !== null) {
                        callback.apply(null, args);
                    }
                },
                error: function(xhr, status, error) {
                    showError();
                }
            });
        }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }

    },

    fetch_grades_for_term: function(term, callback, args) {
        if (!term) { term = ''; }

        if (WSData.course_data_for_term(term)) {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
            return;
        }

        $.ajax({
            url: "/mobile/api/v1/grades/"+term,
            type: 'GET',
            success: function(results) {
                WSData._grade_data[term] = results;
                callback.apply(null, args);
            },
            error: function() {
                showError();
            }
        });
    },

    fetch_current_week_data: function(callback, args) {
        $.ajax({
            url: "/mobile/api/v1/current_week/",
            type: 'GET',
            success: function(results) {
                callback.apply(null, [results, args]);
            },
            error: function() {
                showError();
            }
        });
    },

    fetch_course_data: function(callback, args) {
        console.warn("Use WSData.fetch_current_course_data instead");
    },

    fetch_link_data: function(callback, args) {
            if (WSData._link_data === null) {
                $.ajax({
                    url: "/mobile/api/v1/links/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._link_data = results;
                        callback.apply(null, args);
                    },
                    error: function(xhr, status, error) {
                        showError();
                    }
                });
            }
            else {
                window.setTimeout(function() {
                    callback.apply(null, args);
                }, 0);
            }
        },

    fetch_instructor_data: function(callback, args) {
        var instructor_regid = args[1];
        if (WSData._instructor_data[instructor_regid] === undefined) {
            $.ajax({
                    url: "/mobile/api/v1/person/"+instructor_regid,
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._instructor_data[instructor_regid] = results;
                        callback.apply(null, args);
                    },
                    error: function(xhr, status, error) {
                        showError();
                    }
                });
            }
            else {
                window.setTimeout(function() {
                    callback.apply(null, args);
                }, 0);
            }
        },

    normalize_instructors_for_term: function(term) {
        WSData._normalize_instructors(WSData.course_data_for_term(term));
    },

    normalize_instructors_for_current_term: function() {
        WSData._normalize_instructors(WSData.current_course_data());
    },

    _sort_instructors_by_last_name: function(a, b) {
        if (a.surname < b.surname) return -1;
        if (a.surname > b.surname) return 1;
        return 0;
    },

    _normalize_instructors: function(data) {
        if (!data.sections.length) {
            return;
        }
        if (data.sections[0].instructors !== undefined) {
            return;
        }

        var section_index = 0;
        for (section_index = 0; section_index < data.sections.length; section_index++) {
            var section = data.sections[section_index];
            section.instructors = [];

            var instructors = {};
            var meeting_index = 0;
            for (meeting_index = 0; meeting_index < section.meetings.length; meeting_index++) {
                var meeting = section.meetings[meeting_index];
                var instructor_index = 0;
                for (instructor_index = 0; instructor_index < meeting.instructors.length; instructor_index++) {
                    var instructor = meeting.instructors[instructor_index];

                    if (instructors[instructor.uwregid] === undefined) {
                        section.instructors.push(instructor);
                    }
                    instructors[instructor.uwregid] = true;
                }
            }
            section.instructors = section.instructors.sort(WSData._sort_instructors_by_last_name);
        }
    },

    normalize_instructors: function() {
        if (window.console) {
            console.warn("Use WSData.normalize_instructors_for_current_term");
        }
        WSData.normalize_instructors_for_current_term();
    },

    fetch_hfs_data: function(callback, args) {
        if (WSData._hfs_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/hfs/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._hfs_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        showError();
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_library_data: function(callback, args) {
        if (WSData._library_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/library/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._library_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        showError();
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_tuition_data: function(callback, args) {
        if (WSData._tuition_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/finance/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._tuition_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        showError();
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_notice_data: function(callback, args) {
        if (WSData._notice_data === null) {
            $.ajax({
                url: "/mobile/api/v1/notices/",
                dataType: "JSON",

                type: "GET",
                accepts: {html: "text/html"},
                success: function(results) {
                    WSData._notice_data = results;
                    callback.apply(null, args);
                },
                error: function(xhr, status, error) {
                    showError();
                }
            });
        }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_oquarter_data: function(callback, args) {
        if (WSData._oquarter_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/oquarters/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._oquarter_data = results;
                        callback.apply(null, args);
                        },
                    error: function(xhr, status, error) {
                        showError();
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_category_links: function(callback, args) {
        var category = args[0];
        if (WSData._category_link_data[category] === undefined) {
            $.ajax({
                    url: "/mobile/api/v1/categorylinks/" + category,
                    dataType: "JSON",
                    type: "GET",
                    accepts: {html: "application/json"},
                    success: function(results) {
                        WSData._category_link_data[category] = results;
                        callback.apply(null, args);
                        },
                    error: function(xhr, status, error) {
                        showError();
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_uwemail_data: function(callback, args) {
        if (WSData._uwemail_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/uwemail/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._uwemail_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        showError();
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    save_links: function(links) {
        var csrf_token = $("input[name=csrfmiddlewaretoken]")[0].value;
        $.ajax({
                url: "/mobile/api/v1/links/",
                dataType: "JSON",
                data: JSON.stringify(links),
                type: "PUT",
                accepts: {html: "text/html"},
                headers: {
                     "X-CSRFToken": csrf_token
                },
                success: function(results) {
                },
                error: function(xhr, status, error) {
                }
       });
    },

    log_interaction: function(interaction_type, term) {
        var logging_term;
        if(term === undefined) {
            logging_term = "";
        }
        else {
            logging_term = "_term_" + term.replace(/[^a-z0-9]/gi, '_');
        }

        $.ajax({
                url: "/mobile/logger/" + interaction_type + logging_term,
                type: "GET",
                success: function(results) {},
                error: function(xhr, status, error) {}
        });
    },

};
<commit_msg>Allows callers to supply their own error callback rather than the the page-destroying default callback<commit_after>WSData = {
    _book_data: null,
    _course_data: {},
    _uwemail_data: null,
    _hfs_data: null,
    _library_data: null,
    _tuition_data: null,
    _grade_data: {},
    _notice_data: null,
    _instructor_data: {},
    _link_data: null,
    _oquarter_data: null,
    _category_link_data: {},

    book_data: function() {
        return WSData._book_data;
    },

    normalized_course_data: function(term) {
        var course_data;
        if (term) {
            course_data = WSData.course_data_for_term(term);
        } else {
            course_data = WSData.current_course_data();
        }
        if (course_data) {
            WSData._normalize_instructors(course_data);
        }
        return course_data;
    },

    current_course_data: function() {
        return WSData._course_data["current"];
    },

    course_data: function() {
        if (window.console) {
            console.warn("Use WSData.current_course_data");
        }
        return WSData.current_course_data();
    },

    course_data_for_term: function(term) {
        return WSData._course_data[term];
    },

    grade_data_for_term: function(term) {
        if (!term) { term = ''; }
        return WSData._grade_data[term];
    },

    hfs_data: function() {
        return WSData._hfs_data;
    },

    instructor_data: function(regid) {
        return WSData._instructor_data[regid];
    },

    library_data: function() {
        return WSData._library_data;
    },

    link_data: function() {
        return WSData._link_data;
    },

    notice_data: function() {
        return WSData._notice_data;
    },

    oquarter_data: function() {
        return WSData._oquarter_data;
    },

    category_link_data: function(category) {
        return WSData._category_link_data[category];
    },

    tuition_data: function() {
        return WSData._tuition_data;
    },

    uwemail_data: function() {
        return WSData._uwemail_data;
    },

    fetch_book_data: function(callback, term, err_callback, args) {
        if (WSData._book_data === null) {
            $.ajax({
                url: "/mobile/api/v1/book/" + term,
                dataType: "JSON",

                type: "GET",
                accepts: {html: "text/html"},
                success: function(results) {
                    WSData._book_data = results;
                    callback.apply(null, args);
                },
                error: function(xhr, status, error) {
                        WSData._book_data = null;
                        if (err_callback !== undefined){
                            err_callback.call(null, args);
                        }
                    }
                });
            }
            else {
                window.setTimeout(function() {
                    callback.apply(null, args);
                }, 0);
            }
        },

    fetch_current_course_data: function(callback, err_callback, args) {
        return WSData.fetch_course_data_for_term("current", callback, err_callback, args);
    },

    fetch_course_data_for_term: function(term, callback, err_callback, args) {
        if (!WSData._course_data[term]) {
            $.ajax({
                url: "/mobile/api/v1/schedule/"+term,
                dataType: "JSON",

                type: "GET",
                accepts: {html: "text/html"},
                success: function(results) {
                    // MUWM-549 and MUWM-552
                    var sections = results.sections;
                    var section_count = sections.length;
                    for (var index = 0; index < section_count; index++) {
                        section = sections[index];

                        var canvas_url = section["canvas_url"];
                        if (canvas_url) {
                            if (section["class_website_url"] == canvas_url) {
                                section["class_website_url"] = null;
                            }
                            var matches = canvas_url.match(/\/([0-9]+)$/);
                            var canvas_id = matches[1];
                            var alternate_url = "https://uw.instructure.com/courses/"+canvas_id;

                            if (section["class_website_url"] == alternate_url) {
                                section["class_website_url"] = null;
                            }
                        }
                    }
                    WSData._course_data[term] = results;
                    if (callback !== null) {
                        callback.apply(null, args);
                    }
                },
                error: function(xhr, status, error) {
                    err_callback.call(null, status, error);
                }
            });
        }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }

    },

    fetch_grades_for_term: function(term, callback, err_callback, args) {
        if (!term) { term = ''; }

        if (WSData.course_data_for_term(term)) {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
            return;
        }

        $.ajax({
            url: "/mobile/api/v1/grades/"+term,
            type: 'GET',
            success: function(results) {
                WSData._grade_data[term] = results;
                err_callback.call(null, status, error);
            },
            error: function() {
                err_callback.call(null, status, error);
            }
        });
    },

    fetch_current_week_data: function(callback, err_callback, args) {
        $.ajax({
            url: "/mobile/api/v1/current_week/",
            type: 'GET',
            success: function(results) {
                callback.apply(null, [results, args]);
            },
            error: function() {
                err_callback.call(null, status, error);
            }
        });
    },

    fetch_course_data: function(callback, args) {
        console.warn("Use WSData.fetch_current_course_data instead");
    },

    fetch_link_data: function(callback, err_callback, args) {
            if (WSData._link_data === null) {
                $.ajax({
                    url: "/mobile/api/v1/links/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._link_data = results;
                        callback.apply(null, args);
                    },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                    }
                });
            }
            else {
                window.setTimeout(function() {
                    callback.apply(null, args);
                }, 0);
            }
        },

    fetch_instructor_data: function(callback, err_callback, args) {
        var instructor_regid = args[1];
        if (WSData._instructor_data[instructor_regid] === undefined) {
            $.ajax({
                    url: "/mobile/api/v1/person/"+instructor_regid,
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._instructor_data[instructor_regid] = results;
                        callback.apply(null, args);
                    },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                    }
                });
            }
            else {
                window.setTimeout(function() {
                    callback.apply(null, args);
                }, 0);
            }
        },

    normalize_instructors_for_term: function(term) {
        WSData._normalize_instructors(WSData.course_data_for_term(term));
    },

    normalize_instructors_for_current_term: function() {
        WSData._normalize_instructors(WSData.current_course_data());
    },

    _sort_instructors_by_last_name: function(a, b) {
        if (a.surname < b.surname) return -1;
        if (a.surname > b.surname) return 1;
        return 0;
    },

    _normalize_instructors: function(data) {
        if (!data.sections.length) {
            return;
        }
        if (data.sections[0].instructors !== undefined) {
            return;
        }

        var section_index = 0;
        for (section_index = 0; section_index < data.sections.length; section_index++) {
            var section = data.sections[section_index];
            section.instructors = [];

            var instructors = {};
            var meeting_index = 0;
            for (meeting_index = 0; meeting_index < section.meetings.length; meeting_index++) {
                var meeting = section.meetings[meeting_index];
                var instructor_index = 0;
                for (instructor_index = 0; instructor_index < meeting.instructors.length; instructor_index++) {
                    var instructor = meeting.instructors[instructor_index];

                    if (instructors[instructor.uwregid] === undefined) {
                        section.instructors.push(instructor);
                    }
                    instructors[instructor.uwregid] = true;
                }
            }
            section.instructors = section.instructors.sort(WSData._sort_instructors_by_last_name);
        }
    },

    normalize_instructors: function() {
        if (window.console) {
            console.warn("Use WSData.normalize_instructors_for_current_term");
        }
        WSData.normalize_instructors_for_current_term();
    },

    fetch_hfs_data: function(callback, err_callback, args) {
        if (WSData._hfs_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/hfs/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._hfs_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_library_data: function(callback, err_callback, args) {
        if (WSData._library_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/library/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._library_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_tuition_data: function(callback, err_callback, args) {
        if (WSData._tuition_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/finance/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._tuition_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_notice_data: function(callback, err_callback, args) {
        if (WSData._notice_data === null) {
            $.ajax({
                url: "/mobile/api/v1/notices/",
                dataType: "JSON",

                type: "GET",
                accepts: {html: "text/html"},
                success: function(results) {
                    WSData._notice_data = results;
                    callback.apply(null, args);
                },
                error: function(xhr, status, error) {
                    err_callback.call(null, status, error);
                }
            });
        }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_oquarter_data: function(callback, err_callback, args) {
        if (WSData._oquarter_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/oquarters/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._oquarter_data = results;
                        callback.apply(null, args);
                        },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_category_links: function(callback, err_callback, args) {
        var category = args[0];
        if (WSData._category_link_data[category] === undefined) {
            $.ajax({
                    url: "/mobile/api/v1/categorylinks/" + category,
                    dataType: "JSON",
                    type: "GET",
                    accepts: {html: "application/json"},
                    success: function(results) {
                        WSData._category_link_data[category] = results;
                        callback.apply(null, args);
                        },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    fetch_uwemail_data: function(callback, err_callback, args) {
        if (WSData._uwemail_data === null) {
            $.ajax({
                    url: "/mobile/api/v1/uwemail/",
                    dataType: "JSON",

                    type: "GET",
                    accepts: {html: "text/html"},
                    success: function(results) {
                        WSData._uwemail_data = results;
                        if (callback !== null) {
                            callback.apply(null, args);
                        }
                    },
                    error: function(xhr, status, error) {
                        err_callback.call(null, status, error);
                        }
                    });
              }
        else {
            window.setTimeout(function() {
                callback.apply(null, args);
            }, 0);
        }
    },

    save_links: function(links) {
        var csrf_token = $("input[name=csrfmiddlewaretoken]")[0].value;
        $.ajax({
                url: "/mobile/api/v1/links/",
                dataType: "JSON",
                data: JSON.stringify(links),
                type: "PUT",
                accepts: {html: "text/html"},
                headers: {
                     "X-CSRFToken": csrf_token
                },
                success: function(results) {
                },
                error: function(xhr, status, error) {
                }
       });
    },

    log_interaction: function(interaction_type, term) {
        var logging_term;
        if(term === undefined) {
            logging_term = "";
        }
        else {
            logging_term = "_term_" + term.replace(/[^a-z0-9]/gi, '_');
        }

        $.ajax({
                url: "/mobile/logger/" + interaction_type + logging_term,
                type: "GET",
                success: function(results) {},
                error: function(xhr, status, error) {}
        });
    },

};
<|endoftext|>


<commit_before>import uuid from 'uuid'
import createSecret from '../utils/createSecret'
import promisify from 'q-level'
import levelup from 'levelup'

const db = levelup('./data/db/service', { valueEncoding: 'json' })
const dbPromise = promisify(db)
dbPromise.find = require('../utils/find')(db)

/**
 * group_list: ['account', 'file']
 * group_file [{appId: 'fdada', socketId, status: 'adfad'}, {...}]
 * socket_jkfdlahgklh23hkhfahdf: {
 *  appId: 'aaa',
 *  appName: 'account',
 *  permission: ['admin'],
 *  socketId: 'ccc'
 * }
 */
export const emptySocket = () => {
  return new Promise(async (resolve, reject) => {
      db.get('group_list', async (err, list) => {
        if (err) {
          try {
            await dbPromise.put('group_list', [])
            resolve()
          } catch(e){
            console.log(e)
            reject(e)
          }
          return false
        }

        try {
          const allAppMetaGroup = await Promise.all(list.map(group => {
            return dbPromise.get(`group_${group}`, {valueEncoding: 'json'})
          }))
          const allAppMeta = allAppMetaGroup.reduce((previous, current) => previous.concat(current), [])
          const allSocketIds = allAppMeta.map(app => app.socketId).filter(val => typeof val == 'string')
          await Promise.all(allSocketIds.map(socketId => dbPromise.del(`socket_${socketId}`)))
          resolve()
        } catch(e){
          if (typeof e == 'string') return reject(e)
          console.log(e.stack||e)
          reject('EMPTY_SOCKET_FAIL')
        }
      })

  })
}

export const getAllAppMeta = () => {
  return new Promise(async (resolve, reject) => {
    try {
      const list = await dbPromise.find({prefix: 'service_'})
      resolve(list)
    } catch(e){
      reject(e)
    }
  })
}

/**
 * delete socket
 */
export const deleteSocket = (socketId) => {
  return new Promise(async (resolve, reject) => {
    try {
      const removed = await dbPromise.del(`socket_${socketId}`)
      // todo remove from group
    } catch(e){
      reject(e)
    }
  })
}

/**
 * 根据socketId获取app信息
 * @param socketId
 * @returns {Promise}
 */
export const getAppBySocketId = (socketId) => {
  return new Promise(async (resolve, reject) => {
    try {
      const socket = await dbPromise.get(`socket_${socketId}`, {valueEncoding: 'json'})
      resolve(socket)
    } catch(e){
      if (typeof e == 'string') return reject(e)
      console.log(e)
      reject('NOT_FOUND')
    }
  })
}

/**
 * getSocketByAppId
 * @param appId
 * @returns {Promise}
 */
export const getSocketByAppId = (appId) => {
  return new Promise(async (resolve, reject) => {
    try {
      const app = await dbPromise.get(`service_${appId}`)
      const group = await dbPromise.get(`group_${app.appName}`)
      const targetAppIndex = group.findIndex(item => item.appId == appId)
      if (targetAppIndex < 0) throw 'APP_NOT_FOUND'
      const socket = await dbPromise.get(`socket_${group[targetAppIndex].socketId}`)
      resolve(socket)
    } catch(e) {
      console.log(e.stack||e)
      reject(e)
    }
  })
}

/**
 * 绑定app到socket
 * @param socketId
 * @param registerInfo
 * @returns {Promise}
 */
export const bindAppToSocket = (socketId, registerInfo) => {
  return new Promise(async (resolve, reject) => {
    try {
      const app = await dbPromise.get(`service_${registerInfo.appId}`)
      if (app.appSecret != registerInfo.appSecret) throw 'ERROR_REGISTER_INFO'
      const socketData = Object.assign({}, app, {socketId: socketId})
      await dbPromise.put(`socket_${socketId}`, socketData)
      const group = await getGroupUpset(app.appName)
      const targetAppIndex = group.findIndex(item => item.appId == app.appId)
      group[targetAppIndex] = Object.assign({}, group[targetAppIndex], {socketId: socketId, status: 1})
      await dbPromise.put(`group_${app.appName}`, group)
      resolve(socketData)
    } catch(e){
      if (typeof e == 'string') return reject(e)
      console.log(e)
      reject('REGISTER_FAIL')
    }
  })
}

/**
 * 获取处理请求的app, 并作负载均衡
 */
export const getResSocketIdWithBalance = (importAppName) => {
  return new Promise(async (resolve, reject) => {
    try {
      const group = await dbPromise.get(`group_${importAppName}`, {valueEncoding: 'json'})
      if (group.length == 0) throw 'TARGET_SERVICE_OFFLINE'
      if (group.length == 1) return resolve(group[0].socketId)
      return resolve(group[0].socketId)

    } catch(e){
      if (typeof e == 'string') return reject(e)
      console.log(e.stack||e)
      reject('GET_SOCKET_FAIL')
    }
  })
}

/**
 * getGroupUpset
 * @returns {*}
 */
const getGroupUpset = (appName) => {
  return new Promise(async (resolve, reject) => {
    try {
      resolve(await dbPromise.get(`group_${appName}`))
    } catch(e){
      await dbPromise.put(`group_${appName}`, [], {valueEncoding: 'json'})
      resolve([])
    }
  })
}

/**
 * 新建service
 */
export const createServiceByName = (appName) => {
  return new Promise(async (resolve, reject) => {
    try {
      const group = await getGroupUpset(appName)

      const nextService = {
        appId: uuid.v4(),
        appName: appName,
        appSecret: createSecret()
      }

      group.push({
        appId: nextService.appId,
        socketId: null,
        status: 0,
      })

      await Promise.all([
        dbPromise.put(`group_${appName}`, group, {valueEncoding: 'json'}),
        dbPromise.put(`service_${nextService.appId}`, nextService, {valueEncoding: 'json'})
      ])

      resolve(nextService)
    } catch(e) {
      if (typeof e == 'string') return reject(e)
      console.log(e)
      reject('CREATE_SERVICE_FAIL')
    }
  })
}

/**
 * 导入service
 */
export const importServiceFromConfig = (service) => {
  return new Promise(async (resolve, reject) => {
    try {
      const group = await getGroupUpset(service.appName)
      const index = group.findIndex(item => {
        console.log(item.appId, service.appId)
        return item.appId == service.appId
      })

      if (index > -1) return resolve(group[index])

      const newService = {
        appId: service.appId,
        appName: service.appName,
        appSecret: service.appSecret
      }

      group.push({
        appId: newService.appId,
        socketId: null,
        status: 0,
      })

      await Promise.all([
        dbPromise.put(`group_${service.appName}`, group),
        dbPromise.put(`service_${newService.appId}`, newService)
      ])

      resolve(newService)
    } catch(e) {
      if (typeof e == 'string') return reject(e)
      console.log(e.stack||e)
      reject('CREATE_SERVICE_FAIL')
    }
  })
}
<commit_msg>db: add handle for socket disconnect.<commit_after>import uuid from 'uuid'
import createSecret from '../utils/createSecret'
import promisify from 'q-level'
import levelup from 'levelup'

const db = levelup('./data/db/service', { valueEncoding: 'json' })
const dbPromise = promisify(db)
dbPromise.find = require('../utils/find')(db)

/**
 * group_list: ['account', 'file']
 * group_account [{appId: 'fdada', socketId, status: 1}, {...}]
 * socket_jkfdlahgklh23hkhfahdf: {
 *  appId: 'aaa-123-fda-123',
 *  appName: 'account',
 *  permission: ['admin'],
 *  socketId: 'ccc'
 * }
 */
export const emptySocket = () => {
  return new Promise(async (resolve, reject) => {
      db.get('group_list', async (err, list) => {
        if (err) {
          try {
            await dbPromise.put('group_list', [])
            resolve()
          } catch(e){
            console.log(e)
            reject(e)
          }
          return false
        }

        try {
          const allAppMetaGroup = await Promise.all(list.map(group => {
            return dbPromise.get(`group_${group}`, {valueEncoding: 'json'})
          }))
          const allAppMeta = allAppMetaGroup.reduce((previous, current) => previous.concat(current), [])
          const allSocketIds = allAppMeta.map(app => app.socketId).filter(val => typeof val == 'string')
          await Promise.all(allSocketIds.map(socketId => dbPromise.del(`socket_${socketId}`)))
          resolve()
        } catch(e){
          if (typeof e == 'string') return reject(e)
          console.log(e.stack||e)
          reject('EMPTY_SOCKET_FAIL')
        }
      })

  })
}

export const getAllAppMeta = () => {
  return new Promise(async (resolve, reject) => {
    try {
      const list = await dbPromise.find({prefix: 'service_'})
      resolve(list)
    } catch(e){
      reject(e)
    }
  })
}

/**
 * delete socket
 */
export const deleteSocket = (socketId) => {
  return new Promise(async (resolve, reject) => {
    try {
      const socketInfo = await dbPromise.get(`socket_${socketId}`)
      await dbPromise.del(`socket_${socketId}`)
      const group = await dbPromise.get(`group_${socketInfo.appName}`)
      const nextGroup = group.map(item => {
        if (item.appId == socketInfo.appId) {
          item.socketId = ''
          item.status = 0
        }
        return item
      })
      await dbPromise.put(`group_${socketInfo.appName}`, nextGroup)
      resolve(1)
    } catch(e){
      reject(e)
    }
  })
}

/**
 * 根据socketId获取app信息
 * @param socketId
 * @returns {Promise}
 */
export const getAppBySocketId = (socketId) => {
  return new Promise(async (resolve, reject) => {
    try {
      const socket = await dbPromise.get(`socket_${socketId}`, {valueEncoding: 'json'})
      resolve(socket)
    } catch(e){
      if (typeof e == 'string') return reject(e)
      console.log(e)
      reject('NOT_FOUND')
    }
  })
}

/**
 * getSocketByAppId
 * @param appId
 * @returns {Promise}
 */
export const getSocketByAppId = (appId) => {
  return new Promise(async (resolve, reject) => {
    try {
      const app = await dbPromise.get(`service_${appId}`)
      const group = await dbPromise.get(`group_${app.appName}`)
      const targetAppIndex = group.findIndex(item => item.appId == appId)
      if (targetAppIndex < 0) throw 'APP_NOT_FOUND'
      const socket = await dbPromise.get(`socket_${group[targetAppIndex].socketId}`)
      resolve(socket)
    } catch(e) {
      console.log(e.stack||e)
      reject(e)
    }
  })
}

/**
 * 绑定app到socket
 * @param socketId
 * @param registerInfo
 * @returns {Promise}
 */
export const bindAppToSocket = (socketId, registerInfo) => {
  return new Promise(async (resolve, reject) => {
    try {
      const app = await dbPromise.get(`service_${registerInfo.appId}`)
      if (app.appSecret != registerInfo.appSecret) throw 'ERROR_REGISTER_INFO'
      const socketData = Object.assign({}, app, {socketId: socketId})
      await dbPromise.put(`socket_${socketId}`, socketData)
      const group = await getGroupUpset(app.appName)
      const targetAppIndex = group.findIndex(item => item.appId == app.appId)
      group[targetAppIndex] = Object.assign({}, group[targetAppIndex], {socketId: socketId, status: 1})
      await dbPromise.put(`group_${app.appName}`, group)
      resolve(socketData)
    } catch(e){
      if (typeof e == 'string') return reject(e)
      console.log(e)
      reject('REGISTER_FAIL')
    }
  })
}

/**
 * 获取处理请求的app, 并作负载均衡
 */
export const getResSocketIdWithBalance = (importAppName) => {
  return new Promise(async (resolve, reject) => {
    try {
      const group = await dbPromise.get(`group_${importAppName}`, {valueEncoding: 'json'})
      const onlineItems = group.filter(service => service.status == 1)
      if (onlineItems.length == 0) throw 'TARGET_SERVICE_OFFLINE'
      if (onlineItems.length == 1) return resolve(onlineItems[0].socketId)
      const ts = String(Date.now())
      const randomNumber = Number(ts[ts.length - 1])
      const randomIndex = Math.floor(randomNumber * onlineItems.length / 10)
      return resolve(group[randomIndex].socketId)
    } catch(e){
      if (typeof e == 'string') return reject(e)
      console.log(e.stack||e)
      reject('GET_SOCKET_FAIL')
    }
  })
}

/**
 * getGroupUpset
 * @returns {*}
 */
const getGroupUpset = (appName) => {
  return new Promise(async (resolve, reject) => {
    try {
      resolve(await dbPromise.get(`group_${appName}`))
    } catch(e){
      await dbPromise.put(`group_${appName}`, [], {valueEncoding: 'json'})
      resolve([])
    }
  })
}

/**
 * 新建service
 */
export const createServiceByName = (appName) => {
  return new Promise(async (resolve, reject) => {
    try {
      const group = await getGroupUpset(appName)

      const nextService = {
        appId: uuid.v4(),
        appName: appName,
        appSecret: createSecret()
      }

      group.push({
        appId: nextService.appId,
        socketId: null,
        status: 0,
      })

      await Promise.all([
        dbPromise.put(`group_${appName}`, group, {valueEncoding: 'json'}),
        dbPromise.put(`service_${nextService.appId}`, nextService, {valueEncoding: 'json'})
      ])

      resolve(nextService)
    } catch(e) {
      if (typeof e == 'string') return reject(e)
      console.log(e)
      reject('CREATE_SERVICE_FAIL')
    }
  })
}

/**
 * 导入service
 */
export const importServiceFromConfig = (service) => {
  return new Promise(async (resolve, reject) => {
    try {
      const group = await getGroupUpset(service.appName)
      const index = group.findIndex(item => {
        console.log(item.appId, service.appId)
        return item.appId == service.appId
      })

      if (index > -1) return resolve(group[index])

      const newService = {
        appId: service.appId,
        appName: service.appName,
        appSecret: service.appSecret
      }

      group.push({
        appId: newService.appId,
        socketId: null,
        status: 0,
      })

      await Promise.all([
        dbPromise.put(`group_${service.appName}`, group),
        dbPromise.put(`service_${newService.appId}`, newService)
      ])

      resolve(newService)
    } catch(e) {
      if (typeof e == 'string') return reject(e)
      console.log(e.stack||e)
      reject('CREATE_SERVICE_FAIL')
    }
  })
}
<|endoftext|>