diff --git a/3.4.0.VERSION b/3.5.0.VERSION similarity index 100% rename from 3.4.0.VERSION rename to 3.5.0.VERSION diff --git a/README.md b/README.md index d27b654..c89c2cf 100644 --- a/README.md +++ b/README.md @@ -1,200 +1,48 @@ -### 说明: -此开源版本为基础功能版本,只有消息推送的基础功能!netty版本,和mina版本结构和功能完全一致,大家可以选择自己喜欢的或者合适的版本学习或者使用! +#### 项目介绍 +CIM是一套基于apache mina和Netty框架下的推送系统,平时我们一直使用第三方的推送SDK,比如极光,百度推送,小米推送,以及腾讯信鸽等来支撑自己的移动端的业务,或许有一些企业有着自己一套即时通讯系统的需求,那么CIM为您提供了一个解决方案,目前CIM支撑 webcokset,android,ios,桌面应用,系统应用等多端接入支持,CIM服务端使用springboot搭建仅仅拥有消息推送的功能,关于数据缓存与持久化都需要使用者自己开发,并且配备了比较完整的使用文档。希望能为您带来一些价值。 + -### 侣信专业版2.5.0版本发布 -### [http://farsunset.com](http://farsunset.com) +#### 目录说明 + +1. cim_for_mina目录下为mina版本实现 +2. cim_for_netty目录下为netty版本实现 +3. doc目录中是相关使用文档,以及protubuf结构体文件 +5. cim-android-sdk 是android客户端封装的的jar包 +6. cim-java-sdk 是java版客户端封装的的jar包 +7. cim-server-sdk 是cim-server 用到的封装的jar +8. cim-boot-server是springboot服务端工程,是eclipse4.x基于gradle构建插件 +9. cim-client-android 是android客户端 android studio工具 +10. 说明.txt是服务端环境搭建说明 + + +#### 建议反馈 + +智者千虑必有一失,如果再使用中发现任何问题或者有优化建议,您可以通过QQ3979434或者 邮箱3979434@qq.com向我反馈,当然目前还缺少IOS客户端sdk和.net客户端SDK,如果您有兴趣可以参与开发,CIM将会越来越好。 -### 前言: +#### 相关项目 - 现在市面上有很多 xmpp协议的即时通讯方案,openfire androidpn,等等。它们都是使用了apache mina开发,但是这些东西基本都需要二次改造开发。而且改动还很大,我也看过这些东西的源码,发现代码结构不太理想,耦合的情况太多,实在不好扩展。所谓XMPP 协议。只不过是别人使用mina 自定义了一个消息编码解码协议。通俗的讲就是,xml形式消息的编码与解码,我们完全没有必要在国外这套不成熟的openfire 与xmpp 上耗费过多的精力去研究,我们完全可以通过apache mina 自定义自己的通讯协议,并可以为它使用自己的名字。我们不要盲目崇拜国外的有些东西,自己掌握原理,才是最重要的,各位切记~ +侣信专业版是基于CIM面向中小企业和者各类团队组织内部交流使用工具。具有丰富的功能,聊天,群组,部门组织,内部朋友圈等功能。它可以在局域网内使用保证沟通的信息安全,并且它是完全免费的,而且可以及时获得更新。 +#### [http://farsunset.com](http://farsunset.com) - 这套IM系统为我个人自主开发 使用了 apache mina ,主要功能为 服务端和客户端,客户端 到客户端的即时通信,可以支持包括文字 图片,语音等任何消息形式 服务端使用的 struts2+spring3和 apache mina android端 也使用的apache mina。这套IM系统结构还是非常清晰合理的,非常容易扩展和改造,下面是android版本 的 demo的目的是只是一个演示 ,可以参照它的代码,使用这套系统开发自己的东西,核心价值是一套高灵活性,相对标准化的即时通讯解决方案,即时聊天只是它的一种运用途径! - - -### 服务端集群配置方案 -## 服务端修改 -1.多台服务器集群配置,首先需要重写SessionManager接口(参考com.farsunset.ichat.cim.session.ClusterSessionManager.java),用户登录时,将账号和服务器IP 存入数据库中,这样就可以统计各台服务器接受的连接数量。 -2.客户端连接服务器时,服务端为客户端动态分配 服务器IP,每次分配 较为空闲的服务器IP -3.服务端接受消息后 通过接收者账号查询出对应的Iosession,和 登录的 服务器IP,然后将消息信息传往目标服务器处理发送 - - -### 客户端端修改 -1.客户端登录时将不在是固定的服务器IP 而是先通过http接口获取到当前空闲的服务器IP,然后登录 - -### 成功案例 -### http://blog.csdn.net/xx753277/article/details/17512255 -### http://blog.csdn.net/xx753277/article/details/45535981 - - - - -### 客户端接收消息 -![image](http://staticres.oss-cn-hangzhou.aliyuncs.com/cim-android_client.png) - -### 服务端消息 web入口 -### http://192.168.1.11:8080/ichat-server +#### 功能预览 +1.控制台页面 ![image](http://staticres.oss-cn-hangzhou.aliyuncs.com/cim-server.png) +2.Android客户端 +![image](http://staticres.oss-cn-hangzhou.aliyuncs.com/cim-android_client.png) +3.Web客户端 +![image](http://staticres.oss-cn-hangzhou.aliyuncs.com/cim-server-message.png) +#### 更新日志 +------------------------------------------------------------------------------------------- +版本:3.5.0/时间:2018-08-22 -### 常用功能接口 -所有开放外部接口都集中在 -com.farsunset.cim.client.android.CIMPushManager +1.服务端由原来的 spring+struts2修改为springboot工程 + +2.全面重新websocket的实现,全面拥抱protobuf替换josn序列化方式,更加高效 -#### 1.1连接服务器 -```java -/** -* 初始化,连接服务端,在程序启动页或者 在Application里调用 -* @param context -* @param ip -* @param port -*/ -public static void connect(Context context,String ip,int port) - -//示例 -CIMPushManager.connect(context,"125.12.35.231",28888); - -``` - -#### 1.2绑定账号到服务端 -```java -/** -* 设置一个账号登录到服务端 -* @param account 用户唯一ID -*/ -public static void bindAccount(Context context,String account) - -//示例 -CIMPushManager.bindAccount(context,"xiyang"); - - -``` - -#### 1.3发送一个CIM请求 - - 酌情使用此功能,可用http接口替代 -```java -/** -* 发送一个CIM请求 -* @param context -* @param body 请求体的结构 -*/ -public static void sendRequest(Context context,SentBody body) - -//示例:获取离线消息 -SentBody sent = new SentBody(); -sent.setKey(CIMConstant.RequestKey.CLIENT_OFFLINE_MESSAGE); -sent.put("account", "xiyang"); -CIMPushManager.sendRequest(context, sent); - -//该功能需要服务端实现,详情参考服务端PullOflineMessageHandler.java - -``` - -#### 1.4停止接收消息 -```java -/** -* 停止接受推送,将会退出当前账号登录,端口与服务端的连接 -* @param context -*/ -public static void stop(Context context) -//示例: -CIMPushManager.stop(context); - -``` -#### 1.5恢复接收消息 -```java -/** -* 重新恢复接收推送,重新连接服务端,并登录当前账号 -* @param context -*/ -public static void resume(Context context) -//示例: -CIMPushManager.resume(context); -``` - - -#### 1.6完全销毁连接 -```java -/** -* 完全销毁CIM,一般用于完全退出程序,调用resume将不能恢复 -* @param context -*/ -public static void destroy(Context context) -//示例: -CIMPushManager.destroy(context); - - -``` -#### 1.7获取是否与服务端连接正常 -```java - -/** - * - * @param context - */ -public boolean isConnected(Context context) - -//示例: -CIMPushManager.isConnected(context); -``` - -#### 1.8获取PushManager状态 -```java -//被销毁的destroy() -CIMPushManager.STATE_DESTROYED = 0x0000DE; -//被销停止的 stop() -CIMPushManager.STATE_STOPED = 0x0000EE; - -CIMPushManager.STATE_NORMAL = 0x000000; - -public int getState(Context context) - -//示例: -CIMPushManager.getState(context); -``` - -#### 1.9推送消息以及相关事件的接收 - -首先注册一个广播,并监听以下action 参照 后面androidManifest.xml配置 - -参考CustomCIMMessageReceiver的实现 -```java -/** -* 当收到服务端推送过来的消息时调用 -* @param message -*/ -public abstract void onMessageReceived(Message message); - -/** -* 当调用CIMPushManager.sendRequest()向服务端发送请求,获得相应时调用 -* @param replybody -*/ -public abstract void onReplyReceived(ReplyBody replybody); - -/** -* 当手机网络发生变化时调用 -* @param networkinfo -*/ -public abstract void onNetworkChanged(NetworkInfo networkinfo); - -/** -* 当连接服务器成功时回调 -* @param hasAutoBind : true 已经自动绑定账号到服务器了,不需要再手动调用bindAccount -*/ -public abstract void onConnectionSuccessed(boolean hasAutoBind); - -/** -* 当断开服务器连接的时候回调 -*/ -public abstract void onConnectionClosed(); - -/** -* 当服务器连接失败的时候回调 -* -*/ -public abstract void onConnectionFailed(Exception e); -``` \ No newline at end of file + + \ No newline at end of file diff --git a/doc/WebSDK使用文档.doc b/doc/WebSDK使用文档.doc index 944ca7a..7b4ccfa 100644 Binary files a/doc/WebSDK使用文档.doc and b/doc/WebSDK使用文档.doc differ diff --git a/doc/Message.proto b/doc/proto/java/Message.proto similarity index 100% rename from doc/Message.proto rename to doc/proto/java/Message.proto diff --git a/doc/ReplyBody.proto b/doc/proto/java/ReplyBody.proto similarity index 100% rename from doc/ReplyBody.proto rename to doc/proto/java/ReplyBody.proto diff --git a/doc/SentBody.proto b/doc/proto/java/SentBody.proto similarity index 100% rename from doc/SentBody.proto rename to doc/proto/java/SentBody.proto diff --git a/doc/proto/java/说明.txt b/doc/proto/java/说明.txt new file mode 100644 index 0000000..a9f7dfc --- /dev/null +++ b/doc/proto/java/说明.txt @@ -0,0 +1 @@ +ʹprotoc ɶӦjavaļ֮ǰ޸protoļеϢ \ No newline at end of file diff --git a/doc/proto/js/Message.proto b/doc/proto/js/Message.proto new file mode 100644 index 0000000..205d87a --- /dev/null +++ b/doc/proto/js/Message.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package com.farsunset.cim.sdk.web.model; +message Message { + string mid = 1; + string action = 2; + string content = 3; + string sender = 4; + string receiver = 5; + string extra = 6; + string title = 7; + string format = 8; + int64 timestamp = 9; +} + \ No newline at end of file diff --git a/doc/proto/js/ReplyBody.proto b/doc/proto/js/ReplyBody.proto new file mode 100644 index 0000000..5e8af8c --- /dev/null +++ b/doc/proto/js/ReplyBody.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; +package com.farsunset.cim.sdk.web.model; +message ReplyBody { + string key = 1; + string code = 2; + string message = 3; + int64 timestamp =4; + map data =5; + +} + \ No newline at end of file diff --git a/doc/proto/js/SentBody.proto b/doc/proto/js/SentBody.proto new file mode 100644 index 0000000..45d7e4d --- /dev/null +++ b/doc/proto/js/SentBody.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; +package com.farsunset.cim.sdk.web.model; + +message SentBody { + string key = 1; + int64 timestamp =2; + map data =3; + +} + \ No newline at end of file diff --git a/doc/proto/js/message.js b/doc/proto/js/message.js new file mode 100644 index 0000000..644e760 --- /dev/null +++ b/doc/proto/js/message.js @@ -0,0 +1,2728 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.toObject = function(opt_includeInstance) { + return proto.com.farsunset.cim.sdk.web.model.Message.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.com.farsunset.cim.sdk.web.model.Message} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.com.farsunset.cim.sdk.web.model.Message.toObject = function(includeInstance, msg) { + var f, obj = { + mid: jspb.Message.getFieldWithDefault(msg, 1, ""), + action: jspb.Message.getFieldWithDefault(msg, 2, ""), + content: jspb.Message.getFieldWithDefault(msg, 3, ""), + sender: jspb.Message.getFieldWithDefault(msg, 4, ""), + receiver: jspb.Message.getFieldWithDefault(msg, 5, ""), + extra: jspb.Message.getFieldWithDefault(msg, 6, ""), + title: jspb.Message.getFieldWithDefault(msg, 7, ""), + format: jspb.Message.getFieldWithDefault(msg, 8, ""), + timestamp: jspb.Message.getFieldWithDefault(msg, 9, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.com.farsunset.cim.sdk.web.model.Message} + */ +proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.com.farsunset.cim.sdk.web.model.Message; + return proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.com.farsunset.cim.sdk.web.model.Message} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.com.farsunset.cim.sdk.web.model.Message} + */ +proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setMid(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setAction(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setContent(value); + break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setSender(value); + break; + case 5: + var value = /** @type {string} */ (reader.readString()); + msg.setReceiver(value); + break; + case 6: + var value = /** @type {string} */ (reader.readString()); + msg.setExtra(value); + break; + case 7: + var value = /** @type {string} */ (reader.readString()); + msg.setTitle(value); + break; + case 8: + var value = /** @type {string} */ (reader.readString()); + msg.setFormat(value); + break; + case 9: + var value = /** @type {number} */ (reader.readInt64()); + msg.setTimestamp(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.com.farsunset.cim.sdk.web.model.Message.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.com.farsunset.cim.sdk.web.model.Message} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.com.farsunset.cim.sdk.web.model.Message.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getMid(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getAction(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = message.getContent(); + if (f.length > 0) { + writer.writeString( + 3, + f + ); + } + f = message.getSender(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } + f = message.getReceiver(); + if (f.length > 0) { + writer.writeString( + 5, + f + ); + } + f = message.getExtra(); + if (f.length > 0) { + writer.writeString( + 6, + f + ); + } + f = message.getTitle(); + if (f.length > 0) { + writer.writeString( + 7, + f + ); + } + f = message.getFormat(); + if (f.length > 0) { + writer.writeString( + 8, + f + ); + } + f = message.getTimestamp(); + if (f !== 0) { + writer.writeInt64( + 9, + f + ); + } +}; + + +/** + * optional string mid = 1; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getMid = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setMid = function(value) { + jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string action = 2; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getAction = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setAction = function(value) { + jspb.Message.setProto3StringField(this, 2, value); +}; + + +/** + * optional string content = 3; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getContent = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setContent = function(value) { + jspb.Message.setProto3StringField(this, 3, value); +}; + + +/** + * optional string sender = 4; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getSender = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setSender = function(value) { + jspb.Message.setProto3StringField(this, 4, value); +}; + + +/** + * optional string receiver = 5; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getReceiver = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setReceiver = function(value) { + jspb.Message.setProto3StringField(this, 5, value); +}; + + +/** + * optional string extra = 6; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getExtra = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setExtra = function(value) { + jspb.Message.setProto3StringField(this, 6, value); +}; + + +/** + * optional string title = 7; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getTitle = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 7, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setTitle = function(value) { + jspb.Message.setProto3StringField(this, 7, value); +}; + + +/** + * optional string format = 8; + * @return {string} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getFormat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "")); +}; + + +/** @param {string} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setFormat = function(value) { + jspb.Message.setProto3StringField(this, 8, value); +}; + + +/** + * optional int64 timestamp = 9; + * @return {number} + */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.getTimestamp = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 9, 0)); +}; + + +/** @param {number} value */ +proto.com.farsunset.cim.sdk.web.model.Message.prototype.setTimestamp = function(value) { + jspb.Message.setProto3IntField(this, 9, value); +}; + + +goog.object.extend(exports, proto.com.farsunset.cim.sdk.web.model); + +},{"google-protobuf":3}],2:[function(require,module,exports){ +var messageProtobuf= require('./MessageProtobuf'); + + module.exports = { + DataProto: messageProtobuf + } +},{"./MessageProtobuf":1}],3:[function(require,module,exports){ +(function (global,Buffer){ +var $jscomp={scope:{},getGlobal:function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global?global:a}};$jscomp.global=$jscomp.getGlobal(this);$jscomp.initSymbol=function(){$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol);$jscomp.initSymbol=function(){}};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(a){return"jscomp_symbol_"+a+$jscomp.symbolCounter_++}; +$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();$jscomp.global.Symbol.iterator||($jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));$jscomp.initSymbolIterator=function(){}};$jscomp.makeIterator=function(a){$jscomp.initSymbolIterator();$jscomp.initSymbol();$jscomp.initSymbolIterator();var b=a[Symbol.iterator];if(b)return b.call(a);var c=0;return{next:function(){return cb;)--c in this?this[--a]=this[c]:delete this[a];return this};$jscomp.array.copyWithin$install=function(){$jscomp.array.installHelper_("copyWithin",$jscomp.array.copyWithin)}; +$jscomp.array.fill=function(a,b,c){var d=this.length||0;0>b&&(b=Math.max(0,d+b));if(null==c||c>d)c=d;c=Number(c);0>c&&(c=Math.max(0,d+c));for(b=Number(b||0);b>>0;if(0===a)return 32;var b=0;0===(a&4294901760)&&(a<<=16,b+=16);0===(a&4278190080)&&(a<<=8,b+=8);0===(a&4026531840)&&(a<<=4,b+=4);0===(a&3221225472)&&(a<<=2,b+=2);0===(a&2147483648)&&b++;return b};$jscomp.math.imul=function(a,b){a=Number(a);b=Number(b);var c=a&65535,d=b&65535;return c*d+((a>>>16&65535)*d+c*(b>>>16&65535)<<16>>>0)|0};$jscomp.math.sign=function(a){a=Number(a);return 0===a||isNaN(a)?a:0a&&-.25a&&-.25a?-b:b};$jscomp.math.acosh=function(a){a=Number(a);return Math.log(a+Math.sqrt(a*a-1))};$jscomp.math.asinh=function(a){a=Number(a);if(0===a)return a;var b=Math.log(Math.abs(a)+Math.sqrt(a*a+1));return 0>a?-b:b}; +$jscomp.math.atanh=function(a){a=Number(a);return($jscomp.math.log1p(a)-$jscomp.math.log1p(-a))/2};$jscomp.math.hypot=function(a,b,c){a=Number(a);b=Number(b);var d,e,f,g=Math.max(Math.abs(a),Math.abs(b));for(d=2;dg){a/=g;b/=g;f=a*a+b*b;for(d=2;da?-b:b};$jscomp.math.cbrt=function(a){if(0===a)return a;a=Number(a);var b=Math.pow(Math.abs(a),1/3);return 0>a?-b:b};$jscomp.number=$jscomp.number||{};$jscomp.number.isFinite=function(a){return"number"!==typeof a?!1:!isNaN(a)&&Infinity!==a&&-Infinity!==a};$jscomp.number.isInteger=function(a){return $jscomp.number.isFinite(a)?a===Math.floor(a):!1}; +$jscomp.number.isNaN=function(a){return"number"===typeof a&&isNaN(a)};$jscomp.number.isSafeInteger=function(a){return $jscomp.number.isInteger(a)&&Math.abs(a)<=$jscomp.number.MAX_SAFE_INTEGER};$jscomp.number.EPSILON=function(){return Math.pow(2,-52)}();$jscomp.number.MAX_SAFE_INTEGER=function(){return 9007199254740991}();$jscomp.number.MIN_SAFE_INTEGER=function(){return-9007199254740991}();$jscomp.object=$jscomp.object||{}; +$jscomp.object.assign=function(a,b){for(var c=1;cd||1114111=d?b+=String.fromCharCode(d):(d-=65536,b+=String.fromCharCode(d>>>10&1023|55296),b+=String.fromCharCode(d&1023|56320))}return b}; +$jscomp.string.repeat=function(a){var b=$jscomp.checkStringArgs(this,null,"repeat");if(0>a||1342177279>>=1)b+=b;return c};$jscomp.string.repeat$install=function(){String.prototype.repeat||(String.prototype.repeat=$jscomp.string.repeat)}; +$jscomp.string.codePointAt=function(a){var b=$jscomp.checkStringArgs(this,null,"codePointAt"),c=b.length;a=Number(a)||0;if(0<=a&&ad||56319a||57343=e}; +$jscomp.string.startsWith$install=function(){String.prototype.startsWith||(String.prototype.startsWith=$jscomp.string.startsWith)};$jscomp.string.endsWith=function(a,b){var c=$jscomp.checkStringArgs(this,a,"endsWith");a+="";void 0===b&&(b=c.length);for(var d=Math.max(0,Math.min(b|0,c.length)),e=a.length;0=e};$jscomp.string.endsWith$install=function(){String.prototype.endsWith||(String.prototype.endsWith=$jscomp.string.endsWith)}; +var COMPILED=!0,goog=goog||{};goog.global=this;goog.isDef=function(a){return void 0!==a};goog.exportPath_=function(a,b,c){a=a.split(".");c=c||goog.global;a[0]in c||!c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)!a.length&&goog.isDef(b)?c[d]=b:c=c[d]?c[d]:c[d]={}}; +goog.define=function(a,b){var c=b;COMPILED||(goog.global.CLOSURE_UNCOMPILED_DEFINES&&Object.prototype.hasOwnProperty.call(goog.global.CLOSURE_UNCOMPILED_DEFINES,a)?c=goog.global.CLOSURE_UNCOMPILED_DEFINES[a]:goog.global.CLOSURE_DEFINES&&Object.prototype.hasOwnProperty.call(goog.global.CLOSURE_DEFINES,a)&&(c=goog.global.CLOSURE_DEFINES[a]));goog.exportPath_(a,c)};goog.DEBUG=!0;goog.LOCALE="en";goog.TRUSTED_SITE=!0;goog.STRICT_MODE_COMPATIBLE=!1;goog.DISALLOW_TEST_ONLY_CODE=COMPILED&&!goog.DEBUG; +goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING=!1;goog.provide=function(a){if(!COMPILED&&goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');goog.constructNamespace_(a)};goog.constructNamespace_=function(a,b){if(!COMPILED){delete goog.implicitNamespaces_[a];for(var c=a;(c=c.substring(0,c.lastIndexOf(".")))&&!goog.getObjectByName(c);)goog.implicitNamespaces_[c]=!0}goog.exportPath_(a,b)};goog.VALID_MODULE_RE_=/^[a-zA-Z_$][a-zA-Z0-9._$]*$/; +goog.module=function(a){if(!goog.isString(a)||!a||-1==a.search(goog.VALID_MODULE_RE_))throw Error("Invalid module identifier");if(!goog.isInModuleLoader_())throw Error("Module "+a+" has been loaded incorrectly.");if(goog.moduleLoaderState_.moduleName)throw Error("goog.module may only be called once per module.");goog.moduleLoaderState_.moduleName=a;if(!COMPILED){if(goog.isProvided_(a))throw Error('Namespace "'+a+'" already declared.');delete goog.implicitNamespaces_[a]}};goog.module.get=function(a){return goog.module.getInternal_(a)}; +goog.module.getInternal_=function(a){if(!COMPILED)return goog.isProvided_(a)?a in goog.loadedModules_?goog.loadedModules_[a]:goog.getObjectByName(a):null};goog.moduleLoaderState_=null;goog.isInModuleLoader_=function(){return null!=goog.moduleLoaderState_}; +goog.module.declareLegacyNamespace=function(){if(!COMPILED&&!goog.isInModuleLoader_())throw Error("goog.module.declareLegacyNamespace must be called from within a goog.module");if(!COMPILED&&!goog.moduleLoaderState_.moduleName)throw Error("goog.module must be called prior to goog.module.declareLegacyNamespace.");goog.moduleLoaderState_.declareLegacyNamespace=!0}; +goog.setTestOnly=function(a){if(goog.DISALLOW_TEST_ONLY_CODE)throw a=a||"",Error("Importing test-only code into non-debug environment"+(a?": "+a:"."));};goog.forwardDeclare=function(a){};COMPILED||(goog.isProvided_=function(a){return a in goog.loadedModules_||!goog.implicitNamespaces_[a]&&goog.isDefAndNotNull(goog.getObjectByName(a))},goog.implicitNamespaces_={"goog.module":!0}); +goog.getObjectByName=function(a,b){for(var c=a.split("."),d=b||goog.global,e;e=c.shift();)if(goog.isDefAndNotNull(d[e]))d=d[e];else return null;return d};goog.globalize=function(a,b){var c=b||goog.global,d;for(d in a)c[d]=a[d]};goog.addDependency=function(a,b,c,d){if(goog.DEPENDENCIES_ENABLED){var e;a=a.replace(/\\/g,"/");for(var f=goog.dependencies_,g=0;e=b[g];g++)f.nameToPath[e]=a,f.pathIsModule[a]=!!d;for(d=0;b=c[d];d++)a in f.requires||(f.requires[a]={}),f.requires[a][b]=!0}}; +goog.ENABLE_DEBUG_LOADER=!0;goog.logToConsole_=function(a){goog.global.console&&goog.global.console.error(a)};goog.require=function(a){if(!COMPILED){goog.ENABLE_DEBUG_LOADER&&goog.IS_OLD_IE_&&goog.maybeProcessDeferredDep_(a);if(goog.isProvided_(a))return goog.isInModuleLoader_()?goog.module.getInternal_(a):null;if(goog.ENABLE_DEBUG_LOADER){var b=goog.getPathFromDeps_(a);if(b)return goog.writeScripts_(b),null}a="goog.require could not find: "+a;goog.logToConsole_(a);throw Error(a);}}; +goog.basePath="";goog.nullFunction=function(){};goog.abstractMethod=function(){throw Error("unimplemented abstract method");};goog.addSingletonGetter=function(a){a.getInstance=function(){if(a.instance_)return a.instance_;goog.DEBUG&&(goog.instantiatedSingletons_[goog.instantiatedSingletons_.length]=a);return a.instance_=new a}};goog.instantiatedSingletons_=[];goog.LOAD_MODULE_USING_EVAL=!0;goog.SEAL_MODULE_EXPORTS=goog.DEBUG;goog.loadedModules_={};goog.DEPENDENCIES_ENABLED=!COMPILED&&goog.ENABLE_DEBUG_LOADER; +goog.DEPENDENCIES_ENABLED&&(goog.dependencies_={pathIsModule:{},nameToPath:{},requires:{},visited:{},written:{},deferred:{}},goog.inHtmlDocument_=function(){var a=goog.global.document;return null!=a&&"write"in a},goog.findBasePath_=function(){if(goog.isDef(goog.global.CLOSURE_BASE_PATH))goog.basePath=goog.global.CLOSURE_BASE_PATH;else if(goog.inHtmlDocument_())for(var a=goog.global.document.getElementsByTagName("SCRIPT"),b=a.length-1;0<=b;--b){var c=a[b].src,d=c.lastIndexOf("?"),d=-1==d?c.length: +d;if("base.js"==c.substr(d-7,7)){goog.basePath=c.substr(0,d-7);break}}},goog.importScript_=function(a,b){(goog.global.CLOSURE_IMPORT_SCRIPT||goog.writeScriptTag_)(a,b)&&(goog.dependencies_.written[a]=!0)},goog.IS_OLD_IE_=!(goog.global.atob||!goog.global.document||!goog.global.document.all),goog.importModule_=function(a){goog.importScript_("",'goog.retrieveAndExecModule_("'+a+'");')&&(goog.dependencies_.written[a]=!0)},goog.queuedModules_=[],goog.wrapModule_=function(a,b){return goog.LOAD_MODULE_USING_EVAL&& +goog.isDef(goog.global.JSON)?"goog.loadModule("+goog.global.JSON.stringify(b+"\n//# sourceURL="+a+"\n")+");":'goog.loadModule(function(exports) {"use strict";'+b+"\n;return exports});\n//# sourceURL="+a+"\n"},goog.loadQueuedModules_=function(){var a=goog.queuedModules_.length;if(0\x3c/script>')},goog.appendScriptSrcNode_=function(a){var b=goog.global.document, +c=b.createElement("script");c.type="text/javascript";c.src=a;c.defer=!1;c.async=!1;b.head.appendChild(c)},goog.writeScriptTag_=function(a,b){if(goog.inHtmlDocument_()){var c=goog.global.document;if(!goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING&&"complete"==c.readyState){if(/\bdeps.js$/.test(a))return!1;throw Error('Cannot write "'+a+'" after document load');}var d=goog.IS_OLD_IE_;void 0===b?d?(d=" onreadystatechange='goog.onScriptLoad_(this, "+ ++goog.lastNonModuleScriptIndex_+")' ",c.write('