Add KnowledgeGraph page and recommendationSummary page. Update backend models and create new APIs to support new features. Add KnowledgCard in Dataset search result page.
diff --git a/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql b/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql
index 48130c2..169081f 100644
--- a/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql
+++ b/ApacheCMDA_Backend_1.0/DBDump/Dump20150414.sql
@@ -186,6 +186,7 @@
`dataSourceNameinWebInterface` varchar(255) DEFAULT NULL,
`startTime` datetime DEFAULT NULL,
`endTime` datetime DEFAULT NULL,
+ `agencyURL` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_9x29nf004vryd28iummv5l5r0` (`instrumentId`),
CONSTRAINT `FK_9x29nf004vryd28iummv5l5r0` FOREIGN KEY (`instrumentId`) REFERENCES `Instrument` (`id`)
@@ -203,6 +204,35 @@
UNLOCK TABLES;
--
+-- Table structure for table `DatasetAndUser`
+--
+
+DROP TABLE IF EXISTS `DatasetAndUser`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `DatasetAndUser` (
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
+ `userId` bigint(20) NOT NULL,
+ `datasetId` bigint(20) NOT NULL,
+ `count` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `FK_User_DatasetAndUser` (`userId`),
+ KEY `FK_Dataset_DatasetAndUser` (`datasetId`),
+ CONSTRAINT `FK_User_DatasetAndUser` FOREIGN KEY (`userId`) REFERENCES `User` (`id`),
+ CONSTRAINT `FK_Dataset_DatasetAndUser` FOREIGN KEY (`datasetId`) REFERENCES `Dataset` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `DatasetAndUser`
+--
+
+LOCK TABLES `DatasetAndUser` WRITE;
+/*!40000 ALTER TABLE `DatasetAndUser` DISABLE KEYS */;
+/*!40000 ALTER TABLE `DatasetAndUser` ENABLE KEYS */;
+UNLOCK TABLES;
+
+--
-- Table structure for table `DatasetAndService`
--
@@ -285,6 +315,7 @@
`description` varchar(255) DEFAULT NULL,
`launchDate` datetime DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
+ `instrumentURL` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -464,6 +495,7 @@
`userId` bigint(20) NOT NULL,
`datasetStudyStartTime` datetime DEFAULT NULL,
`datasetStudyEndTime` datetime DEFAULT NULL,
+ `url` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_ly45hkuqs8yyw00iiuyx5hoj4` (`serviceId`),
KEY `FK_g2n3b4rs0xys2r4r967uvi4jr` (`serviceConfigurationId`),
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/AnalyticsController.java b/ApacheCMDA_Backend_1.0/app/controllers/AnalyticsController.java
new file mode 100644
index 0000000..feed694
--- /dev/null
+++ b/ApacheCMDA_Backend_1.0/app/controllers/AnalyticsController.java
@@ -0,0 +1,128 @@
+package controllers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import models.DatasetAndUser;
+import models.DatasetAndUserRepository;
+import play.mvc.Controller;
+import play.mvc.Result;
+
+import com.google.gson.Gson;
+
+@Named
+@Singleton
+public class AnalyticsController extends Controller{
+ private final DatasetAndUserRepository datasetAndUserRepository;
+ @Inject
+ public AnalyticsController(DatasetAndUserRepository datasetAndUserRepository) {
+ this.datasetAndUserRepository = datasetAndUserRepository;
+ }
+
+ public Result getAllDatasetAndUserWithCount(String format) {
+
+ try {
+ Iterable<DatasetAndUser> datasetAndUsers = datasetAndUserRepository.findAll();
+
+ if (datasetAndUsers == null) {
+ System.out.println("User and Dataset: cannot be found!");
+ return notFound("User and Dataset: cannot be found!");
+ }
+
+ Map<String, Object> map = jsonFormat(datasetAndUsers);
+
+ String result = new String();
+ if (format.equals("json")) {
+ result = new Gson().toJson(map);
+ }
+
+ return ok(result);
+ } catch (Exception e) {
+ return badRequest("DatasetLog not found");
+ }
+ }
+
+ private Map<String, Object> jsonFormat(Iterable<DatasetAndUser> userDatasets) {
+
+ List<Map<String, Object>> nodes = new ArrayList<Map<String, Object>>();
+ List<Map<String, Object>> rels = new ArrayList<Map<String, Object>>();
+
+ int i = 1;
+ for (DatasetAndUser userDataset : userDatasets) {
+ int source = 0;
+ int target = 0;
+ // Check whether the current user has already existed
+ for (int j = 0; j < nodes.size(); j++) {
+ if (nodes.get(j).get("title")
+ .equals(userDataset.getUser().getUserName())) {
+ source = (int) nodes.get(j).get("id");
+ break;
+ }
+ }
+ if (source == 0) {
+ nodes.add(map6("id", i, "title", userDataset.getUser()
+ .getUserName(), "label", "user", "cluster", "1",
+ "value", 1, "group", "user"));
+ source = i;
+ i++;
+ }
+ // Check whether the current dataset has already existed
+ for (int j = 0; j < nodes.size(); j++) {
+ if (nodes.get(j).get("title")
+ .equals(userDataset.getDataset().getName())) {
+ target = (int) nodes.get(j).get("id");
+ break;
+ }
+ }
+ if (target == 0) {
+ nodes.add(map6("id", i, "title", userDataset.getDataset()
+ .getName(), "label", "dataset", "cluster", "2",
+ "value", 2, "group", "dataset"));
+ target = i;
+ i++;
+ }
+
+ rels.add(map3("from", source, "to", target, "title", "USE"));
+
+ }
+
+ return map("nodes", nodes, "edges", rels);
+ }
+
+ private Map<String, Object> map(String key1, Object value1, String key2,
+ Object value2) {
+ Map<String, Object> result = new HashMap<String, Object>(2);
+ result.put(key1, value1);
+ result.put(key2, value2);
+ return result;
+ }
+
+ private Map<String, Object> map3(String key1, Object value1, String key2,
+ Object value2, String key3, Object value3) {
+ Map<String, Object> result = new HashMap<String, Object>(3);
+ result.put(key1, value1);
+ result.put(key2, value2);
+ result.put(key3, value3);
+ return result;
+ }
+
+ private Map<String, Object> map6(String key1, Object value1, String key2,
+ Object value2, String key3, Object value3, String key4,
+ Object value4, String key5, Object value5, String key6,
+ Object value6) {
+ Map<String, Object> result = new HashMap<String, Object>(6);
+ result.put(key1, value1);
+ result.put(key2, value2);
+ result.put(key3, value3);
+ result.put(key4, value4);
+ result.put(key5, value5);
+ result.put(key6, value6);
+ return result;
+ }
+}
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java b/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java
index 73722b3..700ad68 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/ClimateServiceController.java
@@ -472,5 +472,22 @@
return ok(result);
}
-
+
+ public Result getTopKUsedClimateServicesByDatasetId(long id, String format) {
+ if (id < 0) {
+ System.out.println("id is negative!");
+ return badRequest("id is negative!");
+ }
+ String result = new String();
+ try {
+ //Parse JSON file
+ List<ClimateService> climateService;
+ climateService = climateServiceRepository.getClimateServiceByDatasetId(5, id);
+ result = new Gson().toJson(climateService);
+ } catch (Exception e) {
+ System.out.println("ServiceExecutionLog cannot be queried, query is corrupt");
+ return badRequest("ServiceExecutionLog cannot be queried, query is corrupt");
+ }
+ return ok(result);
+ }
}
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java b/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java
index 295c0a2..23822b4 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/DatasetController.java
@@ -366,8 +366,6 @@
System.out.println("ServiceExecutionLog cannot be queried, query is corrupt");
return badRequest("ServiceExecutionLog cannot be queried, query is corrupt");
}
- System.out.println("************" + result);
-
return ok(result);
}
}
\ No newline at end of file
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java b/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java
index 432cf16..f92b68e 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/DatasetLogController.java
@@ -3,7 +3,6 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
-
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
diff --git a/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java b/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java
index 507d549..e4594a8 100644
--- a/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java
+++ b/ApacheCMDA_Backend_1.0/app/controllers/ServiceExecutionLogController.java
@@ -342,8 +342,8 @@
String purpose = json.findPath("purpose").asText();
String plotUrl = json.findPath("url").asText();
String dataUrl = json.findPath("dataUrl").asText();
+ String url = json.findPath("urlLink").asText();
JsonNode datasetArray = json.get("datasets");
- System.out.println(datasetArray);
SimpleDateFormat formatter = new SimpleDateFormat(util.Common.DATE_PATTERN);
formatter.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
@@ -415,7 +415,7 @@
ServiceExecutionLog serviceExecutionLog = new ServiceExecutionLog(
climateService, user, serviceConfiguration, purpose,
executionStartTime, executionEndTime, dataUrl, plotUrl,
- datasetStudyStartTime, datasetStudyEndTime);
+ datasetStudyStartTime, datasetStudyEndTime, url);
ServiceExecutionLog savedServiceExecutionLog = serviceExecutionLogRepository
.save(serviceExecutionLog);
ServiceConfiguration savedServiceConfiguration = savedServiceExecutionLog
diff --git a/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java b/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java
index 53a303f..2f70c53 100644
--- a/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java
+++ b/ApacheCMDA_Backend_1.0/app/models/ClimateServiceRepository.java
@@ -21,5 +21,7 @@
@Query(value = "select c.* from ClimateService c, ServiceEntry s where c.id=s.serviceId group by s.serviceId order by s.latestAccessTimeStamp desc", nativeQuery = true)
List<ClimateService> getClimateServiceOrderByLatestAccessTime();
-
+
+ @Query(value = "select * from ClimateService where id in (select serviceId from ServiceEntry s where serviceId in (select climateServiceId from DatasetAndService where datasetId=?2) group by s.serviceId order by s.latestAccessTimeStamp desc) limit ?1", nativeQuery = true)
+ List<ClimateService> getClimateServiceByDatasetId(int k, long id);
}
diff --git a/ApacheCMDA_Backend_1.0/app/models/Dataset.java b/ApacheCMDA_Backend_1.0/app/models/Dataset.java
index 884a957..0d4fc3c 100644
--- a/ApacheCMDA_Backend_1.0/app/models/Dataset.java
+++ b/ApacheCMDA_Backend_1.0/app/models/Dataset.java
@@ -45,6 +45,7 @@
private String comment;
private Date startTime;
private Date endTime;
+ private String agencyURL;
public Dataset() {
@@ -82,6 +83,14 @@
this.endTime = endTime;
}
+ public String getAgencyURL() {
+ return agencyURL;
+ }
+
+ public void setAgencyURL(String agencyURL) {
+ this.agencyURL = agencyURL;
+ }
+
public long getId() {
return id;
}
diff --git a/ApacheCMDA_Backend_1.0/app/models/DatasetAndUser.java b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUser.java
new file mode 100644
index 0000000..53665b3
--- /dev/null
+++ b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUser.java
@@ -0,0 +1,69 @@
+package models;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+
+@Entity
+public class DatasetAndUser {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private long id;
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "userId", referencedColumnName = "id")
+ private User user;
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "datasetId", referencedColumnName = "id")
+ private Dataset dataset;
+ private long count;
+
+ public DatasetAndUser() {
+ }
+
+ public DatasetAndUser(User user, Dataset dataset, long count) {
+ this.user = user;
+ this.dataset = dataset;
+ this.count = count;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ public Dataset getDataset() {
+ return dataset;
+ }
+
+ public void setDataset(Dataset dataset) {
+ this.dataset = dataset;
+ }
+
+ public long getCount() {
+ return count;
+ }
+ public void setCount(long count) {
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ return "DatasetAndUser[id=" + id + ", user=" + user.toString()
+ + ", dataset=" + dataset.toString() + ", count=" + count + "]";
+ }
+}
diff --git a/ApacheCMDA_Backend_1.0/app/models/DatasetAndUserRepository.java b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUserRepository.java
new file mode 100644
index 0000000..ce6b046
--- /dev/null
+++ b/ApacheCMDA_Backend_1.0/app/models/DatasetAndUserRepository.java
@@ -0,0 +1,12 @@
+package models;
+
+import org.springframework.data.repository.CrudRepository;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+@Named
+@Singleton
+public interface DatasetAndUserRepository extends CrudRepository<DatasetAndUser, Long> {
+
+}
diff --git a/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java b/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java
index 3381b4e..de4ce6c 100644
--- a/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java
+++ b/ApacheCMDA_Backend_1.0/app/models/DatasetLogRepository.java
@@ -8,5 +8,5 @@
@Named
@Singleton
public interface DatasetLogRepository extends CrudRepository<DatasetLog, Long> {
-
+
}
diff --git a/ApacheCMDA_Backend_1.0/app/models/Instrument.java b/ApacheCMDA_Backend_1.0/app/models/Instrument.java
index 71a0094..45aed25 100644
--- a/ApacheCMDA_Backend_1.0/app/models/Instrument.java
+++ b/ApacheCMDA_Backend_1.0/app/models/Instrument.java
@@ -15,6 +15,7 @@
private String name;
private String description;
private Date launchDate;
+ private String instrumentURL;
public Instrument() {
}
@@ -26,6 +27,14 @@
this.launchDate = launchDate;
}
+ public String getInstrumentURL() {
+ return instrumentURL;
+ }
+
+ public void setInstrumentURL(String instrumentURL) {
+ this.instrumentURL = instrumentURL;
+ }
+
public long getId() {
return id;
}
diff --git a/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java b/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java
index c35c6c4..86351dc 100644
--- a/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java
+++ b/ApacheCMDA_Backend_1.0/app/models/ServiceExecutionLog.java
@@ -40,6 +40,7 @@
private String dataUrl;
private Date datasetStudyStartTime;
private Date datasetStudyEndTime;
+ private String url;
public ServiceExecutionLog(
@@ -47,7 +48,7 @@
ServiceConfiguration serviceConfiguration, // DatasetLog datasetLog,
String purpose, Date executionStartTime, Date executionEndTime,
String dataUrl, String plotUrl,
- Date datasetStudyStartTime, Date datasetStudyEndTime) {
+ Date datasetStudyStartTime, Date datasetStudyEndTime, String url) {
this.climateService = climateService;
this.user = user;
this.serviceConfiguration = serviceConfiguration;
@@ -59,6 +60,7 @@
this.dataUrl = dataUrl;
this.datasetStudyStartTime = datasetStudyStartTime;
this.datasetStudyEndTime = datasetStudyEndTime;
+ this.url = url;
}
public ServiceExecutionLog() {
@@ -111,6 +113,10 @@
public Date getDatasetStudyEndTime() {
return datasetStudyEndTime;
+ }
+
+ public String getUrl() {
+ return url;
}
public void setId(long id) {
@@ -161,6 +167,10 @@
this.datasetStudyEndTime = datasetStudyEndTime;
}
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
@Override
public String toString() {
return "ServiceExecutionLog [id=" + id + ", climateService="
@@ -171,6 +181,7 @@
+ plotUrl + ", dataUrl=" + dataUrl
+ ", datasetStudyStartTime=" + datasetStudyStartTime
+ ", datasetStudyEndTime=" + datasetStudyEndTime
+ + ", url=" + url
+ "]";
}
diff --git a/ApacheCMDA_Backend_1.0/conf/routes b/ApacheCMDA_Backend_1.0/conf/routes
index ff3d46c..b59bff7 100644
--- a/ApacheCMDA_Backend_1.0/conf/routes
+++ b/ApacheCMDA_Backend_1.0/conf/routes
@@ -13,6 +13,7 @@
GET /climateService/getAllMostRecentClimateServicesByCreateTime/json @controllers.ClimateServiceController.getAllClimateServicesOrderByCreateTime(format: String="json")
GET /climateService/getAllMostRecentClimateServicesByLatestAccessTime/json @controllers.ClimateServiceController.getAllClimateServicesOrderByLatestAccessTime(format: String="json")
GET /climateService/getAllMostUsedClimateServices/json @controllers.ClimateServiceController.getAllClimateServicesOrderByCount(format: String="json")
+GET /climateService/getTopKUsedClimateServicesByDatasetId/:id @controllers.ClimateServiceController.getTopKUsedClimateServicesByDatasetId(id: Long, format: String="json")
POST /climateService/addClimateService @controllers.ClimateServiceController.addClimateService
GET /climateService/getAllServiceEntries/json @controllers.ClimateServiceController.getAllServiceEntries(format: String="json")
POST /climateService/addServiceEntry @controllers.ClimateServiceController.addServiceEntry
@@ -136,5 +137,8 @@
POST /users/isEmailExisted @controllers.UserController.isEmailExisted
DELETE /users/delete/userName/:userName/password/:password @controllers.UserController.deleteUserByUserNameandPassword(userName: String, password: String)
+# Analytics
+GET /analytics/getAllDatasetAndUserWithCount/json @controllers.AnalyticsController.getAllDatasetAndUserWithCount(format: String="json")
+
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java b/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java
index 873ecfa..75d7881 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/AnalyticsController.java
@@ -1,39 +1,14 @@
package controllers;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-
-import org.apache.commons.lang3.StringEscapeUtils;
-
-import models.ClimateService;
import models.ServiceExecutionLog;
-import play.Logger;
-import play.data.DynamicForm;
import play.data.Form;
-import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Result;
import utils.Constants;
import utils.RESTfulCalls;
-import utils.RESTfulCalls.ResponseType;
import views.html.*;
-import models.*;
-import views.*;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
public class AnalyticsController extends Controller{
@@ -41,6 +16,13 @@
.form(ServiceExecutionLog.class);
+ public static Result getKnowledgeGraph() {
+ JsonNode response = RESTfulCalls.getAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT + Constants.GET_DATASET_AND_USER);
+ String resStr = response.toString();
+ return ok(knowledgeGraph.render(resStr));
+ }
+
public static Result getRecommend() {
JsonNode response = RESTfulCalls.getAPI("http://einstein.sv.cmu.edu:9026/api/sgraph");
String resStr = response.toString();
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java b/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java
index f103643..e0c0739 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/ClimateServiceController.java
@@ -12,6 +12,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DateFormat;
+import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -24,6 +25,7 @@
import org.apache.commons.lang3.StringEscapeUtils;
import models.ClimateService;
+import models.Dataset;
import models.ServiceConfigurationItem;
import models.User;
import play.Logger;
@@ -38,13 +40,14 @@
import views.html.*;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
public class ClimateServiceController extends Controller {
final static Form<ClimateService> climateServiceForm = Form
- .form(ClimateService.class);
-
+ .form(ClimateService.class);
+
public static Result addAClimateService() {
return ok(registerAClimateService.render(climateServiceForm));
}
@@ -54,32 +57,17 @@
JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ Constants.CMU_BACKEND_PORT
+ Constants.GET_ALL_CLIMATE_SERVICES);
- System.out.println("GET API: " + Constants.URL_HOST
- + Constants.CMU_BACKEND_PORT
- + Constants.GET_ALL_CLIMATE_SERVICES);
// if no value is returned or error or is not json array
if (climateServicesNode == null || climateServicesNode.has("error")
|| !climateServicesNode.isArray()) {
return ok(allClimateServices.render(climateServicesList,
- climateServiceForm));
+ climateServiceForm));
}
-
+
// parse the json string into object
for (int i = 0; i < climateServicesNode.size(); i++) {
JsonNode json = climateServicesNode.path(i);
- ClimateService oneService = new ClimateService();
- oneService.setName(json.path("name").asText());
- System.out.println("****************"+json.path("name").asText());
- oneService.setPurpose(json.path("purpose").asText());
- // URL here is the dynamic page url
- String name = json.path("name").asText();
- String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" +
- name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
- oneService.setUrl(pageUrl);
- // newService.setCreateTime(json.path("createTime").asText());
- oneService.setScenario(json.path("scenario").asText());
- oneService.setVersionNo(json.path("versionNo").asText());
- oneService.setRootServiceId(json.path("rootServiceId").asLong());
+ ClimateService oneService = deserializeJsonToClimateService(json);
climateServicesList.add(oneService);
}
@@ -88,7 +76,7 @@
}
public static Result addClimateService() {
-// Form<ClimateService> cs = climateServiceForm.bindFromRequest();
+ // Form<ClimateService> cs = climateServiceForm.bindFromRequest();
JsonNode json = request().body().asJson();
String name = json.path("name").asText();
String purpose = json.path("purpose").asText();
@@ -96,7 +84,7 @@
String scenario = json.path("scenario").asText();
String versionNo = json.path("version").asText();
String rootServiceId = json.path("rootServiceId").asText();
-
+
JsonNode response = null;
ObjectNode jsonData = Json.newObject();
try {
@@ -115,21 +103,22 @@
// default val
jsonData.put("purpose", purpose);
jsonData.put("url", url);
- DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
+ DateFormat dateFormat = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ssz");
// get current date time with Date()
Date date = new Date();
jsonData.put("createTime", dateFormat.format(date));
jsonData.put("scenario", scenario);
jsonData.put("versionNo", versionNo);
jsonData.put("rootServiceId", rootServiceId);
-
// POST Climate Service JSON data
- response = RESTfulCalls.postAPI(Constants.URL_HOST + Constants.CMU_BACKEND_PORT
+ response = RESTfulCalls.postAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT
+ Constants.ADD_CLIMATE_SERVICE, jsonData);
// flash the response message
- System.out.println("***************"+response);
+ System.out.println("***************" + response);
Application.flashMsg(response);
} catch (IllegalStateException e) {
e.printStackTrace();
@@ -148,7 +137,7 @@
System.out.println("JSON data: " + jsonData);
String url = jsonData.get("climateServiceCallUrl").toString();
System.out.println("JPL climate service model call url: " + url);
-
+
// transfer JsonNode to Object
ObjectNode object = (ObjectNode) jsonData;
object.remove("climateServiceCallUrl");
@@ -162,7 +151,8 @@
// flash the response message
Application.flashMsg(response);
- System.out .println(ok("Climate Service model has been called successfully!"));
+ System.out
+ .println(ok("Climate Service model has been called successfully!"));
// return jsonData
return ok(response);
}
@@ -173,9 +163,11 @@
String name = request().body().asJson().get("name").toString();
String purpose = request().body().asJson().get("purpose").toString();
String url = request().body().asJson().get("url").toString();
- String outputButton = request().body().asJson().get("pageOutput").toString();
- String dataListContent = request().body().asJson().get("dataListContent").toString();
-
+ String outputButton = request().body().asJson().get("pageOutput")
+ .toString();
+ String dataListContent = request().body().asJson()
+ .get("dataListContent").toString();
+
System.out.println("page string: " + str);
System.out.println("climate service name: " + name);
@@ -187,8 +179,7 @@
JsonNode response = RESTfulCalls.postAPI(Constants.URL_HOST
+ Constants.CMU_BACKEND_PORT
+ Constants.SAVE_CLIMATE_SERVICE_PAGE, jsonData);
-
-
+
System.out.println("WARNING!!!!!!");
// save page in front-end
savePage(str, name, purpose, url, outputButton, dataListContent);
@@ -200,32 +191,31 @@
public static Result ruleEngineData() {
JsonNode result = request().body().asJson();
- //System.out.println("ticking!");
- System.out.println(result);
-
- return ok("good");
+ // System.out.println("ticking!");
+ System.out.println(result);
+
+ return ok("good");
}
-
-
+
public static Result addAllParameters() {
JsonNode result = request().body().asJson();
System.out.println(result);
System.out.println("--------------------------");
Iterator<JsonNode> ite = result.iterator();
-
- while(ite.hasNext()) {
-
+
+ while (ite.hasNext()) {
+
JsonNode tmp = ite.next();
System.out.println(tmp);
- JsonNode response = RESTfulCalls.postAPI(Constants.URL_HOST
- + Constants.CMU_BACKEND_PORT
- + Constants.ADD_ALL_PARAMETERS, tmp);
+ JsonNode response = RESTfulCalls.postAPI(
+ Constants.URL_HOST + Constants.CMU_BACKEND_PORT
+ + Constants.ADD_ALL_PARAMETERS, tmp);
System.out.println("=========" + response);
}
-
- return ok("good");
+
+ return ok("good");
}
-
+
public static void savePage(String str, String name, String purpose,
String url, String outputButton, String dataListContent) {
System.out.println("output button test: " + outputButton);
@@ -234,24 +224,25 @@
.replaceAll(
"<td><button type=\\\\\"button\\\\\" class=\\\\\"btn btn-danger\\\\\" onclick=\\\\\"Javascript:deleteRow\\(this,\\d+\\)\\\\\">delete</button></td>",
"");
-
+
dataListContent = StringEscapeUtils.unescapeJava(dataListContent);
result = StringEscapeUtils.unescapeJava(result);
outputButton = StringEscapeUtils.unescapeJava(outputButton);
System.out.println("output button test: " + outputButton);
-
+
// remove the first char " and the last char " of result, name and
// purpose
- dataListContent = dataListContent.substring(1, dataListContent.length() - 1);
+ dataListContent = dataListContent.substring(1,
+ dataListContent.length() - 1);
result = result.substring(1, result.length() - 1);
outputButton = outputButton.substring(1, outputButton.length() - 1);
-
+
name = name.substring(1, name.length() - 1);
purpose = purpose.substring(1, purpose.length() - 1);
-
+
String putVarAndDataList = Constants.putVar + dataListContent;
System.out.println("putVarAndDataList: " + putVarAndDataList);
-
+
String str11 = Constants.htmlHead1;
// System.out.println("head1: " + str11);
String str12 = Constants.htmlHead2;
@@ -264,8 +255,9 @@
String str22 = Constants.htmlTail2;
String str23 = Constants.htmlTail3;
- result = str11 +putVarAndDataList+ str12 + name + str13 + purpose + str14 + result + str21
- + url.substring(1, url.length() - 1) + str22 + outputButton + str23;
+ result = str11 + putVarAndDataList + str12 + name + str13 + purpose
+ + str14 + result + str21 + url.substring(1, url.length() - 1)
+ + str22 + outputButton + str23;
name = name.replace(" ", "");
@@ -275,7 +267,7 @@
+ ".html";
File theDir = new File("public/html");
-
+
// if the directory does not exist, create it
if (!theDir.exists()) {
System.out.println("creating directory: public/html");
@@ -305,6 +297,7 @@
e.printStackTrace();
}
}
+
public static void flashMsg(JsonNode jsonNode) {
Iterator<Entry<String, JsonNode>> it = jsonNode.fields();
while (it.hasNext()) {
@@ -312,15 +305,14 @@
flash(field.getKey(), field.getValue().asText());
}
}
-
+
public static Result mostRecentlyAddedClimateServices() {
-
+
List<ClimateService> climateServices = new ArrayList<ClimateService>();
- JsonNode climateServicesNode = RESTfulCalls
- .getAPI(Constants.URL_HOST
- + Constants.CMU_BACKEND_PORT
- + Constants.GET_MOST_RECENTLY_ADDED_CLIMATE_SERVICES_CALL);
+ JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT
+ + Constants.GET_MOST_RECENTLY_ADDED_CLIMATE_SERVICES_CALL);
// if no value is returned or error or is not json array
if (climateServicesNode == null || climateServicesNode.has("error")
@@ -331,33 +323,19 @@
// parse the json string into object
for (int i = 0; i < climateServicesNode.size(); i++) {
JsonNode json = climateServicesNode.path(i);
- ClimateService newService = new ClimateService();
- newService.setId(json.get("id").asLong());
- newService.setName(json.get("name").asText());
- newService.setPurpose(json.findPath("purpose").asText());
- //newService.setUrl(json.findPath("url").asText());
- String name = json.path("name").asText();
- String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" +
- name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
- newService.setUrl(pageUrl);
- //newService.setCreateTime(json.findPath("createTime").asText());
- newService.setScenario(json.findPath("scenario").asText());
- newService.setVersionNo(json.findPath("versionNo").asText());
- newService.setRootServiceId(json.findPath("rootServiceId").asLong());
+ ClimateService newService = deserializeJsonToClimateService(json);
climateServices.add(newService);
}
-
+
return ok(mostRecentlyAddedServices.render(climateServices));
}
-
-
-
-
+
public static Result mostPopularServices() {
List<ClimateService> climateServices = new ArrayList<ClimateService>();
- JsonNode climateServicesNode = RESTfulCalls
- .getAPI(Constants.URL_HOST + Constants.CMU_BACKEND_PORT + Constants.GET_MOST_POPULAR_CLIMATE_SERVICES_CALL);
+ JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT
+ + Constants.GET_MOST_POPULAR_CLIMATE_SERVICES_CALL);
// if no value is returned or error or is not json array
if (climateServicesNode == null || climateServicesNode.has("error")
@@ -368,33 +346,101 @@
// parse the json string into object
for (int i = 0; i < climateServicesNode.size(); i++) {
JsonNode json = climateServicesNode.path(i);
- ClimateService newService = new ClimateService();
- newService.setId(json.get("id").asLong());
- newService.setName(json.get("name").asText());
- newService.setPurpose(json.findPath("purpose").asText());
- //newService.setUrl(json.findPath("url").asText());
- String name = json.path("name").asText();
- String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" +
- name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
- newService.setUrl(pageUrl);
- //newService.setCreateTime(json.findPath("createTime").asText());
- newService.setScenario(json.findPath("scenario").asText());
- newService.setVersionNo(json.findPath("versionNo").asText());
- newService.setRootServiceId(json.findPath("rootServiceId").asLong());
+ ClimateService newService = deserializeJsonToClimateService(json);
climateServices.add(newService);
}
-
+
return ok(mostPopularServices.render(climateServices));
}
- public static Result mostRecentlyUsedClimateServices() {
+ public static Result recommendationSummary() {
+ List<ClimateService> climateServices = new ArrayList<ClimateService>();
+ List<Dataset> dataSetsList = new ArrayList<Dataset>();
+
+ List<User> usersList = new ArrayList<User>();
+
+ JsonNode usersNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT
+ + Constants.GET_ALL_USERS);
+
+ // if no value is returned or error or is not json array
+ if (usersNode == null || usersNode.has("error")
+ || !usersNode.isArray()) {
+ return ok(recommendationSummary.render(climateServices, dataSetsList, usersList));
+ }
+
+
+// JsonNode dataSetsNode = RESTfulCalls.getAPI(Constants.URL_HOST
+// + Constants.CMU_BACKEND_PORT
+// + Constants.GET_ALL_DATASETS);
+//
+// System.out.println("GET API: " + Constants.URL_HOST
+// + Constants.CMU_BACKEND_PORT
+// + Constants.GET_ALL_DATASETS);
+
+ JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT
+ + Constants.GET_MOST_POPULAR_CLIMATE_SERVICES_CALL);
+
+ // if no value is returned or error or is not json array
+ if (climateServicesNode == null || climateServicesNode.has("error")
+ || !climateServicesNode.isArray()) {
+ return ok(recommendationSummary.render(climateServices, dataSetsList, usersList));
+ }
+
+// // if no value is returned or error or is not json array
+// if (dataSetsNode == null || dataSetsNode.has("error")
+// || !dataSetsNode.isArray()) {
+// System.out.println("All oneDatasets format has error!");
+// return ok(recommendationSummary.render(climateServices, dataSetsList));
+// }
+
+ // parse the json string into object
+ for (int i = 0; i < climateServicesNode.size(); i++) {
+ JsonNode json = climateServicesNode.path(i);
+ ClimateService newService = deserializeJsonToClimateService(json);
+ climateServices.add(newService);
+ }
+
+// // parse the json string into object
+// for (int i = 0; i < dataSetsNode.size(); i++) {
+// JsonNode json = dataSetsNode.path(i);
+// Dataset oneDataset = DatasetController.deserializeJsonToDataSet(json);
+// dataSetsList.add(oneDataset);
+// }
+
+
+ // parse the json string into object
+ for (int i = 0; i < usersNode.size(); i++) {
+ JsonNode json = usersNode.path(i);
+ User oneUser = new User();
+ oneUser.setId(json.findPath("id").asLong());
+ oneUser.setUserName(json.findPath("userName").asText());
+ oneUser.setPassword(json.findPath("password").asText());
+ oneUser.setFirstName(json.findPath("firstName").asText());
+ oneUser.setMiddleInitial(json.findPath("middleInitial").asText());
+ oneUser.setLastName(json.findPath("lastName").asText());
+ oneUser.setAffiliation(json.findPath("affiliation").asText());
+ oneUser.setEmail(json.findPath("email").asText());
+ oneUser.setResearchFields(json.findPath("researchFields").asText());
+
+ usersList.add(oneUser);
+ }
+
+ int k = Integer.MAX_VALUE; // Set the first popular K datasets
+ dataSetsList = DatasetController.queryFirstKDatasetsWithoutClimateService("", "", "", "", "", new Date(0), new Date(), k);
+ return ok(recommendationSummary.render(climateServices, dataSetsList, usersList));
+ }
+
+ public static Result mostRecentlyUsedClimateServices() {
+
List<ClimateService> climateServices = new ArrayList<ClimateService>();
JsonNode climateServicesNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ Constants.CMU_BACKEND_PORT
+ Constants.GET_MOST_RECENTLY_USED_CLIMATE_SERVICES_CALL);
-
+
// if no value is returned or error or is not json array
if (climateServicesNode == null || climateServicesNode.has("error")
|| !climateServicesNode.isArray()) {
@@ -404,110 +450,182 @@
// parse the json string into object
for (int i = 0; i < climateServicesNode.size(); i++) {
JsonNode json = climateServicesNode.path(i);
- ClimateService newService = new ClimateService();
- newService.setId(json.get("id").asLong());
- newService.setName(json.get("name").asText());
- newService.setPurpose(json.findPath("purpose").asText());
-
- String name = json.path("name").asText();
- String pageUrl = Constants.URL_SERVER + Constants.LOCAL_HOST_PORT + "/assets/html/service" +
- name.substring(0, 1).toUpperCase() + name.substring(1) + ".html";
- newService.setUrl(pageUrl);
-
- newService.setScenario(json.findPath("scenario").asText());
- newService.setVersionNo(json.findPath("versionNo").asText());
- newService.setRootServiceId(json.findPath("rootServiceId").asLong());
+ ClimateService newService = deserializeJsonToClimateService(json);
climateServices.add(newService);
}
-
+
return ok(mostRecentlyUsedServices.render(climateServices));
}
-
+
public static Result replaceFile() {
- File result = request().body().asRaw().asFile();
- System.out.println("result: " + request().body().asRaw().asFile());
-
-// String content = readFile(result.getName(), StandardCharsets.UTF_8);
- System.out.println("result body: "+result.toString());
-
- String line = "";
- try {
- BufferedReader br = new BufferedReader(new FileReader(result.getAbsolutePath()));
- StringBuilder sb = new StringBuilder();
- line = br.readLine();
- int count = 0;
- while (line != null && count < 22) {
- sb.append(line);
- sb.append("\n");
- line = br.readLine();
- count++;
- }
- br.close();
- } catch (FileNotFoundException e) {
+ File result = request().body().asRaw().asFile();
+ System.out.println("result: " + request().body().asRaw().asFile());
+
+ // String content = readFile(result.getName(), StandardCharsets.UTF_8);
+ System.out.println("result body: " + result.toString());
+
+ String line = "";
+ try {
+ BufferedReader br = new BufferedReader(new FileReader(
+ result.getAbsolutePath()));
+ StringBuilder sb = new StringBuilder();
+ line = br.readLine();
+ int count = 0;
+ while (line != null && count < 22) {
+ sb.append(line);
+ sb.append("\n");
+ line = br.readLine();
+ count++;
+ }
+ br.close();
+ } catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
- }
-
- //TEMPOARY SOLUTION : get the fileName from the html page
- String tempName = line.substring(24, line.length()-5);
- String fileName = "public/html/service" + tempName.substring(0, 1).toUpperCase() + tempName.substring(1)+ ".html";
- System.out.println("fileName: " + fileName);
-
- //replace the page in the frontend Server
- try {
- Path newPath = Paths.get(fileName);
- Files.move(result.toPath(), newPath, REPLACE_EXISTING);
- } catch (FileNotFoundException e) {
+ }
+
+ // TEMPOARY SOLUTION : get the fileName from the html page
+ String tempName = line.substring(24, line.length() - 5);
+ String fileName = "public/html/service"
+ + tempName.substring(0, 1).toUpperCase()
+ + tempName.substring(1) + ".html";
+ System.out.println("fileName: " + fileName);
+
+ // replace the page in the frontend Server
+ try {
+ Path newPath = Paths.get(fileName);
+ Files.move(result.toPath(), newPath, REPLACE_EXISTING);
+ } catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
- }
- //executeReplace(result);
-
-
- return ok("File uploaded");
+ }
+ // executeReplace(result);
+
+ return ok("File uploaded");
}
-public static void executeReplace(String result) {
-
- try {
- String path = "public/html/se.html";
- File theDir = new File("public/html");
-
- // if the directory does not exist, create it
- if (!theDir.exists()) {
- System.out.println("creating directory: public/html");
- boolean create = false;
+ public static void executeReplace(String result) {
- try {
- theDir.mkdir();
- create = true;
- } catch (SecurityException se) {
- // handle it
+ try {
+ String path = "public/html/se.html";
+ File theDir = new File("public/html");
+
+ // if the directory does not exist, create it
+ if (!theDir.exists()) {
+ System.out.println("creating directory: public/html");
+ boolean create = false;
+
+ try {
+ theDir.mkdir();
+ create = true;
+ } catch (SecurityException se) {
+ // handle it
+ }
+ if (create) {
+ System.out.println("DIR created");
+ }
}
- if (create) {
- System.out.println("DIR created");
- }
+
+ File file = new File(path);
+ BufferedWriter output = new BufferedWriter(new FileWriter(file));
+ output.write(result);
+ output.close();
+ System.out.println("Beeping!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
}
-
-
- File file = new File(path);
- BufferedWriter output = new BufferedWriter(new FileWriter(file));
- output.write(result);
- output.close();
- System.out.println("Beeping!!!!!!!!!!!!!!!!!!!!!!!!!!!");
- } catch (FileNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-}
+ }
+ public static ClimateService deserializeJsonToClimateService(JsonNode json) {
+
+ ClimateService oneService = new ClimateService();
+ oneService.setName(json.path("name").asText());
+ oneService.setPurpose(json.path("purpose").asText());
+ // URL here is the dynamic page url
+ String name = json.path("name").asText();
+ String url = json.path("url").asText();
+ // Parse NASA URL
+ if (url.contains("/cmac/web")) {
+ oneService.setUrl(url);
+ } else {
+ String pageUrl = Constants.URL_SERVER
+ + Constants.LOCAL_HOST_PORT + "/assets/html/service"
+ + name.substring(0, 1).toUpperCase()
+ + name.substring(1) + ".html";
+ oneService.setUrl(pageUrl);
+ }
+ // newService.setCreateTime(json.path("createTime").asText());
+ oneService.setScenario(json.path("scenario").asText());
+ oneService.setVersionNo(json.path("versionNo").asText());
+ oneService.setRootServiceId(json.path("rootServiceId").asLong());
+
+ return oneService;
+ }
+
+ // Get all climate Services
+ public static Result searchClimateServices() {
+ return ok(searchClimateService.render(climateServiceForm));
+ }
+
+
+ public static Result getSearchResult(){
+ Form<ClimateService> cs = climateServiceForm.bindFromRequest();
+ ObjectNode jsonData = Json.newObject();
+
+ String name = "";
+ String purpose = "";
+ String scenario = "";
+ String url = "";
+ String versionNo = "";
+
+ try {
+ name = cs.field("Climate Service Name").value();
+ purpose = cs.field("Purpose").value();
+ url = cs.field("Url").value();
+ scenario = cs.field("Scenario").value();
+ versionNo = cs.field("Version Number").value();
+
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ Application.flashMsg(RESTfulCalls
+ .createResponse(ResponseType.CONVERSIONERROR));
+ } catch (Exception e) {
+ e.printStackTrace();
+ Application.flashMsg(RESTfulCalls.createResponse(ResponseType.UNKNOWN));
+ }
+
+ List<ClimateService> response = queryClimateService(name, purpose, url, scenario, versionNo);
+ return ok(climateServiceList.render(response));
+ }
+
+public static List<ClimateService> queryClimateService(String name, String purpose, String url, String scenario, String versionNo) {
+
+ List<ClimateService> climateService = new ArrayList<ClimateService>();
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode queryJson = mapper.createObjectNode();
+ queryJson.put("name", name);
+ queryJson.put("purpose", purpose);
+ queryJson.put("url", url);
+ queryJson.put("scenario", scenario);
+ queryJson.put("versionNo", versionNo);
+
+ JsonNode climateServiceNode = RESTfulCalls.postAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT + Constants.QUERY_CLIMATE_SERVICE, queryJson);
+ // parse the json string into object
+ for (int i = 0; i < climateServiceNode.size(); i++) {
+ JsonNode json = climateServiceNode.path(i);
+ ClimateService newClimateService = deserializeJsonToClimateService(json);
+ climateService.add(newClimateService);
+ }
+ return climateService;
+ }
}
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java
index 6f9d711..9a0d6dd 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetController.java
@@ -7,6 +7,7 @@
import java.util.Date;
import java.util.List;
+import models.ClimateService;
import models.Dataset;
import org.joda.time.DateTime;
@@ -36,14 +37,16 @@
return ok(searchDataSet.render(dataSetForm));
}
+ public static Result mostPopularDatasets() {
+ List<Dataset> datasets = queryFirstKDatasetsWithoutClimateService("", "", "", "", "", new Date(0), new Date(), Integer.MAX_VALUE);
+ return ok(dataSetListPopular.render(dataSetForm, datasets));
+ }
+
public static Result showAllDatasets() {
List<Dataset> dataSetsList = new ArrayList<Dataset>();
JsonNode dataSetsNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ Constants.CMU_BACKEND_PORT
+ Constants.GET_ALL_DATASETS);
- System.out.println("GET API: " + Constants.URL_HOST
- + Constants.CMU_BACKEND_PORT
- + Constants.GET_ALL_DATASETS);
// if no value is returned or error or is not json array
if (dataSetsNode == null || dataSetsNode.has("error")
|| !dataSetsNode.isArray()) {
@@ -129,7 +132,7 @@
}
List<Dataset> response = queryDataSet(dataSetName, agency, instrument, physicalVariable, gridDimension, dataSetStartTime, dataSetEndTime);
- int k = 5;
+ int k = 5; // Set the first popular K datasets
List<Dataset> datasetsTopK = queryFirstKDatasets(dataSetName, agency, instrument, physicalVariable, gridDimension, dataSetStartTime, dataSetEndTime, k);
return ok(dataSetList.render(response, dataSetForm, datasetsTopK));
}
@@ -194,12 +197,55 @@
for (int i = 0; i < dataSetNode.size(); i++) {
JsonNode json = dataSetNode.path(i);
Dataset newDataSet = deserializeJsonToDataSet(json);
+ long id = newDataSet.getId();
+ JsonNode climateSetNode = RESTfulCalls.getAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT + Constants.GET_TOP_K_USED_CLIMATE_SERVICES_BY_DATASET_ID + "/" + id);
+ List<ClimateService> climateServices = new ArrayList<ClimateService>();
+ for (int j = 0; j < climateSetNode.size(); j++) {
+ JsonNode json1 = climateSetNode.path(j);
+ ClimateService oneService = ClimateServiceController.deserializeJsonToClimateService(json1);
+ climateServices.add(oneService);
+ }
+ newDataSet.setClimateServices(climateServices);
dataset.add(newDataSet);
}
return dataset;
}
+
+ public static List<Dataset> queryFirstKDatasetsWithoutClimateService(String dataSetName, String agency, String instrument, String physicalVariable, String gridDimension, Date dataSetStartTime, Date dataSetEndTime, int k) {
+
+ List<Dataset> dataset = new ArrayList<Dataset>();
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectNode queryJson = mapper.createObjectNode();
+ queryJson.put("name", dataSetName);
+ queryJson.put("agencyId", agency);
+ queryJson.put("instrument", instrument);
+ queryJson.put("physicalVariable", physicalVariable);
+ queryJson.put("gridDimension", gridDimension);
+ queryJson.put("k", k);
+ if (dataSetEndTime != null) {
+ queryJson.put("dataSetEndTime", dataSetEndTime.getTime());
+ }
+ if (dataSetStartTime != null) {
+ queryJson.put("dataSetStartTime", dataSetStartTime.getTime());
+ }
+ JsonNode dataSetNode = RESTfulCalls.postAPI(Constants.URL_HOST
+ + Constants.CMU_BACKEND_PORT + Constants.GET_MOST_K_POPULAR_DATASETS_CALL, queryJson);
+ if (dataSetNode == null || dataSetNode.has("error")
+ || !dataSetNode.isArray()) {
+ return dataset;
+ }
+
+ // parse the json string into object
+ for (int i = 0; i < dataSetNode.size(); i++) {
+ JsonNode json = dataSetNode.path(i);
+ Dataset newDataSet = deserializeJsonToDataSet(json);
+ dataset.add(newDataSet);
+ }
+ return dataset;
+ }
- private static Dataset deserializeJsonToDataSet(JsonNode json) {
+ public static Dataset deserializeJsonToDataSet(JsonNode json) {
Dataset newDataSet = new Dataset();
newDataSet.setId(json.get("id").asLong());
newDataSet.setName(json.get("name").asText());
@@ -212,14 +258,14 @@
newDataSet.setSource(json.get("source").asText());
newDataSet.setStatus(json.get("status").asText());
newDataSet.setResponsiblePerson(json.get("responsiblePerson").asText());
- // dataset.setComments(json.get(""));
newDataSet.setDataSourceNameinWebInterface(json.get("dataSourceNameinWebInterface").asText());
- // Console.print("aaa"+dataset.getDataSourceName());
newDataSet.setVariableNameInWebInterface(json.get("variableNameInWebInterface").asText());
newDataSet.setDataSourceInputParameterToCallScienceApplicationCode(json.get("dataSourceInputParameterToCallScienceApplicationCode").asText());
newDataSet.setVariableNameInputParameterToCallScienceApplicationCode(json.get("variableNameInputParameterToCallScienceApplicationCode").asText());
- String startTime = json.findPath("startTime").asText();
- String endTime = json.findPath("endTime").asText();
+ newDataSet.setAgencyURL(json.findPath("agencyURL").asText());
+ newDataSet.setInstrumentURL(json.findPath("instrument").findPath("instrumentURL").asText());
+ String startTime = json.get("startTime").asText();
+ String endTime = json.get("endTime").asText();
Date tmpStartTime = null;
Date tmpEndTime = null;
@@ -230,6 +276,7 @@
newDataSet.setStartTime(new SimpleDateFormat("YYYY-MM").format(tmpStartTime));
}
} catch (ParseException e){
+ System.out.println(e);
}
try {
@@ -239,7 +286,7 @@
newDataSet.setEndTime(new SimpleDateFormat("YYYY-MM").format(tmpEndTime));
}
} catch (ParseException e){
-
+ System.out.println(e);
}
DateTime dateTimeFrom = new DateTime(tmpStartTime);
@@ -255,4 +302,5 @@
return newDataSet;
}
+
}
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java
index 7906d85..4839883 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/DatasetLogController.java
@@ -59,7 +59,7 @@
tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyStartTime);
if (tmpTime != null) {
- newDatasetLog.setDatasetStudyStartTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+ newDatasetLog.setDatasetStudyStartTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
}
} catch (ParseException e){
// e.printStackTrace();
@@ -69,7 +69,7 @@
tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyEndTime);
if (tmpTime != null) {
- newDatasetLog.setDatasetStudyEndTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+ newDatasetLog.setDatasetStudyEndTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
}
} catch (ParseException e){
// e.printStackTrace();
diff --git a/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java b/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java
index 847b0f9..163146f 100644
--- a/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java
+++ b/ApacheCMDA_Frontend_1.0/app/controllers/ServiceExecutionLogController.java
@@ -374,7 +374,8 @@
ServiceExecutionLog newServiceLog = new ServiceExecutionLog();
newServiceLog.setId(json.get("id").asLong());
newServiceLog.setServiceId(json.get("climateService").get("id").asLong());
- newServiceLog.setServiceName(json.get("climateService").get("name").asText());
+ String serviceName = json.get("climateService").get("name").asText();
+ newServiceLog.setServiceName(serviceName);
newServiceLog.setPurpose(json.get("purpose").asText());
newServiceLog.setUserName(json.get("user").get("firstName").asText()
+ " " + json.get("user").get("lastName").asText());
@@ -407,7 +408,7 @@
tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyStartTime);
if (tmpTime != null) {
- newServiceLog.setDataSetStartTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+ newServiceLog.setDataSetStartTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
}
} catch (ParseException e){
// e.printStackTrace();
@@ -417,13 +418,20 @@
tmpTime = (new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a")).parse(datasetStudyEndTime);
if (tmpTime != null) {
- newServiceLog.setDataSetEndTime(new SimpleDateFormat("YYYYMM").format(tmpTime));
+ newServiceLog.setDataSetEndTime(new SimpleDateFormat("YYYY-MM").format(tmpTime));
}
} catch (ParseException e){
// e.printStackTrace();
}
newServiceLog.setDatasetLogId(json.findPath("datasetLogId").asText());
+ if(json.get("url") != null) {
+ String pageUrl = Constants.URL_SERVER
+ + Constants.LOCAL_HOST_PORT + "/assets/html/service"
+ + serviceName.substring(0, 1).toUpperCase()
+ + serviceName.substring(1) + ".html" + json.get("url").asText();
+ newServiceLog.setUrl(pageUrl);
+ }
return newServiceLog;
}
diff --git a/ApacheCMDA_Frontend_1.0/app/models/Dataset.java b/ApacheCMDA_Frontend_1.0/app/models/Dataset.java
index f8f4482..d72cfc3 100644
--- a/ApacheCMDA_Frontend_1.0/app/models/Dataset.java
+++ b/ApacheCMDA_Frontend_1.0/app/models/Dataset.java
@@ -44,6 +44,9 @@
private String startTime;
private String endTime;
private String duration;
+ private String agencyURL;
+ private String instrumentURL;
+ private List<ClimateService> climateServices;
public Dataset() {
}
@@ -56,7 +59,7 @@
String responsiblePerson, String variableNameInWebInterface,
String dataSourceInputParameterToCallScienceApplicationCode,
String variableNameInputParameterToCallScienceApplicationCode,
- String comment, String duration) {
+ String comment, String duration, String agencyURL, String instrumentURL, List<ClimateService> climateServices) {
super();
this.name = name;
this.dataSourceNameinWebInterface = dataSourceNameinWebInterface;
@@ -77,6 +80,33 @@
this.variableNameInputParameterToCallScienceApplicationCode = variableNameInputParameterToCallScienceApplicationCode;
this.comment = comment;
this.duration = duration;
+ this.agencyURL = agencyURL;
+ this.instrumentURL = instrumentURL;
+ this.climateServices = climateServices;
+ }
+
+ public List<ClimateService> getClimateServices() {
+ return climateServices;
+ }
+
+ public void setClimateServices(List<ClimateService> climateServices) {
+ this.climateServices = climateServices;
+ }
+
+ public String getInstrumentURL() {
+ return instrumentURL;
+ }
+
+ public void setInstrumentURL(String instrumentURL) {
+ this.instrumentURL = instrumentURL;
+ }
+
+ public String getAgencyURL() {
+ return agencyURL;
+ }
+
+ public void setAgencyURL(String agencyURL) {
+ this.agencyURL = agencyURL;
}
public String getDuration() {
diff --git a/ApacheCMDA_Frontend_1.0/app/models/Instrument.java b/ApacheCMDA_Frontend_1.0/app/models/Instrument.java
index 71a0094..90ac4c1 100644
--- a/ApacheCMDA_Frontend_1.0/app/models/Instrument.java
+++ b/ApacheCMDA_Frontend_1.0/app/models/Instrument.java
@@ -15,6 +15,7 @@
private String name;
private String description;
private Date launchDate;
+ private String instrumentURL;
public Instrument() {
}
@@ -25,6 +26,14 @@
this.description = description;
this.launchDate = launchDate;
}
+
+ public String getInstrumentURL() {
+ return instrumentURL;
+ }
+
+ public void setInstrumentURL(String instrumentURL) {
+ this.instrumentURL = instrumentURL;
+ }
public long getId() {
return id;
diff --git a/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java b/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java
index aff9e87..fa08477 100644
--- a/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java
+++ b/ApacheCMDA_Frontend_1.0/app/models/ServiceExecutionLog.java
@@ -15,6 +15,8 @@
private String serviceName;
private String dataSetStartTime;
private String dataSetEndTime;
+ private String url;
+
public long getId() {
return id;
@@ -55,6 +57,10 @@
public String getDataUrl() {
return dataUrl;
}
+
+ public String getUrl() {
+ return url;
+ }
public void setId(long id) {
this.id = id;
@@ -120,6 +126,10 @@
this.dataSetEndTime = dataSetEndTime;
}
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
@Override
public String toString() {
return "ServiceExecutionLog [id=" + id + ", serviceId="
@@ -127,6 +137,7 @@
+ serviceConfigurationId + ", purpose=" + purpose
+ ", executionStartTime=" + executionStartTime
+ ", executionEndTime=" + executionEndTime + ", plotUrl="
- + plotUrl + ", dataUrl=" + dataUrl + "]";
+ + plotUrl + ", dataUrl=" + dataUrl + ", url=" + url
+ + "]";
}
}
diff --git a/ApacheCMDA_Frontend_1.0/app/utils/Constants.java b/ApacheCMDA_Frontend_1.0/app/utils/Constants.java
index 2ed9d92..2a36d81 100644
--- a/ApacheCMDA_Frontend_1.0/app/utils/Constants.java
+++ b/ApacheCMDA_Frontend_1.0/app/utils/Constants.java
@@ -25,9 +25,11 @@
public static final String GET_MOST_RECENTLY_USED_CLIMATE_SERVICES_CALL = "/climateService/getAllMostRecentClimateServicesByLatestAccessTime/json";
public static final String GET_CLIMATE_SERVICES_CALL = "/climateService/getAllClimateServices/json";
+ public static final String GET_TOP_K_USED_CLIMATE_SERVICES_BY_DATASET_ID = "/climateService/getTopKUsedClimateServicesByDatasetId";
// climate service page
public static final String SAVE_CLIMATE_SERVICE_PAGE = "/climateService/savePage";
+ public static final String QUERY_CLIMATE_SERVICE = "/climateService/queryClimateService";
// user
public static final String IS_USER_VALID = "/users/isUserValid";
@@ -79,5 +81,8 @@
public static final String CONFIG_ITEM = "/serviceConfigurationItem";
public static final String GET_CONFIG_ITEMS_BY_CONFIG= "/serviceConfigurationItemByServiceConfig";
+ //Analytics
+ public static final String GET_DATASET_AND_USER = "/analytics/getAllDatasetAndUserWithCount/json";
+
}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html b/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html
index ba88ee2..320844a 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/aboutUs.scala.html
@@ -11,8 +11,11 @@
<p>Seungwon Lee</p>
<p>Lei Pan</p>
<h3><strong>Current Contributors</strong></h3>
- <p>Xing Wei</p>
<p>Wei Wang</p>
+ <p>Qihao Bao</p>
+ <p>Ruoxiao Wang</p>
+ <p>Ming Qi</p>
+ <p>Xing Wei</p>
<p>Chris Lee</p>
<p>Rao Li</p>
<p>Chenran Gong</p>
@@ -21,7 +24,6 @@
<p>Yichen Liu</p>
<p>Edward Huang</p>
<p>Zhiyu Lin</p>
- <p>Ming Qi</p>
<p>Jian Jiao</p>
<p>Juanchen Li</p>
<h3><strong>Former Contributors</strong></h3>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html b/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html
index 547e8d1..6dabf7f 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/allClimateServices.scala.html
@@ -11,10 +11,10 @@
<th style = "vertical-align: middle;" class="col-md-4">Purpose</th>
<th style = "vertical-align: middle;" class="col-md-4">URL</th>
<th style = "vertical-align: middle;" class="col-md-3">Scenario</th>
- <th style = "vertical-align: middle;" class="col-md-3">Version_No</th>
+ <th style = "vertical-align: middle;" class="col-md-3">Version No</th>
<th style = "vertical-align: middle;" class="col-md-3">Root Service_Id</th>
@if(true){
- <th style = "vertical-align: middle;" class="col-md-3">Operation</th>
+ <th style = "vertical-align: middle;" class="col-md-2">Operation</th>
}
<th style = "vertical-align: middle;" class="col-md-2">Operation</th>
</tr>
@@ -27,14 +27,21 @@
<td><span class="@climateService.getName() editable" data-name='purpose'>
@{
if (climateService.getPurpose().length > 5 ) {
- climateService.getPurpose().substring(0,5);
+ climateService.getPurpose().substring(0,5) + "...";
}else {
climateService.getPurpose();
}
- } ... </span></td>
+ } </span></td>
<td><span class="@climateService.getName() editable" id = "url" data-name='url'>
- <a href = "@climateService.getUrl()">@climateService.getUrl()</a></span></td>
+ <a href = "@climateService.getUrl()">
+ @{
+ if (climateService.getUrl().length > 5 ) {
+ "..." + climateService.getUrl().substring(31);
+ }else {
+ climateService.getUrl();
+ }
+ }</a></span></td>
<td><span class="@climateService.getName() editable" data-name='scenario'>
@climateService.getScenario() </span></td>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html b/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html
index 8976c6b..50649d4 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/allDatasets.scala.html
@@ -12,7 +12,7 @@
-->
<th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
<th style = "vertical-align: top;" class="col-md-1">Agency</th>
- <th style = "vertical-align: top;" class="col-md-1">Instrument</th>
+ <th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
<th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
<th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
<th style = "vertical-align: top;" class="col-md-1">Units</th>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/climateServiceList.scala.html b/ApacheCMDA_Frontend_1.0/app/views/climateServiceList.scala.html
new file mode 100644
index 0000000..f871090
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/climateServiceList.scala.html
@@ -0,0 +1,62 @@
+@(climateServices: List[ClimateService])
+@import helper._
+
+@main("All Climate Services") {
+
+ <h1>@climateServices.size() Climate Services</h1>
+ <table class="table table-striped table-bordered table-condensed ex2 tablesorter" id = "csTable">
+ <thead>
+ <tr class="text-center">
+ <th style = "vertical-align: middle;" class="col-md-2">Climate Service Name</th>
+ <th style = "vertical-align: middle;" class="col-md-4">Purpose</th>
+ <th style = "vertical-align: middle;" class="col-md-4">URL</th>
+ <th style = "vertical-align: middle;" class="col-md-3">Scenario</th>
+ <th style = "vertical-align: middle;" class="col-md-3">Version No</th>
+ <th style = "vertical-align: middle;" class="col-md-3">Root Service_Id</th>
+ @if(true){
+ <th style = "vertical-align: middle;" class="col-md-2">Operation</th>
+ }
+ <th style = "vertical-align: middle;" class="col-md-2">Operation</th>
+ </tr>
+ </thead>
+ <tbody>
+ @for(climateService <- climateServices) {
+ <tr>
+ <td><a href = "@climateService.getUrl()">@climateService.getName()</a></td>
+
+ <td><span class="@climateService.getName() editable" data-name='purpose'>
+ @{
+ if (climateService.getPurpose().length > 5 ) {
+ climateService.getPurpose().substring(0,5) + "...";
+ }else {
+ climateService.getPurpose();
+ }
+ } </span></td>
+
+ <td><span class="@climateService.getName() editable" id = "url" data-name='url'>
+ <a href = "@climateService.getUrl()">
+ @{
+ if (climateService.getUrl().length > 5 ) {
+ "..." + climateService.getUrl().substring(31);
+ }else {
+ climateService.getUrl();
+ }
+ }</a></span></td>
+
+ <td><span class="@climateService.getName() editable" data-name='scenario'>
+ @climateService.getScenario() </span></td>
+
+ <td><span class="@climateService.getName() editable" data-name='versionNo'>
+ @climateService.getVersionNo() </span></td>
+
+ <td><span class="@climateService.getName() editable" data-name='rootServiceId'>
+ @climateService.getRootServiceId() </span></td>
+
+ <td><input type="file" class="btn btn-info" id ="upload @climateService.getName()" ></button></td>
+ <td><button type="button" class="btn btn-danger" id ="doReplace" onclick="Javascript:replaceFile('upload '+'@climateService.getName()')" >Execute</button></td>
+
+ </tr>
+ }
+ </tbody>
+ </table>
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html b/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html
index 83aeb18..078c6c8 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/dataSetList.scala.html
@@ -72,15 +72,15 @@
<div class="well col-lg-offset-4 col-lg-4 col-sm-offset-3 col-sm-6">
<div class="text-center">
<img class="card-img-top" style=" width:60%;"
- src='@routes.Assets.at("images/data.png")' >
+ src='@routes.Assets.at("images/giphy.gif")' >
</div>
<div class="card-block text-center">
<h4 class="card-title">@dataSet.getName()</h4>
<p class="card-text text-muted">A data set (or dataset) is a collection of data.</p>
</div>
<ul class="list-group list-group-flush">
- <li class="list-group-item"><strong>Agency:</strong> <code>@dataSet.getAgencyId()</code></li>
- <li class="list-group-item"><strong>Instrument:</strong> <code>@dataSet.getInstrument()</code></li>
+ <li class="list-group-item"><strong>Agency:</strong> <a href="@dataSet.getAgencyURL()" target="_blank"><code><u>@dataSet.getAgencyId()</u></code></a></li>
+ <li class="list-group-item"><strong>Instrument/Model Experiment:</strong> <a href="@dataSet.getInstrumentURL()" target="_blank"><code><u>@dataSet.getInstrument()</u></code></a></li>
<li class="list-group-item"><strong>Units:</strong> <code>@dataSet.getUnits()</code></li>
<li class="list-group-item"><strong>Start Time:</strong> <code>@dataSet.getStartTime()</code></li>
<li class="list-group-item"><strong>End Time:</strong> <code>@dataSet.getEndTime()</code></li>
@@ -102,15 +102,20 @@
</div>
<div class="row">
+ @for(climateService <- dataSet.getClimateServices()) {
<div class="col-md-6">
<div class="thumbnail">
+ <a href="@climateService.getUrl()">
<img src='@routes.Assets.at("images/github.png")' >
+ </a>
<div class="caption">
- <h5>NASA</h5>
- <p><a href="#" class="btn btn-primary" role="button">Go</a> <a href="#" class="btn btn-default" role="button">Ignore</a></p>
+ <h5>@climateService.getName()</h5>
+
</div>
</div>
</div>
+ }
+ <!--
<div class="col-md-6">
<div class="thumbnail">
<img src='@routes.Assets.at("images/bug.png")' >
@@ -120,6 +125,7 @@
</div>
</div>
</div>
+ -->
</div>
</div>
@@ -162,7 +168,7 @@
-->
<th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
<th style = "vertical-align: top;" class="col-md-1">Agency</th>
- <th style = "vertical-align: top;" class="col-md-1">Instrument</th>
+ <th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
<th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
<th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
<th style = "vertical-align: top;" class="col-md-1">Units</th>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/dataSetListPopular.scala.html b/ApacheCMDA_Frontend_1.0/app/views/dataSetListPopular.scala.html
new file mode 100644
index 0000000..9df2a3d
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/dataSetListPopular.scala.html
@@ -0,0 +1,68 @@
+@(dataSetForm: play.data.Form[Dataset], dataSets: List[Dataset])
+
+@import helper._
+@import java.math.BigInteger;var k=1;var n = 0;
+
+@scripts = {
+ <script src='@routes.Assets.at("javascripts/edit_button.js")'></script>
+ <script type="text/javascript">
+ $(document).ready(function(){
+ //alert($("#url").text());
+ });
+ </script>
+}
+
+@main("Dataset List", scripts) {
+
+ <h1>Datasets List</h1>
+ <h2>@dataSets.size() Datasets Found</h2>
+<div style="overflow-y:scroll">
+ <table class="table table-striped table-bordered table-condensed tablesorter" id ="myTable">
+ <thead>
+ <tr >
+ <!--
+ <th style = "vertical-align: top;" class="col-md-1 header">Id</th>
+ -->
+ <th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
+ <th style = "vertical-align: top;" class="col-md-1">Agency</th>
+ <th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
+ <th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
+ <th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
+ <th style = "vertical-align: top;" class="col-md-1">Units</th>
+ <th style = "vertical-align: top;" class="col-md-1">Grid Dimension</th>
+
+ <th style = "vertical-align: top;" class="col-md-2">Variable Name in Web Interface</th>
+ <th style = "vertical-align: top;" class="col-md-1">Data Source Input Parameter</th>
+
+ <th style = "vertical-align: top;" class="col-md-1">Dataset Start Time</th>
+ <th style = "vertical-align: top;"class="col-md-1">Dataset End Time</th>
+ <th style = "vertical-align: top;"class="col-md-1">Duration</th>
+
+ </tr>
+ </thead>
+ <tbody>
+@for(dataSet <- dataSets){
+ <tr>
+ <!--
+ <td><font size="2">@dataSet.getId()</font></td>
+ -->
+ <td><font size="2">@dataSet.getName()</font></td>
+ <td><font size="2">@dataSet.getAgencyId()</font></td>
+ <td><font size="2">@dataSet.getInstrument()</font></td>
+ <td><font size="2">@dataSet.getPhysicalVariable()</font></td>
+ <td><font size="2">@dataSet.getCMIP5VarName()</font></td>
+ <td><font size="2">@dataSet.getUnits()</font></td>
+ <td><font size="2">@dataSet.getGridDimension()</font></td>
+ <td><font size="2">@dataSet.getVariableNameInWebInterface()</font></td>
+ <td><font size="2">@dataSet.getDataSourceInputParameterToCallScienceApplicationCode()</font></td>
+ <td><font size="2">@dataSet.getStartTime()</font></td>
+ <td><font size="2">@dataSet.getEndTime()</font></td>
+ <td><font size="2">@dataSet.getDuration()</font></td>
+ </tr>
+ }
+
+
+ </tbody>
+ </table>
+ </div>
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/header.scala.html b/ApacheCMDA_Frontend_1.0/app/views/header.scala.html
index c67371d..3446fdd 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/header.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/header.scala.html
@@ -10,8 +10,8 @@
</button>
<a class="navbar-brand" style="padding-bottom: 0;" href="/">
<div>
- <img src='@routes.Assets.at("images/NASA_JPL_logo.png")' style="height: 30px; width: 180px;">  
- <img src='@routes.Assets.at("images/logo.png")' style="height: 15px; width: 200px;">
+ <img src='@routes.Assets.at("images/NASA_JPL_logo.png")' style="height: 24px; width: 144px;">
+ <img src='@routes.Assets.at("images/logo.png")' style="height: 12px; width: 160px;">
</div>
</a>
</div>
@@ -19,6 +19,14 @@
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="text-danger">Recommendation</span><b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="@routes.ClimateServiceController.recommendationSummary()">Summary</a></li>
+
+ </ul>
+ </li>
+
+ <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Web Service<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="@routes.ClimateServiceController.mostRecentlyAddedClimateServices()">Most Recently Added</a></li>
@@ -28,14 +36,14 @@
@if(true) {
<li><a href="@routes.ClimateServiceController.addAClimateService()">Register A Service</a></li>
}
- <li><a>Search Service</a></li>
+ <li><a href="@routes.ClimateServiceController.searchClimateServices()">Search Service</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Dataset<b class="caret"></b></a>
<ul class="dropdown-menu">
- <li><a>Most Popular</a></li>
+ <li><a href="@routes.DatasetController.mostPopularDatasets()">Most Popular</a></li>
<li><a href="@routes.DatasetController.showAllDatasets()">Dataset List</a></li>
@if(true) {
<li><a href="#">Register A Dataset</a></li>
@@ -50,7 +58,7 @@
<li><a href="@routes.ServiceExecutionLogController.getServiceLog">Service Execution Log</a></li>
<li><a href="@routes.ServiceExecutionLogController.searchServiceLog">Search Service Log</a></li>
<li><a href="@routes.DatasetLogController.getAllDatasetLogs">Dataset Log</a></li>
- <li><a href="@routes.AnalyticsController.getRecommend">Semantic Service Analytics</a></li>
+ <li><a href="@routes.AnalyticsController.getKnowledgeGraph">Knowledge Graph</a></li>
<li><a href="@routes.AnalyticsController.getDatasetRecommend">Semantic Dataset Analytics</a></li>
<li><a href="@routes.AnalyticsController.getScientistRecommend">User Analytics</a></li>
<li><a href="@routes.AnalyticsController.getLogGraph">Service Execution Log Analytics</a></li>
diff --git a/ApacheCMDA_Frontend_1.0/app/views/knowledgeGraph.scala.html b/ApacheCMDA_Frontend_1.0/app/views/knowledgeGraph.scala.html
new file mode 100644
index 0000000..fe6a223
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/knowledgeGraph.scala.html
@@ -0,0 +1,320 @@
+@(jsonData: String)
+@import helper._
+
+@scripts = {
+ <link rel="stylesheet" href='@routes.Assets.at("stylesheets/vis.css")'>
+ <style type="text/css">
+ html, body {
+ font: 10pt arial;
+ }
+ #mynetwork {
+ width: 600px;
+ height: 600px;
+ border: 1px solid lightgray;
+ }
+ #loadingBar {
+ position:absolute;
+
+ width: 610px;
+ height: 610px;
+ background-color:rgba(200,200,200,0.8);
+ -webkit-transition: all 0.5s ease;
+ -moz-transition: all 0.5s ease;
+ -ms-transition: all 0.5s ease;
+ -o-transition: all 0.5s ease;
+ transition: all 0.5s ease;
+ opacity:1;
+ }
+ #wrapper {
+ position:relative;
+ width:900px;
+ height:900px;
+ }
+
+ #text {
+ position:absolute;
+ top:0px;
+ left:530px;
+ width:30px;
+ height:50px;
+ margin:auto auto auto auto;
+ font-size:22px;
+ color: #000000;
+ }
+
+
+ div.outerBorder {
+ position:relative;
+ top:300px;
+ width:600px;
+ height:44px;
+ margin:auto auto auto auto;
+ border:8px solid rgba(0,0,0,0.1);
+ background: rgb(252,252,252); /* Old browsers */
+ background: -moz-linear-gradient(top, rgba(252,252,252,1) 0%, rgba(237,237,237,1) 100%); /* FF3.6+ */
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(252,252,252,1)), color-stop(100%,rgba(237,237,237,1))); /* Chrome,Safari4+ */
+ background: -webkit-linear-gradient(top, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* Chrome10+,Safari5.1+ */
+ background: -o-linear-gradient(top, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* Opera 11.10+ */
+ background: -ms-linear-gradient(top, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* IE10+ */
+ background: linear-gradient(to bottom, rgba(252,252,252,1) 0%,rgba(237,237,237,1) 100%); /* W3C */
+ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fcfcfc', endColorstr='#ededed',GradientType=0 ); /* IE6-9 */
+ border-radius:72px;
+ box-shadow: 0px 0px 10px rgba(0,0,0,0.2);
+ }
+
+ #border {
+ position:absolute;
+ top:2px;
+ left:2px;
+ width:500px;
+ height:23px;
+ margin:auto auto auto auto;
+ box-shadow: 0px 0px 4px rgba(0,0,0,0.2);
+ border-radius:10px;
+ }
+
+ #bar {
+ position:absolute;
+
+ width:10px;
+ height:20px;
+ margin:auto auto auto auto;
+ border-radius:11px;
+ border:2px solid rgba(30,30,30,0.05);
+ background: rgb(153, 255, 255); /* Old browsers */
+ box-shadow: 2px 2px 4px rgba(0,0,0,0.4);
+ }
+
+ #config {
+
+ width: 400px;
+ height: 600px;
+
+ }
+ #config input {
+ display: inline-block;
+ }
+ #config input.vis-configuration.vis-config-rangeinput {
+ height: 15px;
+ }
+ #config button, input, select, textarea {
+ line-height: 100%;
+ }
+ </style>
+
+ <script type="text/javascript" src='@routes.Assets.at("javascripts/exampleUtil.js")'></script>
+ <script type="text/javascript" src='@routes.Assets.at("javascripts/vis.js")'></script>
+ <script type="text/javascript">
+ $(document).ready(function() {
+ $("#panel").hide();
+ $("#config").hide();
+
+ draw();
+ })
+
+ var nodes = null;
+ var edges = null;
+ var network = null;
+
+ var visConfig = 1;
+
+ function hideConfig() {
+ if (visConfig == 0) {
+ $("#config").hide();
+ visConfig =1;
+ }else {
+ $("#config").show();
+ visConfig =0;
+ }
+ }
+
+ function draw() {
+ console.log("draw");
+ // create people.
+ // value corresponds with the age of the person
+ var jsonString = $('#jsonData').text();
+ var test = jsonString;
+
+ //test = JSON.stringify(test);
+ console.log("test: " + test);
+ test = JSON.parse(test);
+ var nodes = test.nodes;
+ console.log("nodes: " + nodes);
+ var edges = test.edges;
+ console.log("edges: " + edges);
+
+ // Instantiate our network object.
+ var container = document.getElementById('mynetwork');
+ var data = {
+ nodes: nodes,
+ edges: edges
+ };
+ var options = {
+ nodes: {
+ color: {
+ highlight: {
+ border: "red",
+ background: "pink"
+ }
+ },
+ scaling: {
+ min: 12
+ },
+ shadow: {
+ enabled: true,
+ size: 8
+ },
+ font: {
+ color: "rgba(52,52,52,1)",
+ size: 10,
+ strokeWidth: 4
+ },
+ shape: "dot",
+ shapeProperties: {
+ borderRadius: 5
+ }
+ },
+ edges: {
+ arrows: {
+ to: {
+ enabled: true,
+ scaleFactor: 0.4
+ }
+ },
+ color: {
+ highlight: "rgba(244,1,0,1)",
+ hover: "rgba(0,0,0,1)",
+ inherit: false
+ },
+ smooth: {
+ type: "continuous",
+ forceDirection: "vertical"
+ }
+ },
+ interaction: {
+ hover: true,
+ multiselect: true
+ },
+ groups: {
+ dataset: {
+ color:"#A9E2F3"
+ },
+ user: {
+ color:"#F781F3"
+ }
+ },
+
+ physics: {
+ barnesHut: {
+ centralGravity: 1.95,
+ springLength: 195,
+ springConstant: 0.185,
+ damping: 0.25,
+ avoidOverlap: 0.2
+ },
+ maxVelocity: 10,
+ minVelocity: 0.75,
+ stabilization: {
+ enabled:true,
+ iterations:200,
+ updateInterval:1
+ }
+ },
+ configure: {
+ filter:function (option, path) {
+ if (path.indexOf('physics') !== -1 || option === 'physics') {
+ return true;
+ }
+ if (path.indexOf('smooth') !== -1 || option === 'smooth') {
+ return true;
+ }
+ return true;
+ },
+ container: document.getElementById('config')
+ }
+ };
+ network = new vis.Network(container, data, options);
+
+ network.on("stabilizationProgress", function(params) {
+ var maxWidth = 496;
+ var minWidth = 20;
+ var widthFactor = params.iterations/params.total;
+ var width = Math.max(minWidth,maxWidth * widthFactor);
+
+ document.getElementById('bar').style.width = width + 'px';
+ document.getElementById('text').innerHTML = Math.round(widthFactor*100) + '%';
+ });
+ network.once("stabilizationIterationsDone", function() {
+ document.getElementById('text').innerHTML = '100%';
+ document.getElementById('bar').style.width = '496px';
+ document.getElementById('loadingBar').style.opacity = 0;
+ // really clean the dom element
+ setTimeout(function () {document.getElementById('loadingBar').style.display = 'none';}, 500);
+ });
+
+ //network.focusOnNode(19);
+ network.on('select', function(properties) {
+ var select_node = $.grep(data.nodes, function(e){
+ return e["id"] == properties.nodes[0];
+ })[0];
+
+ $("#nodeName").text(select_node["title"]);
+ //alert(select_node["title"]);
+ $("#id").text(select_node["id"]);
+ $("#cluster").text(select_node["cluster"]);
+ $("#label").text(select_node["label"]);
+
+ $("#panel").show();
+ });
+ }
+
+
+ </script>
+ <script type="text/javascript" src='@routes.Assets.at("javascripts/googleAnalytics.js")'></script>
+}
+
+@main("Knowledge Graph", scripts){
+ <div id="jsonData" style="display: none;">@jsonData</div>
+ <div id="knowledgeGraph">
+
+ <div id="loadingBar" class="col-lg-3">
+ <div class="outerBorder" >
+ <div id="text">0%</div>
+ <div id="border">
+ <div id="bar"></div>
+ </div>
+ </div>
+ </div>
+
+ <div id="mynetwork" class="col-lg-3"></div>
+ <div class = "col-lg-offset-7">
+ <button type="button" onclick="hideConfig()" class="btn btn-default btn-lg "><span class="glyphicon glyphicon-star" aria-hidden="true">
+ </span> Show Advanced Settings </button>
+ </div>
+ <div id="config" class="col-lg-offset-7"></div>
+ </div>
+
+ <br>
+
+ <div id="panel" class="col-lg-offset-7">
+ <div class="well col-lg-5">
+ <div class="text-center">
+ <img class="card-img-top" style=" width:60%;" src="/assets/images/data.png">
+ </div>
+ <div class="card-block text-center">
+ <h4 class="card-title" id="nodeName">card</h4>
+ <p class="card-text text-muted" >A data set (or dataset) is a collection of data.</p>
+ </div>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item"><strong>Id:</strong><code id="id"></code></li>
+ <li class="list-group-item"><strong>Cluster:</strong><code id="cluster"></code></li>
+ <li class="list-group-item"><strong>Label:</strong><code id="label"></code></li>
+ </ul>
+ <div class="card-block">
+ <a href="#" class="card-link col-lg-offset-0 col-lg-6">Card link</a>
+ <a href="#" class="card-link col-lg-6">Another link</a>
+ </div>
+ </div>
+ </div>
+
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/main.scala.html b/ApacheCMDA_Frontend_1.0/app/views/main.scala.html
index 37a5caa..a41f414 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/main.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/main.scala.html
@@ -32,8 +32,6 @@
href='@routes.Assets.at("stylesheets/livefitler.css")'>
<link rel="stylesheet"
href='@routes.Assets.at("stylesheets/custom_recommend.css")'>
-<link rel="stylesheet"
- href="https://cdnjs.cloudflare.com/ajax/libs/vis/3.11.0/vis.min.css">
<link rel="stylesheet"
href="http://code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css">
diff --git a/ApacheCMDA_Frontend_1.0/app/views/recommendationSummary.scala.html b/ApacheCMDA_Frontend_1.0/app/views/recommendationSummary.scala.html
new file mode 100644
index 0000000..7cd4d3f
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/recommendationSummary.scala.html
@@ -0,0 +1,171 @@
+@(climateServices: List[ClimateService], dataSets: List[Dataset], users: List[User])
+
+@import helper._
+
+@scripts = {
+ <script type="text/javascript">
+ $(document).ready(function(){
+ $('#myTabs a').click(function (e) {
+ e.preventDefault()
+ $(this).tab('show')
+ })
+ });
+ </script>
+}
+
+@main("Climate Services", scripts) {
+
+ <h1>Recommendation</h1>
+
+ <div>
+ <!-- Nav tabs -->
+ <ul class="nav nav-tabs" role="tablist">
+ <li role="presentation" class="active"><a href="#mostPopService" aria-controls="home" role="tab" data-toggle="tab">Most Popular Service</a></li>
+ <li role="presentation"><a href="#mostPopDataset" aria-controls="profile" role="tab" data-toggle="tab">Most Popular Dataset</a></li>
+ <li role="presentation"><a href="#mostActUsers" aria-controls="messages" role="tab" data-toggle="tab">Most Active User</a></li>
+ <li role="presentation"><a href="#settings" aria-controls="settings" role="tab" data-toggle="tab">Settings</a></li>
+ </ul>
+ <!-- Tab panes -->
+ <div class="tab-content">
+ <div role="tabpanel" class="tab-pane active" id="mostPopService">
+ <table class="table table-striped table-bordered table-condensed ex2">
+ <tr>
+ <td class="col-md-2">Climate Service Name</td>
+ <td class="col-md-2">Purpose</td>
+ <td class="col-md-4">Url</td>
+ <td class="col-md-2">Scenario</td>
+ <td class="col-md-2">Version</td>
+ <td class="col-md-2">Root_Service</td>
+ </tr>
+
+ @for(climateService <- climateServices){
+ <tr>
+ <td><a href = "@climateService.getUrl()">@climateService.getName()</a></td>
+
+ <td><span class="@climateService.getName() editable" data-name='purpose'>
+ @climateService.getPurpose() </span></td>
+
+ <td><span class="@climateService.getName() editable" id = "url" data-name='url'>
+ <a href = "@climateService.getUrl()">
+ @climateService.getUrl()</a> </span></td>
+
+ <td><span class="@climateService.getName() editable" data-name='scenario'>
+ @climateService.getScenario() </span></td>
+
+ <td><span class="@climateService.getName() editable" data-name='versionNo'>
+ @climateService.getVersionNo() </span></td>
+
+ <td><span class="@climateService.getName() editable" data-name='rootServiceId'>
+ @climateService.getRootServiceId() </span></td>
+ </tr>
+ }
+ </table>
+ </div>
+
+ <div role="tabpanel" class="tab-pane" id="mostPopDataset">
+ <table class="table table-striped table-bordered table-condensed ex2 tablesorter" >
+ <thead>
+ <tr>
+ <!--
+ <th style = "vertical-align: top;" class="col-md-1 header">Id</th>
+ -->
+ <th style = "vertical-align: top;" class="col-md-2">Dataset Name</th>
+ <th style = "vertical-align: top;" class="col-md-1">Agency</th>
+ <th style = "vertical-align: top;" class="col-md-1">Instrument/Model Experiment</th>
+ <th style = "vertical-align: top;" class="col-md-2">Physical variable</th>
+ <th style = "vertical-align: top;" class="col-md-1">Variable short name</th>
+ <th style = "vertical-align: top;" class="col-md-1">Units</th>
+ <th style = "vertical-align: top;" class="col-md-1">Grid Dimension</th>
+ <!--
+ <th style = "vertical-align: top;" class="col-md-3">Source</th>
+ <th style = "vertical-align: top;" class="col-md-3">Status</th>
+ <th style = "vertical-align: top;" class="col-md-4">Responsible Person</th>
+ <th style = "vertical-align: top;" class="col-md-4">Data Source Name in Web Interface</th>
+ -->
+ <th style = "vertical-align: top;" class="col-md-2">Variable Name in Web Interface</th>
+ <th style = "vertical-align: top;" class="col-md-1">Data Source Input Parameter</th>
+ <!-- <th style = "vertical-align: top;" class="col-md-3">Variable Name Input Parameter</th> -->
+ <th style = "vertical-align: top;" class="col-md-1">Dataset Start Time</th>
+ <th style = "vertical-align: top;"class="col-md-1">Dataset End Time</th>
+ <th style = "vertical-align: top;"class="col-md-1">Duration</th>
+ </tr>
+ </thead>
+ <tbody>
+ @for(dataSet <- dataSets){
+ <tr>
+ <!--
+ <td><font size="2">@dataSet.getId()</font></td>
+ -->
+ <td><font size="2">@dataSet.getName()</font></td>
+ <td><font size="2">@dataSet.getAgencyId()</font></td>
+ <td><font size="2">@dataSet.getInstrument()</font></td>
+ <td><font size="2">@dataSet.getPhysicalVariable()</font></td>
+ <td><font size="2">@dataSet.getCMIP5VarName()</font></td>
+ <td><font size="2">@dataSet.getUnits()</font></td>
+ <td><font size="2">@dataSet.getGridDimension()</font></td>
+ <!--
+ <td><font size="2">@dataSet.getSource()</font></td>
+ <td><font size="2">@dataSet.getStatus()</font></td>
+ <td><font size="2">@dataSet.getResponsiblePerson()</font></td>
+ <td><font size="2">@dataSet.getDataSourceNameinWebInterface()</font></td>
+ -->
+ <td><font size="2">@dataSet.getVariableNameInWebInterface()</font></td>
+ <td><font size="2">@dataSet.getDataSourceInputParameterToCallScienceApplicationCode()</font></td>
+ <!--<td><font size="2">@dataSet.getVariableNameInputParameterToCallScienceApplicationCode()</font></td> -->
+ <td><font size="2">@dataSet.getStartTime()</font></td>
+ <td><font size="2">@dataSet.getEndTime()</font></td>
+ <td><font size="2">@dataSet.getDuration()</font></td>
+ </tr>
+ }
+ </tbody>
+ </table>
+
+ </div>
+ <div role="tabpanel" class="tab-pane" id="mostActUsers">
+
+ <table class="table table-striped table-bordered table-condensed ex2">
+ <tr>
+ <td class="col-md-1">Id</td>
+ <td class="col-md-2">User Name</td>
+ <td class="col-md-2">First Name</td>
+ <td class="col-md-2">Middle Name</td>
+ <td class="col-md-2">Last Name</td>
+ <td class="col-md-2">Afflication</td>
+ <td class="col-md-4">Email</td>
+ <td class="col-md-2">Research Area</td>
+
+ </tr>
+ @for(user <- users){
+ <tr>
+ <td>@user.getId()</td>
+
+ <td><span class="@user.getId() editable" data-name='username'>@user.getUserName()</span></td>
+
+ <td><span class="@user.getId() editable" data-name='firstName'>@user.getFirstName()</span></td>
+
+ <td><span class="@user.getId() editable" data-name='middleInitial'>@user.getMiddleInitial()</span></td>
+
+ <td><span class="@user.getId() editable" data-name='lastName'>@user.getLastName()</span></td>
+
+ <td><span class="@user.getId() editable" data-name='affiliation'>@user.getAffiliation()</span></td>
+
+ <td><span class="@user.getId() editable" data-name='email'>@user.getEmail()</span></td>
+
+ <td><span class="@user.getId() editable" data-name='researchFields'>@user.getResearchFields()</span></td>
+
+ </tr>
+ }
+ </table>
+
+
+
+ </div>
+ <div role="tabpanel" class="tab-pane" id="settings">...</div>
+ </div>
+ </div>
+
+
+
+
+
+}
diff --git a/ApacheCMDA_Frontend_1.0/app/views/searchClimateService.scala.html b/ApacheCMDA_Frontend_1.0/app/views/searchClimateService.scala.html
new file mode 100644
index 0000000..84ae278
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/app/views/searchClimateService.scala.html
@@ -0,0 +1,117 @@
+@(climateServiceForm: play.data.Form[ClimateService])
+
+@import helper._
+
+@scripts = {
+ <script src='@routes.Assets.at("javascripts/edit_button.js")'></script>
+ <link rel="stylesheet" href="//code.jquery.com/ui/1.11.3/themes/smoothness/jquery-ui.css">
+ <script src="//code.jquery.com/jquery-1.10.2.js"></script>
+ <script src="//code.jquery.com/ui/1.11.3/jquery-ui.js"></script>
+ <script type="text/javascript">
+ $(document)
+ .ready(
+ function() {
+ $("#preview")
+ .click(
+ function() {
+ var target = document
+ .getElementById("show");
+ if (target.style.display == "none") {
+ target.style.display = "block";
+ $("#preview").text("Hide");
+ var climateServiceName = $(
+ "#climateServiceName")
+ .val();
+ var purpose = $("#purpose")
+ .val();
+ var scenario = $(
+ "#scenario")
+ .val();
+ var url = $(
+ "#url")
+ .val();
+ var versionNo = $(
+ "#versionNo")
+ .val();
+
+ if (climateServiceName != "") {
+ $("#content")
+ .append(
+ " Climate Service Name = "
+ + climateServiceName);
+ }
+ if (purpose != ""
+ && purpose != null) {
+ $("#content")
+ .append(
+ " Purpose = "
+ + purpose);
+ }
+
+ if (scenario != ""
+ && scenario != null) {
+ $("#content")
+ .append(
+ " Scenario = "
+ + scenario);
+ }
+ if (url != ""
+ && url != null) {
+ $("#content")
+ .append(
+ " URL = "
+ + url);
+ }
+ if (versionNo != ""
+ && versionNo != null) {
+ $("#content")
+ .append(
+ " Version Number = "
+ + versionNo);
+ }
+
+ } else {
+ $("#content").val('');
+ target.style.display = "none";
+ $("#preview").text(
+ "Preview");
+ }
+ });
+ });
+ </script>
+}
+
+@main("Search Climate Service", scripts){
+
+ <h1 style="margin-left:420px">Search Climate Service</h1>
+ @helper.form(routes.ClimateServiceController.getSearchResult()) {
+ <div class="ui-widget col-sm-offset-3 col-sm-7">
+ <div class = "form-group">
+ @inputText(climateServiceForm("Climate Service Name"), 'class -> "form-control", 'id -> "climateServiceName", '_label -> Messages("Climate Service Name"), 'placeholder -> "twoDimSlice3D", 'size->70)
+ </div>
+ <div class = "form-group">
+ @inputText(climateServiceForm("Purpose"), 'class -> "form-control", 'id -> "purpose", '_label -> Messages("Purpose"), 'placeholder -> "service purpose", 'size->70)
+ </div>
+
+ <div class = "form-group">
+ @inputText(climateServiceForm("Scenario"), 'class -> "form-control", 'id -> "scenario", '_label -> Messages("Scenario"), 'placeholder -> "1", 'size->70)
+ </div>
+ <div class = "form-group">
+ @inputText(climateServiceForm("Url"), 'class -> "form-control", 'id -> "url", '_label -> Messages("Url"), 'placeholder -> "http://", 'size->70)
+
+ </div>
+ <div class = "form-group">
+ @inputText(climateServiceForm("Version Number"), 'class -> "form-control", 'id -> "versionNo", '_label -> Messages("Version Number"), 'placeholder -> "1", 'size->70)
+
+ <div id="show" style="display: none;">
+ <textarea style="width: 640px" rows="4" id="content"></textarea>
+ </div>
+ </div>
+ <div align="center">
+ <button id="preview" type="button" class="btn btn-info"> Preview</button>
+ <input class="btn" type="submit" value="Search">
+ </div>
+ </div>
+
+ }
+ }
\ No newline at end of file
diff --git a/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html b/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html
index b5d99f2..1ef867e 100644
--- a/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html
+++ b/ApacheCMDA_Frontend_1.0/app/views/serviceLog.scala.html
@@ -57,12 +57,19 @@
<td><font size="2">@serviceLog.getDataSetStartTime</font></td>
<td><font size="2">@serviceLog.getDataSetEndTime</font></td>
<td>
- @form(routes.ServiceExecutionLogController.getConfigurationByConfId()){
- <input
- name="logId" class="hidden" type="hidden"
- value="@serviceLog.getId">
- <input
- type="submit" value="see details">
+ @if(serviceLog.getUrl() != null){
+ <form method="post" action="@serviceLog.getUrl">
+ <input type="submit" value="see details">
+ </form>
+
+ }else{
+ @form(routes.ServiceExecutionLogController.getConfigurationByConfId()){
+ <input
+ name="logId" class="hidden" type="hidden"
+ value="@serviceLog.getId">
+ <input
+ type="submit" value="see details">
+ }
}
</td>
<!--
diff --git a/ApacheCMDA_Frontend_1.0/conf/routes b/ApacheCMDA_Frontend_1.0/conf/routes
index 35927f7..47558c9 100644
--- a/ApacheCMDA_Frontend_1.0/conf/routes
+++ b/ApacheCMDA_Frontend_1.0/conf/routes
@@ -12,6 +12,9 @@
GET /createSuccess controllers.Application.createSuccess()
POST /isEmailExisted controllers.Application.isEmailExisted()
+#recommendation overview
+GET /climateService/recommendationSummary controllers.ClimateServiceController.recommendationSummary()
+
# Climate Service
GET /climateService/allServices controllers.ClimateServiceController.showAllClimateServices()
POST /climateService/add controllers.ClimateServiceController.addClimateService()
@@ -24,6 +27,8 @@
GET /climateService/mostPopularServices controllers.ClimateServiceController.mostPopularServices()
GET /climateService/mostRecentlyUsedClimateServices controllers.ClimateServiceController.mostRecentlyUsedClimateServices()
GET /climateService/mostRecentlyAddedClimateServices controllers.ClimateServiceController.mostRecentlyAddedClimateServices()
+GET /climateService/searchClimateService controllers.ClimateServiceController.searchClimateServices()
+GET /climateService/getSearchResult controllers.ClimateServiceController.getSearchResult()
#replace page
POST /climateService/replaceFile controllers.ClimateServiceController.replaceFile()
@@ -39,6 +44,7 @@
GET /dataSet/allDatasets controllers.DatasetController.showAllDatasets()
GET /dataSet/searchDataSet controllers.DatasetController.searchDataset()
GET /dataSet/getSearchResult controllers.DatasetController.getSearchResult()
+GET /dataSet/mostPopularDatasets controllers.DatasetController.mostPopularDatasets()
# Dataset Log
GET /datasetLog/allDatasetLogs controllers.DatasetLogController.getAllDatasetLogs()
@@ -49,6 +55,7 @@
GET /profileRecommend controllers.AnalyticsController.getScientistRecommend()
GET /serviceLogGraph controllers.AnalyticsController.getLogGraph()
GET /searchAndGenerateWorkflow controllers.AnalyticsController.getSearchAndGenerateWorkflow()
+GET /serviceKnowledgeGraph controllers.AnalyticsController.getKnowledgeGraph()
# Users
GET /scientist/allUsers controllers.UsersController.getAllUsers()
@@ -56,6 +63,7 @@
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
+POST /assets/*file controllers.Assets.at(path="/public", file)
# About
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/.DS_Store b/ApacheCMDA_Frontend_1.0/public/html/js2/.DS_Store
new file mode 100644
index 0000000..5008ddf
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/.DS_Store
Binary files differ
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/common.css b/ApacheCMDA_Frontend_1.0/public/html/js2/common.css
new file mode 100644
index 0000000..477fb80
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/common.css
@@ -0,0 +1,29 @@
+ .center1 {text-align: center;}
+ .right1 {text-align: right;}
+ /*.left1 {text-align: left; margin-left: 50px;}*/
+ .left1 {text-align: left;}
+ .middle1 {vertical-align: middle;}
+ .textwrapper {
+ border:1px solid #999999;
+ margin:5px 0;
+ padding:3px;
+ }
+ textarea { width:100%; }
+
+ .subtitle1 {color: DarkBlue; font-weight: bold; }
+
+ /*.color-head {background-color: Aqua;} */
+ .color-head {background-color: #C2EBFF ;} /* A6C0CE *A6C0CE ABD8C0 */
+ .color4 {background-color: #B8B8B8;} /*Azure EBFFF5 */
+ .color3 {background-color: #C8C8C8;} /* Beige C2F0F0 */
+ /* .color3 {border-color: Azure; border-style: solid; } */ /* Beige C2F0F0 */
+ .color2 {background-color: #E8E8E8;} /* YellowGreen B2B2E0 */
+ .color1 {background-color: #D8D8D8;} /* SpringGreen*/
+ .color0 {background-color: #F8F8F8;} /* Turquoise*/
+
+
+@media screen and (max-width: 768px) {
+.right1 {text-align: left;}
+}
+
+
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/common.js b/ApacheCMDA_Frontend_1.0/public/html/js2/common.js
new file mode 100644
index 0000000..625de5f
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/common.js
@@ -0,0 +1,548 @@
+// The following is for the code navigation purpose.
+//
+// enable_download data button
+// disable_pres1__
+// enable_pres1__(ID)
+// put_data__
+// data_block_str__
+// change_datan_
+// put_var__
+// is3D__
+// select_var__
+// time_range__
+// time_range2__
+// time_range3__
+// monthList__
+// fillMonth__
+// reset_months__
+// no_month_check
+// select_all_months__
+// select_months__
+// getMonthStr__
+// parse_pres__
+// get_querystring__
+
+var naValue = "-999999";
+
+// disable download data button
+function disable_download_button()
+{
+ var x=document.getElementById("download_data");
+ x.disabled=true;
+}
+
+// enable_download data button
+function enable_download_button()
+{
+ var x=document.getElementById("download_data");
+ x.disabled=false;
+}
+
+// disable pressure level box for 2D var
+// disable_pres1__
+function disable_pres1(ID)
+{
+ // if isPressure1 is defined, there is no pressure widget
+ try {
+ var x;
+ x=document.getElementById("pres"+ID);
+ x.value = "N/A";
+ x.disabled=true;
+ var y=document.getElementById("pressureLabel"+ID);
+ y.innerHTML = "pressure:";
+ }
+ catch(err) {}
+}
+
+// enable pressure level box for 3D var
+// enable_pres1__(ID)
+function enable_pres1(ID)
+{
+ var var_string = $("#var"+ID).val();
+ var oceanStr = "";
+
+ try {
+ var rangeStr0 = eval("rangeStr"+ID);
+ } catch(err) {
+ var rangeStr0 = "";
+ }
+
+ try {
+ var y=document.getElementById("pressureLabel"+ID);
+ //alert(y.value);
+ y.innerHTML = "atmospheric pressure " + rangeStr0 + "(hPa):";
+ } catch(err) {}
+
+ if (varList[var_string][1]==="ocean") {
+ oceanStr = "o";
+ try {
+ y.innerHTML = "ocean pressure " + rangeStr0 + "(dbar):";
+ } catch(err) {}
+ }
+
+ // there can be no pressure widget, so there is a error catch here.
+ try {
+ var pressDf0;
+ try {
+ pressDf0 = eval("pressDf"+ID+oceanStr);
+ } catch(err) {
+ pressDf0 = "500";
+ }
+
+
+ var x=document.getElementById("pres"+ID);
+ x.value = pressDf0;
+ x.disabled=false;
+ } catch(err) {}
+/* try {
+ if ( eval("typeof pressDf"+ID) !== 'undefined') var pressDf0 = eval("pressDf"+ID);
+ else var pressDf0 = "500";
+
+ var x=document.getElementById("pres"+ID);
+ x.value = pressDf0;
+ x.disabled=false;
+ } catch(err) {}
+*/
+}
+
+// put_data__
+function put_data(ID){
+ var list1=document.getElementById("data"+ID);
+
+ for(var key in dataList) {
+ if (key.slice(0,5)==="group") {
+ var og = document.createElement("OPTGROUP");
+ og.setAttribute('label', dataList[key][0]);
+ list1.add(og);
+
+ } else {
+ var toAdd = true;
+
+ // whether the dataset has only 2D or only 3D variables
+ if ( (typeof isOnly2d !== 'undefined')
+ || (typeof isOnly3d !== 'undefined') ) {
+ var dims = "";
+ var varList2 = dataList[key][1];
+ for(var i=0; i<varList2.length; i++)
+ dims += String(varList[ varList2[i] ][2]);
+
+ if (typeof isOnly2d !== 'undefined')
+ if (dims.indexOf('2')==-1) toAdd = false;
+ if (typeof isOnly3d !== 'undefined')
+ if (dims.indexOf('3')==-1) toAdd = false;
+ }
+
+ // add to the option group
+ if (toAdd) og.appendChild(new Option(key,key));
+ }
+ }
+}
+
+// change_datan_
+function change_datan(numTB){
+ var nVar=Number( document.getElementById("nVar").value );
+ //alert(nVar);
+
+ for (var i=0; i<nVar; i++) {
+ var str1 = data_block_str(String(i+2), numTB, "Source Variable "+String(i+1), "", 500);
+ //alert(str1);
+ document.getElementById("datan"+(i+1)).innerHTML = str1;
+ put_data(i+2);
+ put_var(i+2);
+ select_var(i+2);
+ }
+ for (var i=nVar; i<10; i++) {
+ document.getElementById("datan"+(i+1)).innerHTML = "";
+ }
+}
+
+// data_block_str__
+function data_block_str(ID, numTB, dataTitle, isRange, pressDf){
+var temp1= '';
+temp1 += '<div class="row ">\n'
+temp1 += '<div class="col-sm-12 center1 subtitle1">\n';
+temp1 += dataTitle + '\n';
+temp1 += '</div>\n';
+temp1 += '</div> <!-- row --> \n';
+
+temp1 += '<div class="row">\n';
+temp1 += ' <div class="col-sm-4 right1">\n';
+temp1 += ' source:' + '\n';
+temp1 += ' </div> <!-- col-sm-6 -->\n';
+temp1 += ' <div class="col-sm-8 left1">\n';
+temp1 += ' <select name="data' + ID + '", id="data' + ID;
+temp1 += '" onchange="put_var(' + ID + '); select_var(' + ID + ');time_range' + numTB + '()"></select>\n';
+temp1 += ' </div> <!-- col-sm-6 level2-->\n';
+temp1 += '</div> <!-- row -->\n';
+
+temp1 += '<div class="row">\n';
+temp1 += ' <div class="col-sm-4 right1">\n';
+temp1 += ' variable name:\n';
+temp1 += ' </div> <!-- col-sm-6 level2-->\n';
+temp1 += ' <div class="col-sm-8 left1">\n';
+temp1 += ' <select name="var' + ID +'", id="var' + ID;
+temp1 += '" onchange="select_var(' + ID + '); select_var(' + ID + '); time_range' + numTB + '()"> </select>\n';
+temp1 += ' </div> <!-- col-sm-6 level2-->\n';
+temp1 += '</div> <!-- row -->\n';
+
+temp1 += '<div class="row">\n';
+temp1 += ' <div class="col-sm-4 right1" id="pressureLabel' + ID + '">\n';
+//temp1 += ' pressure ' + isRange + '(atmosphere hPa) <br> or (ocean dbar):\n';
+temp1 += ' pressure ' + isRange +':\n';
+temp1 += ' </div> <!-- col-sm-6 level2-->\n';
+temp1 += ' <div class="col-sm-8 left1">\n';
+temp1 += ' <input id="pres' + ID + '" value="' + pressDf + '" alt="pressure"/>\n';
+temp1 += ' </div> <!-- col-sm-6 level2-->\n';
+temp1 += '</div> <!-- row -->\n';
+// alert(temp1);
+return temp1;
+}
+
+// put_var__
+function put_var(ID) {
+ var list1=document.getElementById("var"+ID);
+ for (var i=list1.length-1; i>=0; i--) {
+ list1.remove(i);
+ }
+
+ data_string = document.getElementById("data"+ID).value;
+ var varList2 = dataList[data_string][1];
+
+ if (typeof isOnly2d !== 'undefined') {
+ // list only 2D variables
+ for (var i=0; i<varList2.length; i++) {
+ var k = varList2[i];
+ if (varList[k][2]==2) list1.add(new Option(varList[k][0],k));
+ }
+
+ } else if (typeof isOnly3d !== 'undefined') {
+ // list only 3D variables
+ for (var i=0; i<varList2.length; i++) {
+ var k = varList2[i];
+ if (varList[k][2]==3) list1.add(new Option(varList[k][0],k));
+ }
+
+ } else {
+ // list all 2D/3D variables
+ for (var i=0; i<varList2.length; i++) {
+ var k = varList2[i];
+ list1.add(new Option(varList[k][0],k));
+ }
+ }
+
+ var nVar = list1.options.length;
+ if (nVar==0) {
+ alert(data_string + " has no suitable variable.");
+ document.getElementById("data"+ID).options[0].selected = true;
+ put_var(ID);
+ }
+}
+
+// is3D__
+function is3D(ID)
+{
+ var var_string = $("#var"+ID).val();
+ return varList[var_string][2]==3;
+}
+
+// select_var__
+function select_var(ID)
+{
+ // if there isOnly2d is defined, there is no pressure widget.
+ try {
+ //var var_string = $("#var"+ID).val();
+ //alert(is3D(ID));
+ if (is3D(ID)) {
+ enable_pres1(ID);
+ } else {
+ disable_pres1(ID);
+ }
+ }
+ catch(err) {}
+}
+
+
+// time_range__
+// this is identical to time_range1()
+function time_range() {
+ var var_string1 = $("#var"+1).val();
+ var data_string1 = $("#data"+1).val();
+
+ var sTime = dataList[data_string1][2][var_string1][0].toString();
+ var eTime = dataList[data_string1][2][var_string1][1].toString();
+
+ $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+ $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+function time_range1() {
+ var var_string1 = $("#var"+1).val();
+ var data_string1 = $("#data"+1).val();
+
+ var sTime = dataList[data_string1][2][var_string1][0].toString();
+ var eTime = dataList[data_string1][2][var_string1][1].toString();
+
+ $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+ $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+// time_range2__
+function time_range2() {
+ var var_string1 = $("#var"+1).val();
+ var var_string2 = $("#var"+2).val();
+ var data_string1 = $("#data"+1).val();
+ var data_string2 = $("#data"+2).val();
+ //alert(data_string1);
+ //alert(data_string2);
+
+
+ var sTime = Math.max( Number(dataList[data_string1][2][var_string1][0]),
+ Number(dataList[data_string2][2][var_string2][0]) ).toString();
+ var eTime = Math.min( Number(dataList[data_string1][2][var_string1][1]),
+ Number(dataList[data_string2][2][var_string2][1]) ).toString();
+
+ //sTime = sTime.toString();
+ //eTime = eTime.toString();
+
+ $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+ $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+// time_range3__
+function time_range3() {
+ var var_string1 = $("#var"+1).val();
+ var var_string2 = $("#var"+2).val();
+ var var_string3 = $("#var"+3).val();
+ var data_string1 = $("#data"+1).val();
+ var data_string2 = $("#data"+2).val();
+ var data_string3 = $("#data"+3).val();
+
+ var sTime = Math.max(
+ Number(dataList[data_string1][2][var_string1][0]),
+ Number(dataList[data_string2][2][var_string2][0]),
+ Number(dataList[data_string3][2][var_string3][0])
+ ).toString();
+ var eTime = Math.min(
+ Number(dataList[data_string1][2][var_string1][1]),
+ Number(dataList[data_string2][2][var_string2][1]),
+ Number(dataList[data_string3][2][var_string3][1])
+ ).toString();
+
+ $("#startYear").html("start year-month: (earliest:" + sTime.slice(0,4) + "-" + sTime.slice(4,6) + ")");
+ $("#endYear").html("end year-month: (latest:" + eTime.slice(0,4) + "-" + eTime.slice(4,6) + ")");
+}
+
+// monthList__
+var monthList = [
+"Jan",
+"Feb",
+"Mar",
+"Apr",
+"May",
+"Jun",
+"Jul",
+"Aug",
+"Sep",
+"Oct",
+"Nov",
+"Dec",
+];
+
+// fillMonth__
+function fillMonth() {
+ var temp1 = 'select months:\
+<select name="months" id="months" onchange="select_months()">\
+<option id="all">select all</option>\
+<option id="none">select none</option>\
+<option id="summer">Summer:Jun-Jul-Aug</option>\
+<option id="autum">Autumn:Sep-Oct-Nov</option>\
+<option id="winter">Winter:Dec-Jan-Feb</option>\
+<option id="spring">Spring:Mar-Apr-May</option> </select>';
+ $("#monthSelect0").html(temp1);
+
+ temp1 = '<form role="form">';
+ for (var i=0; i<monthList.length; i++) {
+ temp1 +=
+ '<label"><input type="checkbox" id="' + monthList[i] + '" value="' + monthList[i] + '" checked/></label>'
+ + monthList[i] + " ";
+ }
+ temp1 += '</form>';
+ $("#monthSelect").html(temp1);
+}
+
+// unselect all months in the checkboxes
+// reset_months__
+function reset_months()
+{
+ for (var i=0; i<monthList.length; i++) {
+ document.getElementById(monthList[i]).checked = false;
+ }
+}
+
+// see if no month is selected
+// no_month_check
+function no_month_check()
+{
+ var nonChecked = true;
+ for (var i=0; i<monthList.length; i++) {
+ if (document.getElementById(monthList[i]).checked == true) {
+ nonChecked = false;
+ }
+ }
+ return nonChecked;
+}
+
+// select all months in the checkboxes
+// select_all_months__
+function select_all_months()
+{
+ for (var i=0; i<monthList.length; i++) {
+ document.getElementById(monthList[i]).checked = true;
+ }
+}
+
+// select checkboxes based on "months" dropdown
+// select_months__
+function select_months()
+{
+ var s1=document.getElementById("months");
+ // alert(s1.selectedIndex);
+ // alert(s1.options[s1.selectedIndex].value);
+
+ // disable the download button because of this change
+ disable_download_button();
+
+ // "select none"
+ if (s1.selectedIndex == 1) {
+ reset_months();
+ }
+ // "select all"
+ if (s1.selectedIndex == 0) {
+ select_all_months();
+ }
+ // "summer"
+ if (s1.selectedIndex == 2) {
+ reset_months();
+ document.getElementById('Jun').checked = true;
+ document.getElementById('Jul').checked = true;
+ document.getElementById('Aug').checked = true;
+ }
+ // "autumn"
+ if (s1.selectedIndex == 3) {
+ reset_months();
+ document.getElementById('Sep').checked = true;
+ document.getElementById('Oct').checked = true;
+ document.getElementById('Nov').checked = true;
+ }
+ // "winter"
+ if (s1.selectedIndex == 4) {
+ reset_months();
+ document.getElementById('Dec').checked = true;
+ document.getElementById('Jan').checked = true;
+ document.getElementById('Feb').checked = true;
+ }
+ // "spring"
+ if (s1.selectedIndex == 5) {
+ reset_months();
+ document.getElementById('Mar').checked = true;
+ document.getElementById('Apr').checked = true;
+ document.getElementById('May').checked = true;
+ }
+
+}
+
+// getMonthStr__
+function getMonthStr() {
+ // get months checked by client
+ var month_str = "";
+ for (var i=0; i<monthList.length; i++) {
+ var mm = document.getElementById(monthList[i]);
+ if (mm.checked == true) {
+ month_str += ","+(i+1);
+ }
+ }
+ month_str = month_str.substr(1);
+ return month_str;
+}
+
+// parse_pres__
+function parse_pres(pres10) {
+ var pres1 = "";
+
+ if (pres10=="") {pres1 = naValue; }
+ else {
+ if (!(isNaN(Number(pres10)))) {
+ pres1 = pres10;
+ } else {
+ var checkNan = 0;
+ var pres2 = [];
+ var temp1=pres10.split(",");
+ //for (var i in temp1) {
+ for (var i=0; i<temp1.length; i++) {
+ if (isNaN(Number(temp1[i]))) {
+ checkNan = 1;
+ } else {
+ pres2.push(Number(temp1[i]));
+ }
+ }
+ if (pres2.length>0) { pres1 = pres2.join(); }
+ else { pres1 = naValue; }
+ }
+ }
+ return pres1;
+}
+
+// get_querystring__
+function get_querystring() {
+ var queries = {};
+ $.each(document.location.search.substr(1).split('&'),function(c,q){
+ var i = q.split('=');
+ queries[i[0].toString()] = i[1].toString();
+ });
+ return queries;
+}
+
+
+function showUrl(inputs) {
+ var v1q, v2, key0, key1;
+ try {
+ var temp1 = "";
+ //for (var i=0; i<inputs.length; i++) {
+ for (key0 in inputs) {
+ if (!inputs.hasOwnProperty(key0)) { continue; }
+ if ( key0 == "Image" || key0 == "data_url" ) { continue; }
+ key1 = inputs[key0];
+
+ try {
+ v1q = $("#"+key1).val();
+
+ if ( key0.slice(0,5) == "model" ) {
+ v1q = v1q.replace("/", "_");
+ }
+
+ if ( key0 == "startT" || key0 == "endT" ) {
+ v1q = v1q.replace("-", "");
+ }
+
+ if ( key0 == "purpose" ) {
+ v1q = escape(v1q);
+ }
+
+/*
+ if ( key0.slice(0,4) == "pres" ) {
+ if ( Number(v1q) == NaN ) {
+ v1q = "-999999";
+ }
+ }
+*/
+ temp1 += key0 + "=" + v1q + "&";
+ } catch(err) {}
+ }
+ temp1 = temp1.slice(0,-1);
+ } catch(err) {
+ var temp1 = document.location.search.substr(1);
+ }
+
+ document.getElementById("actionUrl").innerHTML = document.location.href.split('?')[0] + "?" + temp1;
+}
+
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/dataList2.js b/ApacheCMDA_Frontend_1.0/public/html/js2/dataList2.js
new file mode 100644
index 0000000..8cf0903
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/dataList2.js
@@ -0,0 +1,42 @@
+// modelName: [category, listOfVar],
+var groupList={
+"group1": ["Model: Historical"],
+"group2": ["Model: AMIP"],
+"group3": ["Observation"],
+"group4": ["Reanalysis"],
+};
+
+var dataList={
+"group1": ["Model: Historical"],
+"GFDL/ESM2G": ["Model: Historical", ["pr", "clt", "ts", "tos", "uas", "vas", "sfcWind", "zos", "lai", "rlds", "rlus", "rldscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'uas': [199101, 200512, '.'], 'rlutcs': [199601, 200512, '.'], 'rsutcs': [199101, 200512, '.'], 'rldscs': [199601, 200512, '.'], 'pr': [199601, 200512, '.'], 'rlut': [199601, 200512, '.'], 'cli': [199101, 200512, 'regridded'], 'rlus': [199601, 200512, '.'], 'tos': [199601, 200512, 'regridded'], 'ts': [199601, 200512, '.'], 'zos': [199601, 200512, 'regridded'], 'clt': [199601, 200512, '.'], 'vas': [199101, 200512, '.'], 'clw': [199101, 200512, 'regridded'], 'ta': [199101, 200512, '.'], 'lai': [199601, 200512, '.'], 'rsdt': [199101, 200512, '.'], 'hur': [198601, 200512, '.'], 'hus': [199101, 200512, '.'], 'sfcWind': [199101, 200512, '.'], 'rlds': [199601, 200512, '.'], 'wap': [198601, 200512, '.'], 'rsut': [199101, 200512, '.']} ],
+"GISS/E2-H": ["Model: Historical", ["pr", "clt", "ts", "tos", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [195101, 200512, '.'], 'rsuscs': [199501, 200512, '.'], 'rsdscs': [199501, 200512, '.'], 'rlutcs': [199501, 200512, '.'], 'rsutcs': [199501, 200512, '.'], 'rldscs': [199501, 200512, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200512, '.'], 'cli': [195101, 200512, 'regridded'], 'rlus': [199501, 200512, '.'], 'rlds': [199501, 200512, '.'], 'ts': [199501, 200512, '.'], 'clt': [199501, 200512, '.'], 'vas': [199501, 200512, '.'], 'clw': [195101, 200512, 'regridded'], 'uas': [199501, 200512, '.'], 'wap': [195101, 200512, '.'], 'rsdt': [199501, 200512, '.'], 'rsds': [199501, 200512, '.'], 'prw': [199501, 200512, '.'], 'sfcWind': [199501, 200512, '.'], 'tos': [199501, 200512, '.'], 'rsus': [199501, 200512, '.'], 'rsut': [199501, 200512, '.']} ],
+"GISS/E2-R": ["Model: Historical", ["pr", "clt", "ts", "tos", "uas", "vas", "sfcWind", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [195101, 200512, '.'], 'rsuscs': [199501, 200012, '.'], 'uas': [199501, 200512, '.'], 'rlutcs': [199501, 200012, '.'], 'rsutcs': [199501, 200012, '.'], 'rldscs': [199501, 200012, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200012, '.'], 'cli': [200101, 200512, 'regridded'], 'rlus': [199501, 200012, '.'], 'tos': [199501, 200512, '.'], 'ts': [199501, 200512, '.'], 'clt': [199501, 200512, '.'], 'vas': [199501, 200512, '.'], 'clw': [199501, 200512, 'regridded'], 'rsdscs': [199501, 200012, '.'], 'wap': [195101, 200512, '.'], 'rsdt': [199501, 200012, '.'], 'rsds': [199501, 200012, '.'], 'sfcWind': [199501, 200512, '.'], 'rsus': [199501, 200012, '.'], 'rsut': [199501, 200012, '.']} ],
+"NCAR/CAM5": ["Model: Historical", ["pr", "clt", "ts", "tos", "sfcWind", "zos", "lai", "rlds", "rsds", "rlus", "rsus", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [195001, 200512, '.'], 'rsuscs': [199501, 200512, '.'], 'rsdscs': [199501, 200512, '.'], 'rlutcs': [199501, 200512, '.'], 'rsutcs': [199501, 200512, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200512, '.'], 'cli': [197501, 200512, 'regridded'], 'rlus': [199501, 200512, '.'], 'tos': [199501, 200512, 'regridded'], 'ts': [199501, 200512, '.'], 'zos': [199501, 200512, 'regridded'], 'clt': [199501, 200512, '.'], 'clw': [197501, 200512, 'regridded'], 'wap': [195001, 200512, '.'], 'lai': [199501, 200512, '.'], 'rsdt': [199501, 200512, '.'], 'rsds': [199501, 200512, '.'], 'sfcWind': [199501, 200512, '.'], 'rlds': [199501, 200512, '.'], 'rsus': [199501, 200512, '.'], 'rsut': [199501, 200512, '.']} ],
+"NCC/NORESM": ["Model: Historical", ["pr", "clt", "ts", "tos", "uas", "vas", "zos", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'hur': [195001, 200512, '.'], 'rsuscs': [199501, 200512, '.'], 'rsdscs': [199501, 200512, '.'], 'rlutcs': [199501, 200512, '.'], 'rsutcs': [199501, 200512, '.'], 'rldscs': [199501, 200512, '.'], 'pr': [199501, 200512, '.'], 'rlut': [199501, 200512, '.'], 'cli': [195001, 200512, 'regridded'], 'rlus': [199501, 200512, '.'], 'tos': [199501, 200512, 'regridded'], 'ts': [199501, 200512, '.'], 'zos': [199501, 200512, 'regridded'], 'clt': [199501, 200512, '.'], 'vas': [199501, 200512, '.'], 'clw': [195001, 200512, 'regridded'], 'uas': [199501, 200512, '.'], 'wap': [195001, 200512, '.'], 'lai': [199501, 200512, '.'], 'rsdt': [199501, 200512, '.'], 'rsds': [199501, 200512, '.'], 'hus': [199501, 200512, '.'], 'rlds': [199501, 200512, '.'], 'rsus': [199501, 200512, '.'], 'ta': [199501, 200512, '.'], 'rsut': [199501, 200512, '.']} ],
+"UKMO/HadGEM2-ES":["Model: Historical", ["pr", "clt", "ts", "uas", "vas", "sfcWind", "zos", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'rsds': [199501, 200511, '.'], 'rsuscs': [199501, 200511, '.'], 'rsdscs': [199501, 200511, '.'], 'rlutcs': [199501, 200511, '.'], 'rsutcs': [199501, 200511, '.'], 'rldscs': [199501, 200511, '.'], 'pr': [199501, 200511, '.'], 'rlut': [199501, 200511, '.'], 'cli': [198412, 200511, 'regridded'], 'rlus': [199501, 200511, '.'], 'rlds': [199501, 200511, '.'], 'ts': [199501, 200511, '.'], 'zos': [195912, 200512, '.'], 'clt': [199501, 200511, '.'], 'vas': [199501, 200511, '.'], 'clw': [198412, 200511, 'regridded'], 'uas': [199501, 200511, '.'], 'wap': [198412, 200511, '.'], 'lai': [199501, 200511, '.'], 'rsdt': [199501, 200511, '.'], 'hur': [198412, 200511, '.'], 'sfcWind': [199501, 200511, '.'], 'rsus': [199501, 200511, '.'], 'rsut': [199501, 200511, '.']} ],
+// [
+"group2": ["Model: AMIP"],
+"CCCMA/CANAM4": ["Model: AMIP", ["pr", "clt", "ts", "tas", "hurs", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'va': [195001, 200912, '.'], 'ci': [195001, 200912, '.'], 'sci': [195001, 200912, '.'], 'rsds': [195001, 200912, '.'], 'prc': [195001, 200912, '.'], 'cl': [195001, 200912, '.'], 'rsuscs': [195001, 200912, '.'], 'uas': [195001, 200912, '.'], 'huss': [195001, 200912, '.'], 'hfss': [195001, 200912, '.'], 'rlutcs': [195001, 200912, '.'], 'evspsbl': [195001, 200912, '.'], 'prsn': [195001, 200912, '.'], 'rldscs': [195001, 200912, '.'], 'ccb': [195001, 200912, '.'], 'pr': [195001, 200912, '.'], 'ps': [195001, 200912, '.'], 'cli': [195001, 200912, 'regridded'], 'rlus': [195001, 200912, '.'], 'rlds': [195001, 200912, '.'], 'tas': [195001, 200912, '.'], 'ts': [195001, 200912, '.'], 'prw': [195001, 200912, '.'], 'clt': [195001, 200912, '.'], 'vas': [195001, 200912, '.'], 'clw': [195001, 200912, 'regridded'], 'rsdscs': [195001, 200912, '.'], 'wap': [195001, 200912, '.'], 'zg': [195001, 200912, '.'], 'tasmin': [195001, 200912, '.'], 'psl': [195001, 200912, '.'], 'rlut': [195001, 200912, '.'], 'hurs': [195001, 200912, '.'], 'rsdt': [195001, 200912, '.'], 'hur': [195001, 200912, '.'], 'hus': [195001, 200912, '.'], 'sbl': [195001, 200912, '.'], 'clivi': [195001, 200912, '.'], 'mc': [195001, 200912, '.'], 'rtmt': [195001, 200912, '.'], 'cct': [195001, 200912, '.'], 'rsutcs': [195001, 200912, '.'], 'sfcWind': [195001, 200912, '.'], 'tauv': [195001, 200912, '.'], 'clwvi': [195001, 200912, '.'], 'tauu': [195001, 200912, '.'], 'tasmax': [195001, 200912, '.'], 'rsus': [195001, 200912, '.'], 'ta': [195001, 200912, '.'], 'ua': [195001, 200912, '.'], 'rsut': [195001, 200912, '.'], 'hfls': [195001, 200912, '.']} ],
+"CSIRO/MK3.6": ["Model: AMIP", ["pr", "clt", "ts", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'rsds': [197901, 200912, '.'], 'rsuscs': [197901, 200912, '.'], 'uas': [197901, 200912, '.'], 'rlutcs': [197901, 200912, '.'], 'rsutcs': [197901, 200912, '.'], 'rldscs': [197901, 200912, '.'], 'pr': [199501, 200912, '.'], 'rlut': [197901, 200912, '.'], 'cli': [197901, 200912, 'regridded'], 'rlus': [197901, 200912, '.'], 'rlds': [197901, 200912, '.'], 'ts': [199501, 200912, '.'], 'clt': [199501, 200912, '.'], 'vas': [197901, 200912, '.'], 'clw': [199901, 200912, 'regridded'], 'rsdscs': [197901, 200912, '.'], 'wap': [199001, 200512, '.'], 'rsdt': [197901, 200912, '.'], 'hur': [199001, 200512, '.'], 'sfcWind': [197901, 200912, '.'], 'rsus': [197901, 200912, '.'], 'rsut': [197901, 200912, '.']} ],
+"GFDL/CM3": ["Model: AMIP", ["pr", "clt", "ts", "uas", "vas", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "ta", "hus", "cli", "clw", "wap", "hur", ], {'hur': [198901, 200812, '.'], 'rsuscs': [199401, 200812, '.'], 'rsdscs': [199401, 200812, '.'], 'rlutcs': [199401, 200812, '.'], 'rsutcs': [199401, 200812, '.'], 'rldscs': [199401, 200812, '.'], 'pr': [199401, 200812, '.'], 'rlut': [199401, 200812, '.'], 'cli': [197901, 200812, 'regridded'], 'rlus': [199401, 200812, '.'], 'rlds': [199401, 200812, '.'], 'ts': [199401, 200812, '.'], 'clt': [199401, 200812, '.'], 'vas': [199401, 200812, '.'], 'clw': [197901, 200812, 'regridded'], 'uas': [199401, 200812, '.'], 'wap': [198901, 200812, '.'], 'rsdt': [199401, 200812, '.'], 'rsds': [199401, 200812, '.'], 'hus': [198901, 200812, '.'], 'rsus': [199401, 200812, '.'], 'ta': [198901, 200812, '.'], 'rsut': [199401, 200812, '.']} ],
+"IPSL/CM5A-LR": ["Model: AMIP", ["pr", "clt", "ts", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", ], {'pr': [199501, 200912, '.'], 'rlut': [199501, 200912, '.'], 'rsus': [199501, 200912, '.'], 'cli': [197901, 200912, 'regridded'], 'rlus': [199501, 200912, '.'], 'rsdt': [199501, 200912, '.'], 'rlds': [199501, 200912, '.'], 'rsdscs': [199501, 200912, '.'], 'ts': [199501, 200912, '.'], 'rlutcs': [199501, 200912, '.'], 'rsutcs': [199501, 200912, '.'], 'rsds': [199501, 200912, '.'], 'rsuscs': [199501, 200912, '.'], 'clt': [199501, 200912, '.'], 'rldscs': [199501, 200912, '.'], 'clw': [197901, 200912, 'regridded'], 'rsut': [199501, 200912, '.'], 'lai': [199501, 200912, '.']} ],
+"MIROC/MIROC5": ["Model: AMIP", ["pr", "clt", "ts", "uas", "vas", "sfcWind", "lai", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [192001, 200912, '.'], 'rsuscs': [197901, 200812, '.'], 'rsdscs': [197901, 200812, '.'], 'rlutcs': [197901, 200812, '.'], 'rsutcs': [197901, 200812, '.'], 'rldscs': [197901, 200812, '.'], 'pr': [197901, 200812, '.'], 'rlut': [197901, 200812, '.'], 'cli': [197901, 199812, 'regridded'], 'rlus': [197901, 200812, '.'], 'rlds': [197901, 200812, '.'], 'ts': [197901, 200812, '.'], 'clt': [197901, 200812, '.'], 'vas': [197901, 200812, '.'], 'clw': [197901, 200812, 'regridded'], 'uas': [197901, 200812, '.'], 'wap': [192001, 200912, '.'], 'lai': [197901, 200812, '.'], 'rsdt': [197901, 200812, '.'], 'rsds': [197901, 200812, '.'], 'sfcWind': [197901, 200812, '.'], 'rsus': [197901, 200812, '.'], 'rsut': [197901, 200812, '.']} ],
+"UKMO/HadGEM2-A": ["Model: AMIP", ["pr", "clt", "ts", "hurs", "uas", "vas", "sfcWind", "rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", "cli", "clw", "wap", "hur", ], {'hur': [197809, 200812, '.'], 'rsuscs': [199501, 200812, '.'], 'rsdscs': [199501, 200812, '.'], 'rlutcs': [199501, 200812, '.'], 'rsutcs': [199501, 200812, '.'], 'rldscs': [199501, 200812, '.'], 'pr': [199501, 200812, '.'], 'rlut': [199501, 200812, '.'], 'cli': [197809, 200811, 'regridded'], 'rlus': [200812, 200812, '.'], 'rlds': [199501, 200812, '.'], 'ts': [199501, 200812, '.'], 'clt': [199501, 200812, '.'], 'vas': [199501, 200812, '.'], 'clw': [197809, 200811, 'regridded'], 'uas': [199501, 200812, '.'], 'wap': [197809, 200812, '.'], 'hurs': [197809, 200812, '.'], 'rsdt': [199501, 200812, '.'], 'rsds': [199501, 200812, '.'], 'sfcWind': [199501, 200812, '.'], 'rsus': [199501, 200812, '.'], 'rsut': [199501, 200812, '.']} ],
+// [
+"group3": ["Observation"],
+"NASA/GRACE": ["Observation", ["zl", "zo", ], {'zl': [200302, 201112, '.'], 'zo': [200302, 201112, '.']} ],
+"NASA/MODIS": ["Observation", ["clt", "lai", ], {'cltNobs': [200003, 201109, '.'], 'clt': [200003, 201109, '.'], 'lai': [200002, 200912, '.'], 'cltStddev': [200003, 201109, '.']} ],
+"NASA/AMSRE": ["Observation", ["tos", ], {'tosNobs': [200206, 201012, '.'], 'tos': [200206, 201012, '.'], 'tosStderr': [200206, 201012, '.']} ],
+"NASA/TRMM": ["Observation", ["pr", ], {'pr': [199801, 201312, '.']} ],
+"NASA/GPCP": ["Observation", ["pr", ], {'pr': [197901, 201106, '.']} ],
+"NASA/QuikSCAT": ["Observation", ["uas", "vas", "sfcWind", ], {'uasNobs': [199908, 200910, '.'], 'vasStderr': [199908, 200910, '.'], 'uas': [199908, 200910, '.'], 'uasStderr': [199908, 200910, '.'], 'vas': [199908, 200910, '.'], 'sfcWind': [199908, 200910, '.'], 'sfcWindStderr': [199908, 200910, '.'], 'sfcWindNobs': [199908, 200910, '.'], 'vasNobs': [199908, 200910, '.']} ],
+"NASA/AVISO": ["Observation", ["zos", ], {'zos': [199210, 201012, '.']} ],
+"NOAA/NODC": ["Observation", ["ohc700", "ohc2000", ], {'ohc2000': [200501, 201212, '.'], 'ohc700': [195501, 201212, '.']} ],
+"NASA/CERES": ["Observation", ["rlds", "rsds", "rlus", "rsus", "rldscs", "rsdscs", "rsuscs", "rsdt", "rlut", "rsut", "rlutcs", "rsutcs", ], {'rlut': [200003, 201206, '.'], 'rlus': [200003, 201002, '.'], 'rlutcs': [200003, 201206, '.'], 'rsuscs': [200003, 201002, '.'], 'rsdscs': [200003, 201002, '.'], 'rsdt': [200003, 201206, '.'], 'rsutcs': [200003, 201206, '.'], 'rsds': [200003, 201002, '.'], 'rlds': [200003, 201002, '.'], 'rldscs': [200003, 201002, '.'], 'rsus': [200003, 201002, '.'], 'rsut': [200003, 201206, '.']} ],
+"NASA/AIRS": ["Observation", ["tas", "ta", "hus", ], {'hus': [200209, 201105, '.'], 'tas': [200209, 201112, '.'], 'ta': [200209, 201105, '.']} ],
+"NASA/MLS": ["Observation", ["ta", "hus", ], {'hus': [200408, 201012, '.'], 'ta': [200408, 201012, '.']} ],
+"ARGO/ARGO": ["Observation", ["ot", "os", ], {'ot': [200101, 201305, '.'], 'os': [200101, 201305, '.']} ],
+//
+"group4": ["Reanalysis"],
+"ECMWF/interim": ["Reanalysis", ["clt", "tos", "uas", "vas", "sfcWind", "wap", "hur", ], {'tos': [197901, 201402, '.'], 'uas': [199501, 201412, '.'], 'sfcWind': [199501, 201412, '.'], 'hur': [197901, 201402, '.'], 'clt': [197901, 201404, '.'], 'vas': [199501, 201412, '.'], 'wap': [197901, 201402, '.']} ],
+};
diff --git a/ApacheCMDA_Frontend_1.0/public/html/js2/varList.js b/ApacheCMDA_Frontend_1.0/public/html/js2/varList.js
new file mode 100644
index 0000000..51cb5ad
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/js2/varList.js
@@ -0,0 +1,40 @@
+// shortName: [longName, groupName, dimension, units],
+var varList = {
+"pr": ["Precipitation Flux", "", 2, "kg m-2 s-1"],
+"clt": ["Total Cloud Fraction", "", 2, "%"],
+"ts": ["Surface Temperature", "", 2, "K"],
+"lst_day": ["Daytime Land Surface Temperature", "", 2, "K"],
+"lst_night":["Nighttime Land Surface Temperature", "", 2, "K"],
+"tas": ["Near-Surface Air Temperature", "", 2, "K"],
+"hurs": ["Near-Surface Relative Humidity", "", 2, "%"],
+"tos": ["Sea Surface Temperature", "", 2, "K"],
+"uas": ["Eastward Near-Surface Wind", "", 2, "m s-1"],
+"vas": ["Northward Near-Surface Wind", "", 2, "m s-1"],
+"sfcWind": ["Near-Surface Wind Speed", "", 2, "m s-1"],
+"zos": ["Sea Surface Height", "", 2, "m"],
+"lai": ["Leaf Area Index", "", 2, "1"],
+"zl": ["Equivalent Water Height Over Land", "", 2, "cm"],
+"zo": ["Equivalent Water Height Over Ocean", "", 2, "cm"],
+"ohc700": ["Ocean Heat Content Anomaly within 700 m Depth", "", 2, "1e18 joules"],
+"ohc2000": ["Ocean Heat Content Anomaly within 2000 m Depth", "", 2, "1e18 joules"],
+"rlds": ["Surface Downwelling Longwave Radiation", "", 2, "W m-2"],
+"rsds": ["Surface Downwelling Shortwave Radiation", "", 2, "W m-2"],
+"rlus": ["Surface Upwelling Longwave Radiation", "", 2, "W m-2"],
+"rsus": ["Surface Upwelling Shortwave Radiation", "", 2, "W m-2"],
+"rldscs": ["Surface Downwelling Clear-Sky Longwave Radiation", "", 2, "W m-2"],
+"rsdscs": ["Surface Downwelling Clear-Sky Shortwave Radiation", "", 2, "W m-2"],
+"rsuscs": ["Surface Upwelling Clear-Sky Shortwave Radiation", "", 2, "W m-2"],
+"rsdt": ["TOA Incident Shortwave Radiation", "", 2, "W m-2"],
+"rlut": ["TOA Outgoing Longwave Radiation", "", 2, "W m-2"],
+"rsut": ["TOA Outgoing Shortwave Radiation", "", 2, "W m-2"],
+"rlutcs": ["TOA Outgoing Clear-Sky Longwave Radiation", "", 2, "W m-2"],
+"rsutcs": ["TOA Outgoing Clear-Sky Shortwave Radiation", "", 2, "W m-2"],
+"ta": ["Air Temperature", "", 3, "K"],
+"hus": ["Specific Humidity", "", 3, "1"],
+"cli": ["Cloud Ice Water Content", "", 3, "1"],
+"clw": ["Cloud Liquid Water Content", "", 3, "1"],
+"ot": ["Ocean Temperature", "ocean", 3, "K"],
+"os": ["Ocean Salinity", "ocean", 3, "psu"],
+"wap": ["Vertical Wind Velocity", "", 3, "m s-1"],
+"hur": ["Relative Humidity", "", 3, "%"],
+};
diff --git a/ApacheCMDA_Frontend_1.0/public/html/serviceScatterPlot2Vars.html b/ApacheCMDA_Frontend_1.0/public/html/serviceScatterPlot2Vars.html
new file mode 100644
index 0000000..6e96fd2
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/html/serviceScatterPlot2Vars.html
@@ -0,0 +1,672 @@
+<!DOCTYPE html>
+
+
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+
+ <!-- for Bootstrap -->
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
+ <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
+
+ <!-- still needed? -->
+<!--
+ <script src="js/jquery.flot.min.js"></script>
+ <script src="js/json2.js"></script>
+ <script src="js/xmisc.js"></script>
+-->
+
+ <!-- cmac related -->
+ <link rel="stylesheet" href="js2/common.css">
+ <script src='js2/dataList2.js'></script>
+ <script src='js2/varList.js'></script>
+ <script src='js2/common.js'></script>
+
+ <title>Scatter and Histogram Plot</title>
+
+ <script>
+
+ var Response = null;
+ var variable1 = "";
+ var variable2 = "";
+
+ var naValue = "-999999";
+ var defaultDataIndex = 0; // not used yet
+
+
+ var isPressure1 = true;
+
+
+
+ var pressDf1 = "500";
+
+
+
+
+
+
+
+
+ var inputs = {
+'model1':'data1',
+'var1':'var1',
+'pres1':'pres1',
+'model2':'data2',
+'var2':'var2',
+'pres2':'pres2',
+'lon1':'lon0',
+'lon2':'lon1',
+'lat1':'lat0',
+'lat2':'lat1',
+'nSample':'nSample',
+'startT':'t0',
+'endT':'t1',
+'purpose':'purpose',
+'Image':'Image',
+'data_url':'data_url',
+};
+
+
+ // called on load or reload
+ window.onload = function() {
+
+ put_data(1);
+ put_data(2);
+ put_var(1);
+ put_var(2);
+ select_var(1);
+ select_var(2);
+ time_range2();
+ //fillMonth();
+
+ disable_download_button();
+
+
+
+ // parse querystring
+
+ queries = get_querystring();
+ //for (var key in queries) { if (queries.hasOwnProperty(key)) { alert(key + " -> " + queries[key]); } }
+
+ var key0, key1, v1q;
+
+ for (key0 in queries) {
+ if (!queries.hasOwnProperty(key0)) { continue; }
+ if (!inputs.hasOwnProperty(key0)) { continue; }
+ key1 = inputs[key0];
+ v1q = queries[key0];
+ v1q = v1q.trim();
+
+ if ( key0.slice(0,5) == "model" ) {
+ v1q = v1q.replace("_", "/");
+ }
+
+ if ( key0 == "startT" || key0 == "endT" ) {
+ v1q = v1q.slice(0,4) + "-" + v1q.slice(4,6);
+ }
+
+ if ( key0 == "purpose" ) {
+ v1q = unescape(v1q);
+ }
+
+ try {
+ if ( key1 == "Image" ) {
+ $("#"+key1).html( "<img src='" + v1q + "' width='820'/>" );
+ } else if ( key1 == "data_url" ) {
+ $("#"+key1).val( v1q );
+ enable_download_button();
+ } else {
+ $("#"+key1).val(v1q);
+ $("#"+key1).change();
+ }
+ } catch(err) {}
+ }
+
+/*
+ key1 = "var1";
+ if (queries.hasOwnProperty(key1)) {
+ v1q = queries[key1];
+ v1q = v1q.trim();
+ $("#var1").val(v1q);
+ $("#var1").change();
+ }
+
+ key1 = "pres1";
+ if (queries.hasOwnProperty(key1)) {
+ v1q = queries[key1];
+ v1q = v1q.trim();
+ $("#pres1").val(v1q);
+ }
+*/
+
+ }
+
+ $(document).ready(function(){
+
+
+ $("a").click(function(event){
+ alert("As you can see, the link no longer took you to jquery.com");
+ event.preventDefault();
+ });
+
+
+ $("#download_data").click(function(event) {
+ var durl = $("#data_url").val();
+ // alert(durl);
+ window.location.assign(durl);
+ });
+
+
+ $("#action1").click(function(event) {
+ showUrl(inputs);
+ Response = null;
+
+ // no data to download yet
+ disable_download_button();
+
+ $("#Response").html("Calculating ...");
+ $("#data_url").html("Calculating ...");
+ $("#Image").html("");
+
+ // sample url: http://cmacws.jpl.nasa.gov:8090/svc/scatterPlot2V?model1=ukmo_hadgem2-a&var1=ts&pres1=200&model2=ukmo_hadgem2-a&var2=clt&pres2=200&start_time=199001&end_time=199512&lon1=0&lon2=100&lat1=-29&lat2=29
+ // form url string
+ // var url = "http://cmacws.jpl.nasa.gov:8090/svc/scatterPlot2V?";
+ var url = "http://" + window.location.hostname + ":9002/svc/newScatterPlot2V?";
+ // alert("url: " + url);
+
+ var d1 = $("#data1").val();
+ var model1 = d1.replace("/", "_");
+ model1 = model1.toLowerCase();
+
+ var arglist = "";
+ arglist = arglist.concat("model1=");
+ arglist = arglist.concat(model1);
+
+ // alert("arglist: " + arglist);
+
+ var variable1 = $("#var1").val();
+ arglist = arglist.concat("&var1=");
+ arglist = arglist.concat(variable1);
+
+ // alert("arglist: " + arglist);
+
+ var pres1 = $("#pres1").val();
+ arglist = arglist.concat("&pres1=");
+ arglist = arglist.concat(pres1);
+
+ var d2 = $("#data2").val();
+ var model2 = d2.replace("/", "_");
+ model2 = model2.toLowerCase();
+
+ arglist = arglist.concat("&model2=");
+ arglist = arglist.concat(model2);
+
+ // alert("arglist: " + arglist);
+
+ var variable2 = $("#var2").val();
+ arglist = arglist.concat("&var2=");
+ arglist = arglist.concat(variable2);
+
+ // alert("arglist: " + arglist);
+
+ var pres2 = $("#pres2").val();
+ arglist = arglist.concat("&pres2=");
+ arglist = arglist.concat(pres2);
+
+ var t0 = $("#t0").val();
+ var t1 = $("#t1").val();
+
+ t0 = t0.replace("-", "");
+ t1 = t1.replace("-", "");
+
+ arglist = arglist.concat("&start_time=");
+ arglist = arglist.concat(t0);
+
+ arglist = arglist.concat("&end_time=");
+ arglist = arglist.concat(t1);
+
+ // alert("arglist: " + arglist);
+
+ var lon0 = $("#lon0").val();
+ var lon1 = $("#lon1").val();
+ var lat0 = $("#lat0").val();
+ var lat1 = $("#lat1").val();
+
+ arglist = arglist.concat("&lon1=");
+ arglist = arglist.concat(lon0);
+
+ arglist = arglist.concat("&lon2=");
+ arglist = arglist.concat(lon1);
+
+ arglist = arglist.concat("&lat1=");
+ arglist = arglist.concat(lat0);
+
+ arglist = arglist.concat("&lat2=");
+ arglist = arglist.concat(lat1);
+
+ var nSample = $("#nSample").val();
+ arglist = arglist.concat("&nSample=");
+ arglist = arglist.concat(nSample);
+
+ var purpose = $("#purpose").val();
+ arglist = arglist.concat("&purpose=");
+ arglist = arglist.concat(purpose);
+
+ // alert("arglist: " + arglist);
+
+ // url = url + encodeURIComponent(arglist);
+ url = url + encodeURI(arglist);
+ // url = url + arglist;
+ // alert("url: " + url);
+
+ var urlTimeBounds = "http://" + window.location.hostname + ":9002/svc/two_time_bounds?";
+ var arglistTB = "";
+ arglistTB = arglistTB.concat("serviceType=");
+ arglistTB = arglistTB.concat("2");
+ arglistTB = arglistTB.concat("&source1=");
+ arglistTB = arglistTB.concat(d1);
+ arglistTB = arglistTB.concat("&var1=");
+ arglistTB = arglistTB.concat(variable1);
+ arglistTB = arglistTB.concat("&source2=");
+ arglistTB = arglistTB.concat(d2);
+ arglistTB = arglistTB.concat("&var2=");
+ arglistTB = arglistTB.concat(variable2);
+ urlTimeBounds = urlTimeBounds + encodeURI(arglistTB);
+ // alert("urlTimeBounds: " + urlTimeBounds);
+
+ $.ajax({
+ type: "GET",
+ url: urlTimeBounds,
+ dataType: "json",
+ data: null,
+ success: function(data, textStatus, xhr) {
+ Response = data;
+ // alert("data: " + data);
+ if (data.success == false) {
+ // alert(data.error);
+ Response = null;
+ var text = JSON.stringify(data, null, 4);
+ text = "Error in backend: <br>" + text;
+ $("#Response").html(text);
+ $("#data_url").html(text);
+ return;
+ }
+ var text = JSON.stringify(data, null, 4);
+ // alert("text: " + text);
+ // $("#Response").html("<pre>"+text+"</pre>");
+ // $("#Response").html(text);
+
+ var tb1 = data.time_bounds1;
+ var bds1 = String(tb1).split(",");
+ // alert("tb1: " + tb1);
+ // alert("bds1: " + bds1);
+ var lowerT1 = parseInt(bds1[0]);
+ // alert("inside ajax, lowerT1: " + lowerT1);
+ var upperT1 = parseInt(bds1[1]);
+ // alert("upperT1: " + upperT1);
+
+ var tb2 = data.time_bounds2;
+ var bds2 = String(tb2).split(",");
+ // alert("tb2: " + tb2);
+ // alert("bds2: " + bds2);
+ var lowerT2 = parseInt(bds2[0]);
+ // alert("inside ajax, lowerT2: " + lowerT2);
+ var upperT2 = parseInt(bds2[1]);
+ // alert("upperT2: " + upperT2);
+
+ var t0I = parseInt(t0);
+ var t1I = parseInt(t1);
+ // alert("t0: " + t0I);
+ // alert("t1: " + t1I);
+
+ var lowerT, upperT;
+ // compute the intersection of the two data bounds
+ if (lowerT1 == 0 || upperT1 == 0){ // no data-1
+ alert("We do not have data for the data-1 source and variable configuration.");
+ return;
+ }
+ else if (lowerT2 == 0 || upperT2 == 0){ // no data-2
+ alert("We do not have data for the data-2 source and variable configuration.");
+ return;
+ }
+ else if (lowerT2 > upperT1 || lowerT1 > upperT2) { // no intersection
+ alert("The two data sets/vars do not have a common time range.");
+ return;
+ }
+ else { // compute intersection
+ if (lowerT1 > lowerT2) { // pick bigger lower time bound
+ lowerT = lowerT1;
+ }
+ else {
+ lowerT = lowerT2;
+ }
+
+ if (upperT1 > upperT2) { // pick smaller upper time bound
+ upperT = upperT2;
+ }
+ else {
+ upperT = upperT1;
+ }
+ }
+ // alert("lowerT: " + lowerT);
+ // alert("upperT: " + upperT);
+
+ if (t0I < lowerT && t1I < lowerT ||
+ t0I > upperT && t1I > upperT) {
+ alert("We do not have data that span your time range. Try the range inside ["+lowerT+", "+upperT+"].");
+ return;
+ }
+
+ if (t0I < lowerT && t1I <= upperT) {
+ alert("Your start year-month is out of bound. It has to be in or later than " + lowerT +
+ ". We will use the range ["+lowerT+", "+t1I+"] for you.");
+ }
+
+ if (t1I > upperT && t0I >= lowerT) {
+ alert("Your end year-month is out of bound. It has to be in or earlier than " + upperT +
+ ". We will use the range ["+t0I+", "+upperT+"] for you.");
+ }
+
+ if (t0I < lowerT && t1I > upperT ) {
+ alert("Both of your start and end year-months are out of bounds. They have to be in or earlier than " + upperT +
+ ", and in or later than " + lowerT + ". We will use the range ["+lowerT+", "+upperT+"] for you.");
+ }
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ $("#Response").html("error!");
+ $("#data_url").html("error!");
+ // alert("xhr.status: "+xhr.status);
+ // alert("error status: "+textStatus);
+ },
+ complete: function(xhr, textStatus) {
+ //alert("complete status: "+textStatus);
+ },
+ });
+
+
+ $.ajax({
+ type: "GET",
+ url: url,
+ dataType: "json",
+ data: null,
+ success: function(data, textStatus, xhr) {
+ Response = data;
+ // alert("data: " + data);
+ if (data.success == false) {
+ // alert(data.error);
+ Response = null;
+ var text = JSON.stringify(data, null, 4);
+
+ if (text.indexOf("No Data") != -1) {
+ $("#Image").html("No Data");
+ $("#Response").html("No Data");
+ $("#data_url").html("No Data");
+ return;
+ }
+
+ text = "Error in backend: <br>" + text;
+ // $("#Response").html("<span style='color:red'>" + text + "</span>");
+ $("#Response").html(text);
+ $("#data_url").html(text);
+
+ return;
+ }
+ var text = JSON.stringify(data, null, 4);
+ // alert(text);
+ // $("#Response").html("<pre>"+text+"</pre>");
+ $("#Response").html(text);
+
+ var html = "<img src='"+data.url+"' width='820'/>";
+ // alert(html);
+ $("#Image").html(html);
+
+ // post dataUrl to textarea and enable download button
+ $("#data_url").html(data.dataUrl);
+ enable_download_button();
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ $("#Response").html("error!");
+ $("#data_url").html("error!");
+ // alert("xhr.status: "+xhr.status);
+ // alert("error status: "+textStatus);
+ },
+ complete: function(xhr, textStatus) {
+ //alert("complete status: "+textStatus);
+ },
+ });
+
+ });
+
+ });
+
+
+
+ </script>
+
+</head>
+
+<body>
+<div class="container-fluid">
+<div class="row center1">
+<div class="col-sm-8 col-sm-offset-2 col-xs-12 color-head">
+<h3>Service: Scatter and Histogram Plot of Two Variables</h3>
+This service generates a scatter plot between two specified variables and the histograms of the two variables, and calculates the correlation of the two variables. The two variables can be either a two-dimensional variable or a slice of a three-dimensional variable at a specific pressure level. The number of samples used for this analysis should be specified.
+</div> <!-- col-sm -->
+<div class="col-sm-offset-2">
+</div> <!-- col-sm -->
+</div> <!-- row center1 -->
+
+
+
+<div class="color0">
+<div class="row ">
+<div class="col-sm-12 center1 subtitle1">
+Variable 1
+</div>
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ model:
+ </div> <!-- col-sm-6 -->
+ <div class="col-sm-8 left1">
+ <select name="data1" id="data1" onchange="put_var(1); select_var(1); time_range2()"></select>;
+ </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ variable:
+ </div> <!-- col-sm-6 -->
+ <div class="col-sm-8 left1">
+ <select name="var1" id="var1" onchange="select_var(1); time_range2()"></select>;
+ </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+
+<div class="row">
+ <div class="col-sm-4 right1" id="pressureLabel1" >
+ pressure:
+ </div> <!-- col-sm-6 level2-->
+ <div class="col-sm-8 left1">
+ <input id="pres1" value="500" alt="pressure" size=7>
+ </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+
+</div> <!-- color0 -->
+
+
+
+
+
+
+<div class="color1">
+<div class="row ">
+<div class="col-sm-12 center1 subtitle1">
+Variable 2
+</div>
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ model:
+ </div> <!-- col-sm-6 -->
+ <div class="col-sm-8 left1">
+ <select name="data2" id="data2" onchange="put_var(2); select_var(2); time_range2()"></select>;
+ </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ variable:
+ </div> <!-- col-sm-6 -->
+ <div class="col-sm-8 left1">
+ <select name="var2" id="var2" onchange="select_var(2); time_range2()"></select>;
+ </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+
+<div class="row">
+ <div class="col-sm-4 right1" id="pressureLabel2" >
+ pressure:
+ </div> <!-- col-sm-6 level2-->
+ <div class="col-sm-8 left1">
+ <input id="pres2" value="500" alt="pressure" size=7>
+ </div> <!-- col-sm-6 level2-->
+</div> <!-- row -->
+
+
+</div> <!-- color1 -->
+
+
+
+<div class="color3">
+<div class="row center1 subtitle1" >
+Data Subsetting
+</div> <!-- row -->
+
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ <div id=startYear>start year-month:</div>
+ </div>
+ <div class="col-sm-2 left1">
+ <input id="t0" value="2004-01" alt="start" size=7>
+ </div>
+
+ <div class="col-sm-4 right1">
+ <div id=endYear>end year-month:</div>
+ </div>
+ <div class="col-sm-2 left1">
+ <input id="t1" value="2004-12" alt="end" size=7>
+ </div>
+</div> <!-- row -->
+
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ start lon (deg):
+ </div>
+ <div class="col-sm-2 left1">
+ <input id="lon0" value="0" size=7>
+ </div>
+ <div class="col-sm-4 right1">
+ end lon (deg):
+ </div>
+ <div class="col-sm-2 left1">
+ <input id="lon1" value="360" size=7>
+ </div>
+</div> <!-- row -->
+
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ start lat (deg):
+ </div>
+ <div class="col-sm-2 left1">
+ <input id="lat0" value="-90" size=7>
+ </div>
+ <div class="col-sm-4 right1">
+ end lat (deg):
+ </div>
+ <div class="col-sm-2 left1">
+ <input id="lat1" value="90" size=7>
+ </div>
+</div> <!-- row -->
+
+<div class="row">
+ <div class="col-sm-4 right1">
+ number of samples:
+ </div>
+ <div class="col-sm-8 left1">
+ <input id="nSample" value="500" size=7>
+ </div>
+</div>
+
+</div> <!-- color3 -->
+
+
+
+
+
+
+<div class="color2">
+<div class="row">
+ <div class="col-sm-4 right1">
+ Execution purpose:
+ </div> <!-- col -->
+ <div class="col-sm-8 left1">
+ <form>
+ <textarea name="purpose" id="purpose" rows="4" cols="50"> </textarea>
+ </form>
+ </div> <!-- col -->
+</div> <!-- row -->
+</div> <!-- color2 -->
+
+<div class="color4">
+<div class="row">
+ <div class="col-sm-4 center1">
+ <input id="action1" type="submit" value="Get Plot" style="height:28px"/>
+ </div>
+ <div class="col-sm-4 center1">
+ <button id="showUrl1" onclick="showUrl(inputs)">Show Service URL</button>
+ </div>
+ <div class="col-sm-4 center1">
+ <form>
+ <input id="download_data" type="button" value="Download Data" style="height:28px"/>
+ </form>
+ </div>
+</div> <!-- row -->
+
+</div> <!-- color4 -->
+
+<div class="row" >
+ <div class="col-sm-12 center1">
+ <textarea readonly id="actionUrl" >Service URL Here</textarea>
+ </div>
+</div> <!-- row -->
+
+<div class="row" center1>
+ <div class="col-sm-12 center1">
+ <div id="Image">Image Here</div>
+ </div>
+</div> <!-- row -->
+
+<div class="row" >
+ <div class="col-sm-12 center1">
+ <textarea id="data_url" cols="150" rows="1">Data URL Here</textarea>
+ </div>
+</div> <!-- row -->
+
+<div class="row" center1>
+ <div class="col-sm-12 center1">
+ <textarea id="Response" cols="150" rows="6">Service Response Text Here</textarea>
+ </div>
+</div> <!-- row -->
+
+</div> <!-- container -->
+</body>
+</html>
diff --git a/ApacheCMDA_Frontend_1.0/public/images/giphy.gif b/ApacheCMDA_Frontend_1.0/public/images/giphy.gif
new file mode 100644
index 0000000..f5dabf5
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/images/giphy.gif
Binary files differ
diff --git a/ApacheCMDA_Frontend_1.0/public/javascripts/exampleUtil.js b/ApacheCMDA_Frontend_1.0/public/javascripts/exampleUtil.js
new file mode 100644
index 0000000..aeeb2df
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/javascripts/exampleUtil.js
@@ -0,0 +1,126 @@
+/**
+ * Created by Alex on 5/20/2015.
+ */
+
+function loadJSON(path, success, error) {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
+ success(JSON.parse(xhr.responseText));
+ }
+ else {
+ error(xhr);
+ }
+ }
+ };
+ xhr.open('GET', path, true);
+ xhr.send();
+}
+
+
+function getScaleFreeNetwork(nodeCount) {
+ var nodes = [];
+ var edges = [];
+ var connectionCount = [];
+
+ // randomly create some nodes and edges
+ for (var i = 0; i < nodeCount; i++) {
+ nodes.push({
+ id: i,
+ label: String(i)
+ });
+
+ connectionCount[i] = 0;
+
+ // create edges in a scale-free-network way
+ if (i == 1) {
+ var from = i;
+ var to = 0;
+ edges.push({
+ from: from,
+ to: to
+ });
+ connectionCount[from]++;
+ connectionCount[to]++;
+ }
+ else if (i > 1) {
+ var conn = edges.length * 2;
+ var rand = Math.floor(Math.random() * conn);
+ var cum = 0;
+ var j = 0;
+ while (j < connectionCount.length && cum < rand) {
+ cum += connectionCount[j];
+ j++;
+ }
+
+
+ var from = i;
+ var to = j;
+ edges.push({
+ from: from,
+ to: to
+ });
+ connectionCount[from]++;
+ connectionCount[to]++;
+ }
+ }
+
+ return {nodes:nodes, edges:edges};
+}
+
+var randomSeed = 764; // Math.round(Math.random()*1000);
+function seededRandom() {
+ var x = Math.sin(randomSeed++) * 10000;
+ return x - Math.floor(x);
+}
+
+function getScaleFreeNetworkSeeded(nodeCount) {
+ var nodes = [];
+ var edges = [];
+ var connectionCount = [];
+
+ // randomly create some nodes and edges
+ for (var i = 0; i < nodeCount; i++) {
+ nodes.push({
+ id: i,
+ label: String(i)
+ });
+
+ connectionCount[i] = 0;
+
+ // create edges in a scale-free-network way
+ if (i == 1) {
+ var from = i;
+ var to = 0;
+ edges.push({
+ from: from,
+ to: to
+ });
+ connectionCount[from]++;
+ connectionCount[to]++;
+ }
+ else if (i > 1) {
+ var conn = edges.length * 2;
+ var rand = Math.floor(seededRandom() * conn);
+ var cum = 0;
+ var j = 0;
+ while (j < connectionCount.length && cum < rand) {
+ cum += connectionCount[j];
+ j++;
+ }
+
+
+ var from = i;
+ var to = j;
+ edges.push({
+ from: from,
+ to: to
+ });
+ connectionCount[from]++;
+ connectionCount[to]++;
+ }
+ }
+
+ return {nodes:nodes, edges:edges};
+}
\ No newline at end of file
diff --git a/ApacheCMDA_Frontend_1.0/public/javascripts/googleAnalytics.js b/ApacheCMDA_Frontend_1.0/public/javascripts/googleAnalytics.js
new file mode 100644
index 0000000..3bdfc85
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/javascripts/googleAnalytics.js
@@ -0,0 +1,12 @@
+(function (i, s, o, g, r, a, m) {
+ i['GoogleAnalyticsObject'] = r;
+ i[r] = i[r] || function () {
+ (i[r].q = i[r].q || []).push(arguments)
+ }, i[r].l = 1 * new Date();
+ a = s.createElement(o), m = s.getElementsByTagName(o)[0];
+ a.async = 1;
+ a.src = g;
+ m.parentNode.insertBefore(a, m)
+})(window, document, 'script', 'http://www.google-analytics.com/analytics.js', 'ga');
+ga('create', 'UA-61231638-1', 'auto');
+ga('send', 'pageview');
\ No newline at end of file
diff --git a/ApacheCMDA_Frontend_1.0/public/javascripts/vis.js b/ApacheCMDA_Frontend_1.0/public/javascripts/vis.js
new file mode 100644
index 0000000..3388fc4
--- /dev/null
+++ b/ApacheCMDA_Frontend_1.0/public/javascripts/vis.js
@@ -0,0 +1,43387 @@
+/**
+ * vis.js
+ * https://github.com/almende/vis
+ *
+ * A dynamic, browser-based visualization library.
+ *
+ * @version 4.9.0
+ * @date 2015-10-01
+ *
+ * @license
+ * Copyright (C) 2011-2015 Almende B.V, http://almende.com
+ *
+ * Vis.js is dual licensed under both
+ *
+ * * The Apache 2.0 License
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * and
+ *
+ * * The MIT License
+ * http://opensource.org/licenses/MIT
+ *
+ * Vis.js may be distributed under either license.
+ */
+
+"use strict";
+
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["vis"] = factory();
+ else
+ root["vis"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // utils
+ 'use strict';
+
+ exports.util = __webpack_require__(1);
+ exports.DOMutil = __webpack_require__(7);
+
+ // data
+ exports.DataSet = __webpack_require__(8);
+ exports.DataView = __webpack_require__(10);
+ exports.Queue = __webpack_require__(9);
+
+ // Graph3d
+ exports.Graph3d = __webpack_require__(11);
+ exports.graph3d = {
+ Camera: __webpack_require__(15),
+ Filter: __webpack_require__(16),
+ Point2d: __webpack_require__(14),
+ Point3d: __webpack_require__(13),
+ Slider: __webpack_require__(17),
+ StepNumber: __webpack_require__(18)
+ };
+
+ // Timeline
+ exports.Timeline = __webpack_require__(19);
+ exports.Graph2d = __webpack_require__(49);
+ exports.timeline = {
+ DateUtil: __webpack_require__(27),
+ DataStep: __webpack_require__(52),
+ Range: __webpack_require__(24),
+ stack: __webpack_require__(32),
+ TimeStep: __webpack_require__(30),
+
+ components: {
+ items: {
+ Item: __webpack_require__(34),
+ BackgroundItem: __webpack_require__(38),
+ BoxItem: __webpack_require__(36),
+ PointItem: __webpack_require__(37),
+ RangeItem: __webpack_require__(33)
+ },
+
+ Component: __webpack_require__(26),
+ CurrentTime: __webpack_require__(44),
+ CustomTime: __webpack_require__(42),
+ DataAxis: __webpack_require__(51),
+ GraphGroup: __webpack_require__(53),
+ Group: __webpack_require__(31),
+ BackgroundGroup: __webpack_require__(35),
+ ItemSet: __webpack_require__(29),
+ Legend: __webpack_require__(57),
+ LineGraph: __webpack_require__(50),
+ TimeAxis: __webpack_require__(39)
+ }
+ };
+
+ // Network
+ exports.Network = __webpack_require__(59);
+ exports.network = {
+ Images: __webpack_require__(117),
+ dotparser: __webpack_require__(115),
+ gephiParser: __webpack_require__(116),
+ allOptions: __webpack_require__(111)
+ };
+ exports.network.convertDot = function (input) {
+ return exports.network.dotparser.DOTToGraph(input);
+ };
+ exports.network.convertGephi = function (input, options) {
+ return exports.network.gephiParser.parseGephi(input, options);
+ };
+
+ // bundled external libraries
+ exports.moment = __webpack_require__(2);
+ exports.Hammer = __webpack_require__(20);
+ exports.keycharm = __webpack_require__(41);
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // utility functions
+
+ // first check if moment.js is already loaded in the browser window, if so,
+ // use this instance. Else, load via commonjs.
+
+ 'use strict';
+
+ var moment = __webpack_require__(2);
+ var uuid = __webpack_require__(6);
+
+ /**
+ * Test whether given object is a number
+ * @param {*} object
+ * @return {Boolean} isNumber
+ */
+ exports.isNumber = function (object) {
+ return object instanceof Number || typeof object == 'number';
+ };
+
+ /**
+ * Remove everything in the DOM object
+ * @param DOMobject
+ */
+ exports.recursiveDOMDelete = function (DOMobject) {
+ if (DOMobject) {
+ while (DOMobject.hasChildNodes() === true) {
+ exports.recursiveDOMDelete(DOMobject.firstChild);
+ DOMobject.removeChild(DOMobject.firstChild);
+ }
+ }
+ };
+
+ /**
+ * this function gives you a range between 0 and 1 based on the min and max values in the set, the total sum of all values and the current value.
+ *
+ * @param min
+ * @param max
+ * @param total
+ * @param value
+ * @returns {number}
+ */
+ exports.giveRange = function (min, max, total, value) {
+ if (max == min) {
+ return 0.5;
+ } else {
+ var scale = 1 / (max - min);
+ return Math.max(0, (value - min) * scale);
+ }
+ };
+
+ /**
+ * Test whether given object is a string
+ * @param {*} object
+ * @return {Boolean} isString
+ */
+ exports.isString = function (object) {
+ return object instanceof String || typeof object == 'string';
+ };
+
+ /**
+ * Test whether given object is a Date, or a String containing a Date
+ * @param {Date | String} object
+ * @return {Boolean} isDate
+ */
+ exports.isDate = function (object) {
+ if (object instanceof Date) {
+ return true;
+ } else if (exports.isString(object)) {
+ // test whether this string contains a date
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return true;
+ } else if (!isNaN(Date.parse(object))) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Create a semi UUID
+ * source: http://stackoverflow.com/a/105074/1262753
+ * @return {String} uuid
+ */
+ exports.randomUUID = function () {
+ return uuid.v4();
+ };
+
+ /**
+ * assign all keys of an object that are not nested objects to a certain value (used for color objects).
+ * @param obj
+ * @param value
+ */
+ exports.assignAllKeys = function (obj, value) {
+ for (var prop in obj) {
+ if (obj.hasOwnProperty(prop)) {
+ if (typeof obj[prop] !== 'object') {
+ obj[prop] = value;
+ }
+ }
+ }
+ };
+
+ /**
+ * Fill an object with a possibly partially defined other object. Only copies values if the a object has an object requiring values.
+ * That means an object is not created on a property if only the b object has it.
+ * @param obj
+ * @param value
+ */
+ exports.fillIfDefined = function (a, b) {
+ var allowDeletion = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];
+
+ for (var prop in a) {
+ if (b[prop] !== undefined) {
+ if (typeof b[prop] !== 'object') {
+ if ((b[prop] === undefined || b[prop] === null) && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ } else {
+ if (typeof a[prop] === 'object') {
+ exports.fillIfDefined(a[prop], b[prop], allowDeletion);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Extend object a with the properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+ exports.protoExtend = function (a, b) {
+ for (var i = 1; i < arguments.length; i++) {
+ var other = arguments[i];
+ for (var prop in other) {
+ a[prop] = other[prop];
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Extend object a with the properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+ exports.extend = function (a, b) {
+ for (var i = 1; i < arguments.length; i++) {
+ var other = arguments[i];
+ for (var prop in other) {
+ if (other.hasOwnProperty(prop)) {
+ a[prop] = other[prop];
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.<String>} props
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ exports.selectiveExtend = function (props, a, b) {
+ if (!Array.isArray(props)) {
+ throw new Error('Array with property names expected as first argument');
+ }
+
+ for (var i = 2; i < arguments.length; i++) {
+ var other = arguments[i];
+
+ for (var p = 0; p < props.length; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
+ a[prop] = other[prop];
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.<String>} props
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ exports.selectiveDeepExtend = function (props, a, b) {
+ var allowDeletion = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
+
+ // TODO: add support for Arrays to deepExtend
+ if (Array.isArray(b)) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ }
+ for (var i = 2; i < arguments.length; i++) {
+ var other = arguments[i];
+ for (var p = 0; p < props.length; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ exports.deepExtend(a[prop], b[prop], false, allowDeletion);
+ } else {
+ if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ } else if (Array.isArray(b[prop])) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ } else {
+ if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.<String>} props
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ exports.selectiveNotDeepExtend = function (props, a, b) {
+ var allowDeletion = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
+
+ // TODO: add support for Arrays to deepExtend
+ if (Array.isArray(b)) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ }
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ if (props.indexOf(prop) == -1) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ exports.deepExtend(a[prop], b[prop]);
+ } else {
+ if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ } else if (Array.isArray(b[prop])) {
+ a[prop] = [];
+ for (var i = 0; i < b[prop].length; i++) {
+ a[prop].push(b[prop][i]);
+ }
+ } else {
+ if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Deep extend an object a with the properties of object b
+ * @param {Object} a
+ * @param {Object} b
+ * @param [Boolean] protoExtend --> optional parameter. If true, the prototype values will also be extended.
+ * (ie. the options objects that inherit from others will also get the inherited options)
+ * @param [Boolean] global --> optional parameter. If true, the values of fields that are null will not deleted
+ * @returns {Object}
+ */
+ exports.deepExtend = function (a, b, protoExtend, allowDeletion) {
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop) || protoExtend === true) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ exports.deepExtend(a[prop], b[prop], protoExtend);
+ } else {
+ if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ } else if (Array.isArray(b[prop])) {
+ a[prop] = [];
+ for (var i = 0; i < b[prop].length; i++) {
+ a[prop].push(b[prop][i]);
+ }
+ } else {
+ if (b[prop] === null && a[prop] !== undefined && allowDeletion === true) {
+ delete a[prop];
+ } else {
+ a[prop] = b[prop];
+ }
+ }
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Test whether all elements in two arrays are equal.
+ * @param {Array} a
+ * @param {Array} b
+ * @return {boolean} Returns true if both arrays have the same length and same
+ * elements.
+ */
+ exports.equalArray = function (a, b) {
+ if (a.length != b.length) return false;
+
+ for (var i = 0, len = a.length; i < len; i++) {
+ if (a[i] != b[i]) return false;
+ }
+
+ return true;
+ };
+
+ /**
+ * Convert an object to another type
+ * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
+ * @param {String | undefined} type Name of the type. Available types:
+ * 'Boolean', 'Number', 'String',
+ * 'Date', 'Moment', ISODate', 'ASPDate'.
+ * @return {*} object
+ * @throws Error
+ */
+ exports.convert = function (object, type) {
+ var match;
+
+ if (object === undefined) {
+ return undefined;
+ }
+ if (object === null) {
+ return null;
+ }
+
+ if (!type) {
+ return object;
+ }
+ if (!(typeof type === 'string') && !(type instanceof String)) {
+ throw new Error('Type must be a string');
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'boolean':
+ case 'Boolean':
+ return Boolean(object);
+
+ case 'number':
+ case 'Number':
+ return Number(object.valueOf());
+
+ case 'string':
+ case 'String':
+ return String(object);
+
+ case 'Date':
+ if (exports.isNumber(object)) {
+ return new Date(object);
+ }
+ if (object instanceof Date) {
+ return new Date(object.valueOf());
+ } else if (moment.isMoment(object)) {
+ return new Date(object.valueOf());
+ }
+ if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return new Date(Number(match[1])); // parse number
+ } else {
+ return moment(object).toDate(); // parse string
+ }
+ } else {
+ throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date');
+ }
+
+ case 'Moment':
+ if (exports.isNumber(object)) {
+ return moment(object);
+ }
+ if (object instanceof Date) {
+ return moment(object.valueOf());
+ } else if (moment.isMoment(object)) {
+ return moment(object);
+ }
+ if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return moment(Number(match[1])); // parse number
+ } else {
+ return moment(object); // parse string
+ }
+ } else {
+ throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type Date');
+ }
+
+ case 'ISODate':
+ if (exports.isNumber(object)) {
+ return new Date(object);
+ } else if (object instanceof Date) {
+ return object.toISOString();
+ } else if (moment.isMoment(object)) {
+ return object.toDate().toISOString();
+ } else if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return new Date(Number(match[1])).toISOString(); // parse number
+ } else {
+ return new Date(object).toISOString(); // parse string
+ }
+ } else {
+ throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ISODate');
+ }
+
+ case 'ASPDate':
+ if (exports.isNumber(object)) {
+ return '/Date(' + object + ')/';
+ } else if (object instanceof Date) {
+ return '/Date(' + object.valueOf() + ')/';
+ } else if (exports.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ var value;
+ if (match) {
+ // object is an ASP date
+ value = new Date(Number(match[1])).valueOf(); // parse number
+ } else {
+ value = new Date(object).valueOf(); // parse string
+ }
+ return '/Date(' + value + ')/';
+ } else {
+ throw new Error('Cannot convert object of type ' + exports.getType(object) + ' to type ASPDate');
+ }
+
+ default:
+ throw new Error('Unknown type "' + type + '"');
+ }
+ };
+
+ // parse ASP.Net Date pattern,
+ // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
+ // code from http://momentjs.com/
+ var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+
+ /**
+ * Get the type of an object, for example exports.getType([]) returns 'Array'
+ * @param {*} object
+ * @return {String} type
+ */
+ exports.getType = function (object) {
+ var type = typeof object;
+
+ if (type == 'object') {
+ if (object === null) {
+ return 'null';
+ }
+ if (object instanceof Boolean) {
+ return 'Boolean';
+ }
+ if (object instanceof Number) {
+ return 'Number';
+ }
+ if (object instanceof String) {
+ return 'String';
+ }
+ if (Array.isArray(object)) {
+ return 'Array';
+ }
+ if (object instanceof Date) {
+ return 'Date';
+ }
+ return 'Object';
+ } else if (type == 'number') {
+ return 'Number';
+ } else if (type == 'boolean') {
+ return 'Boolean';
+ } else if (type == 'string') {
+ return 'String';
+ } else if (type === undefined) {
+ return 'undefined';
+ }
+
+ return type;
+ };
+
+ /**
+ * Used to extend an array and copy it. This is used to propagate paths recursively.
+ *
+ * @param arr
+ * @param newValue
+ * @returns {Array}
+ */
+ exports.copyAndExtendArray = function (arr, newValue) {
+ var newArr = [];
+ for (var i = 0; i < arr.length; i++) {
+ newArr.push(arr[i]);
+ }
+ newArr.push(newValue);
+ return newArr;
+ };
+
+ /**
+ * Used to extend an array and copy it. This is used to propagate paths recursively.
+ *
+ * @param arr
+ * @param newValue
+ * @returns {Array}
+ */
+ exports.copyArray = function (arr) {
+ var newArr = [];
+ for (var i = 0; i < arr.length; i++) {
+ newArr.push(arr[i]);
+ }
+ return newArr;
+ };
+
+ /**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} left The absolute left position of this element
+ * in the browser page.
+ */
+ exports.getAbsoluteLeft = function (elem) {
+ return elem.getBoundingClientRect().left;
+ };
+
+ /**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} top The absolute top position of this element
+ * in the browser page.
+ */
+ exports.getAbsoluteTop = function (elem) {
+ return elem.getBoundingClientRect().top;
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ exports.addClassName = function (elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ exports.removeClassName = function (elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * For each method for both arrays and objects.
+ * In case of an array, the built-in Array.forEach() is applied.
+ * In case of an Object, the method loops over all properties of the object.
+ * @param {Object | Array} object An Object or Array
+ * @param {function} callback Callback method, called for each item in
+ * the object or array with three parameters:
+ * callback(value, index, object)
+ */
+ exports.forEach = function (object, callback) {
+ var i, len;
+ if (Array.isArray(object)) {
+ // array
+ for (i = 0, len = object.length; i < len; i++) {
+ callback(object[i], i, object);
+ }
+ } else {
+ // object
+ for (i in object) {
+ if (object.hasOwnProperty(i)) {
+ callback(object[i], i, object);
+ }
+ }
+ }
+ };
+
+ /**
+ * Convert an object into an array: all objects properties are put into the
+ * array. The resulting array is unordered.
+ * @param {Object} object
+ * @param {Array} array
+ */
+ exports.toArray = function (object) {
+ var array = [];
+
+ for (var prop in object) {
+ if (object.hasOwnProperty(prop)) array.push(object[prop]);
+ }
+
+ return array;
+ };
+
+ /**
+ * Update a property in an object
+ * @param {Object} object
+ * @param {String} key
+ * @param {*} value
+ * @return {Boolean} changed
+ */
+ exports.updateProperty = function (object, key, value) {
+ if (object[key] !== value) {
+ object[key] = value;
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * Throttle the given function to be only executed once every `wait` milliseconds
+ * @param {function} fn
+ * @param {number} wait Time in milliseconds
+ * @returns {function} Returns the throttled function
+ */
+ exports.throttle = function (fn, wait) {
+ var timeout = null;
+ var needExecution = false;
+
+ return function throttled() {
+ if (!timeout) {
+ needExecution = false;
+ fn();
+
+ timeout = setTimeout(function () {
+ timeout = null;
+ if (needExecution) {
+ throttled();
+ }
+ }, wait);
+ } else {
+ needExecution = true;
+ }
+ };
+ };
+
+ /**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture]
+ */
+ exports.addEventListener = function (element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined) useCapture = false;
+
+ if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
+ action = 'DOMMouseScroll'; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent('on' + action, listener); // IE browsers
+ }
+ };
+
+ /**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture]
+ */
+ exports.removeEventListener = function (element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined) useCapture = false;
+
+ if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
+ action = 'DOMMouseScroll'; // For Firefox
+ }
+
+ element.removeEventListener(action, listener, useCapture);
+ } else {
+ // IE browsers
+ element.detachEvent('on' + action, listener);
+ }
+ };
+
+ /**
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
+ */
+ exports.preventDefault = function (event) {
+ if (!event) event = window.event;
+
+ if (event.preventDefault) {
+ event.preventDefault(); // non-IE browsers
+ } else {
+ event.returnValue = false; // IE browsers
+ }
+ };
+
+ /**
+ * Get HTML element which is the target of the event
+ * @param {Event} event
+ * @return {Element} target element
+ */
+ exports.getTarget = function (event) {
+ // code from http://www.quirksmode.org/js/events_properties.html
+ if (!event) {
+ event = window.event;
+ }
+
+ var target;
+
+ if (event.target) {
+ target = event.target;
+ } else if (event.srcElement) {
+ target = event.srcElement;
+ }
+
+ if (target.nodeType != undefined && target.nodeType == 3) {
+ // defeat Safari bug
+ target = target.parentNode;
+ }
+
+ return target;
+ };
+
+ /**
+ * Check if given element contains given parent somewhere in the DOM tree
+ * @param {Element} element
+ * @param {Element} parent
+ */
+ exports.hasParent = function (element, parent) {
+ var e = element;
+
+ while (e) {
+ if (e === parent) {
+ return true;
+ }
+ e = e.parentNode;
+ }
+
+ return false;
+ };
+
+ exports.option = {};
+
+ /**
+ * Convert a value into a boolean
+ * @param {Boolean | function | undefined} value
+ * @param {Boolean} [defaultValue]
+ * @returns {Boolean} bool
+ */
+ exports.option.asBoolean = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return value != false;
+ }
+
+ return defaultValue || null;
+ };
+
+ /**
+ * Convert a value into a number
+ * @param {Boolean | function | undefined} value
+ * @param {Number} [defaultValue]
+ * @returns {Number} number
+ */
+ exports.option.asNumber = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return Number(value) || defaultValue || null;
+ }
+
+ return defaultValue || null;
+ };
+
+ /**
+ * Convert a value into a string
+ * @param {String | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} str
+ */
+ exports.option.asString = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return String(value);
+ }
+
+ return defaultValue || null;
+ };
+
+ /**
+ * Convert a size or location into a string with pixels or a percentage
+ * @param {String | Number | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} size
+ */
+ exports.option.asSize = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (exports.isString(value)) {
+ return value;
+ } else if (exports.isNumber(value)) {
+ return value + 'px';
+ } else {
+ return defaultValue || null;
+ }
+ };
+
+ /**
+ * Convert a value into a DOM element
+ * @param {HTMLElement | function | undefined} value
+ * @param {HTMLElement} [defaultValue]
+ * @returns {HTMLElement | null} dom
+ */
+ exports.option.asElement = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ return value || defaultValue || null;
+ };
+
+ /**
+ * http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
+ *
+ * @param {String} hex
+ * @returns {{r: *, g: *, b: *}} | 255 range
+ */
+ exports.hexToRGB = function (hex) {
+ // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
+ var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+ hex = hex.replace(shorthandRegex, function (m, r, g, b) {
+ return r + r + g + g + b + b;
+ });
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : null;
+ };
+
+ /**
+ * This function takes color in hex format or rgb() or rgba() format and overrides the opacity. Returns rgba() string.
+ * @param color
+ * @param opacity
+ * @returns {*}
+ */
+ exports.overrideOpacity = function (color, opacity) {
+ if (color.indexOf('rgba') != -1) {
+ return color;
+ } else if (color.indexOf('rgb') != -1) {
+ var rgb = color.substr(color.indexOf('(') + 1).replace(')', '').split(',');
+ return 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',' + opacity + ')';
+ } else {
+ var rgb = exports.hexToRGB(color);
+ if (rgb == null) {
+ return color;
+ } else {
+ return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + opacity + ')';
+ }
+ }
+ };
+
+ /**
+ *
+ * @param red 0 -- 255
+ * @param green 0 -- 255
+ * @param blue 0 -- 255
+ * @returns {string}
+ * @constructor
+ */
+ exports.RGBToHex = function (red, green, blue) {
+ return '#' + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1);
+ };
+
+ /**
+ * Parse a color property into an object with border, background, and
+ * highlight colors
+ * @param {Object | String} color
+ * @return {Object} colorObject
+ */
+ exports.parseColor = function (color) {
+ var c;
+ if (exports.isString(color) === true) {
+ if (exports.isValidRGB(color) === true) {
+ var rgb = color.substr(4).substr(0, color.length - 5).split(',').map(function (value) {
+ return parseInt(value);
+ });
+ color = exports.RGBToHex(rgb[0], rgb[1], rgb[2]);
+ }
+ if (exports.isValidHex(color) === true) {
+ var hsv = exports.hexToHSV(color);
+ var lighterColorHSV = { h: hsv.h, s: hsv.s * 0.8, v: Math.min(1, hsv.v * 1.02) };
+ var darkerColorHSV = { h: hsv.h, s: Math.min(1, hsv.s * 1.25), v: hsv.v * 0.8 };
+ var darkerColorHex = exports.HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v);
+ var lighterColorHex = exports.HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v);
+ c = {
+ background: color,
+ border: darkerColorHex,
+ highlight: {
+ background: lighterColorHex,
+ border: darkerColorHex
+ },
+ hover: {
+ background: lighterColorHex,
+ border: darkerColorHex
+ }
+ };
+ } else {
+ c = {
+ background: color,
+ border: color,
+ highlight: {
+ background: color,
+ border: color
+ },
+ hover: {
+ background: color,
+ border: color
+ }
+ };
+ }
+ } else {
+ c = {};
+ c.background = color.background || undefined;
+ c.border = color.border || undefined;
+
+ if (exports.isString(color.highlight)) {
+ c.highlight = {
+ border: color.highlight,
+ background: color.highlight
+ };
+ } else {
+ c.highlight = {};
+ c.highlight.background = color.highlight && color.highlight.background || undefined;
+ c.highlight.border = color.highlight && color.highlight.border || undefined;
+ }
+
+ if (exports.isString(color.hover)) {
+ c.hover = {
+ border: color.hover,
+ background: color.hover
+ };
+ } else {
+ c.hover = {};
+ c.hover.background = color.hover && color.hover.background || undefined;
+ c.hover.border = color.hover && color.hover.border || undefined;
+ }
+ }
+
+ return c;
+ };
+
+ /**
+ * http://www.javascripter.net/faq/rgb2hsv.htm
+ *
+ * @param red
+ * @param green
+ * @param blue
+ * @returns {*}
+ * @constructor
+ */
+ exports.RGBToHSV = function (red, green, blue) {
+ red = red / 255;green = green / 255;blue = blue / 255;
+ var minRGB = Math.min(red, Math.min(green, blue));
+ var maxRGB = Math.max(red, Math.max(green, blue));
+
+ // Black-gray-white
+ if (minRGB == maxRGB) {
+ return { h: 0, s: 0, v: minRGB };
+ }
+
+ // Colors other than black-gray-white:
+ var d = red == minRGB ? green - blue : blue == minRGB ? red - green : blue - red;
+ var h = red == minRGB ? 3 : blue == minRGB ? 1 : 5;
+ var hue = 60 * (h - d / (maxRGB - minRGB)) / 360;
+ var saturation = (maxRGB - minRGB) / maxRGB;
+ var value = maxRGB;
+ return { h: hue, s: saturation, v: value };
+ };
+
+ var cssUtil = {
+ // split a string with css styles into an object with key/values
+ split: function split(cssText) {
+ var styles = {};
+
+ cssText.split(';').forEach(function (style) {
+ if (style.trim() != '') {
+ var parts = style.split(':');
+ var key = parts[0].trim();
+ var value = parts[1].trim();
+ styles[key] = value;
+ }
+ });
+
+ return styles;
+ },
+
+ // build a css text string from an object with key/values
+ join: function join(styles) {
+ return Object.keys(styles).map(function (key) {
+ return key + ': ' + styles[key];
+ }).join('; ');
+ }
+ };
+
+ /**
+ * Append a string with css styles to an element
+ * @param {Element} element
+ * @param {String} cssText
+ */
+ exports.addCssText = function (element, cssText) {
+ var currentStyles = cssUtil.split(element.style.cssText);
+ var newStyles = cssUtil.split(cssText);
+ var styles = exports.extend(currentStyles, newStyles);
+
+ element.style.cssText = cssUtil.join(styles);
+ };
+
+ /**
+ * Remove a string with css styles from an element
+ * @param {Element} element
+ * @param {String} cssText
+ */
+ exports.removeCssText = function (element, cssText) {
+ var styles = cssUtil.split(element.style.cssText);
+ var removeStyles = cssUtil.split(cssText);
+
+ for (var key in removeStyles) {
+ if (removeStyles.hasOwnProperty(key)) {
+ delete styles[key];
+ }
+ }
+
+ element.style.cssText = cssUtil.join(styles);
+ };
+
+ /**
+ * https://gist.github.com/mjijackson/5311256
+ * @param h
+ * @param s
+ * @param v
+ * @returns {{r: number, g: number, b: number}}
+ * @constructor
+ */
+ exports.HSVToRGB = function (h, s, v) {
+ var r, g, b;
+
+ var i = Math.floor(h * 6);
+ var f = h * 6 - i;
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0:
+ r = v, g = t, b = p;break;
+ case 1:
+ r = q, g = v, b = p;break;
+ case 2:
+ r = p, g = v, b = t;break;
+ case 3:
+ r = p, g = q, b = v;break;
+ case 4:
+ r = t, g = p, b = v;break;
+ case 5:
+ r = v, g = p, b = q;break;
+ }
+
+ return { r: Math.floor(r * 255), g: Math.floor(g * 255), b: Math.floor(b * 255) };
+ };
+
+ exports.HSVToHex = function (h, s, v) {
+ var rgb = exports.HSVToRGB(h, s, v);
+ return exports.RGBToHex(rgb.r, rgb.g, rgb.b);
+ };
+
+ exports.hexToHSV = function (hex) {
+ var rgb = exports.hexToRGB(hex);
+ return exports.RGBToHSV(rgb.r, rgb.g, rgb.b);
+ };
+
+ exports.isValidHex = function (hex) {
+ var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
+ return isOk;
+ };
+
+ exports.isValidRGB = function (rgb) {
+ rgb = rgb.replace(' ', '');
+ var isOk = /rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(rgb);
+ return isOk;
+ };
+ exports.isValidRGBA = function (rgba) {
+ rgba = rgba.replace(' ', '');
+ var isOk = /rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(rgba);
+ return isOk;
+ };
+
+ /**
+ * This recursively redirects the prototype of JSON objects to the referenceObject
+ * This is used for default options.
+ *
+ * @param referenceObject
+ * @returns {*}
+ */
+ exports.selectiveBridgeObject = function (fields, referenceObject) {
+ if (typeof referenceObject == 'object') {
+ var objectTo = Object.create(referenceObject);
+ for (var i = 0; i < fields.length; i++) {
+ if (referenceObject.hasOwnProperty(fields[i])) {
+ if (typeof referenceObject[fields[i]] == 'object') {
+ objectTo[fields[i]] = exports.bridgeObject(referenceObject[fields[i]]);
+ }
+ }
+ }
+ return objectTo;
+ } else {
+ return null;
+ }
+ };
+
+ /**
+ * This recursively redirects the prototype of JSON objects to the referenceObject
+ * This is used for default options.
+ *
+ * @param referenceObject
+ * @returns {*}
+ */
+ exports.bridgeObject = function (referenceObject) {
+ if (typeof referenceObject == 'object') {
+ var objectTo = Object.create(referenceObject);
+ for (var i in referenceObject) {
+ if (referenceObject.hasOwnProperty(i)) {
+ if (typeof referenceObject[i] == 'object') {
+ objectTo[i] = exports.bridgeObject(referenceObject[i]);
+ }
+ }
+ }
+ return objectTo;
+ } else {
+ return null;
+ }
+ };
+
+ /**
+ * this is used to set the options of subobjects in the options object. A requirement of these subobjects
+ * is that they have an 'enabled' element which is optional for the user but mandatory for the program.
+ *
+ * @param [object] mergeTarget | this is either this.options or the options used for the groups.
+ * @param [object] options | options
+ * @param [String] option | this is the option key in the options argument
+ * @private
+ */
+ exports.mergeOptions = function (mergeTarget, options, option) {
+ var allowDeletion = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3];
+ var globalOptions = arguments.length <= 4 || arguments[4] === undefined ? {} : arguments[4];
+
+ if (options[option] === null) {
+ mergeTarget[option] = Object.create(globalOptions[option]);
+ } else {
+ if (options[option] !== undefined) {
+ if (typeof options[option] === 'boolean') {
+ mergeTarget[option].enabled = options[option];
+ } else {
+ if (options[option].enabled === undefined) {
+ mergeTarget[option].enabled = true;
+ }
+ for (var prop in options[option]) {
+ if (options[option].hasOwnProperty(prop)) {
+ mergeTarget[option][prop] = options[option][prop];
+ }
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses
+ * this function will then iterate in both directions over this sorted list to find all visible items.
+ *
+ * @param {Item[]} orderedItems | Items ordered by start
+ * @param {function} searchFunction | -1 is lower, 0 is found, 1 is higher
+ * @param {String} field
+ * @param {String} field2
+ * @returns {number}
+ * @private
+ */
+ exports.binarySearchCustom = function (orderedItems, searchFunction, field, field2) {
+ var maxIterations = 10000;
+ var iteration = 0;
+ var low = 0;
+ var high = orderedItems.length - 1;
+
+ while (low <= high && iteration < maxIterations) {
+ var middle = Math.floor((low + high) / 2);
+
+ var item = orderedItems[middle];
+ var value = field2 === undefined ? item[field] : item[field][field2];
+
+ var searchResult = searchFunction(value);
+ if (searchResult == 0) {
+ // jihaa, found a visible item!
+ return middle;
+ } else if (searchResult == -1) {
+ // it is too small --> increase low
+ low = middle + 1;
+ } else {
+ // it is too big --> decrease high
+ high = middle - 1;
+ }
+
+ iteration++;
+ }
+
+ return -1;
+ };
+
+ /**
+ * This function does a binary search for a specific value in a sorted array. If it does not exist but is in between of
+ * two values, we return either the one before or the one after, depending on user input
+ * If it is found, we return the index, else -1.
+ *
+ * @param {Array} orderedItems
+ * @param {{start: number, end: number}} target
+ * @param {String} field
+ * @param {String} sidePreference 'before' or 'after'
+ * @returns {number}
+ * @private
+ */
+ exports.binarySearchValue = function (orderedItems, target, field, sidePreference) {
+ var maxIterations = 10000;
+ var iteration = 0;
+ var low = 0;
+ var high = orderedItems.length - 1;
+ var prevValue, value, nextValue, middle;
+
+ while (low <= high && iteration < maxIterations) {
+ // get a new guess
+ middle = Math.floor(0.5 * (high + low));
+ prevValue = orderedItems[Math.max(0, middle - 1)][field];
+ value = orderedItems[middle][field];
+ nextValue = orderedItems[Math.min(orderedItems.length - 1, middle + 1)][field];
+
+ if (value == target) {
+ // we found the target
+ return middle;
+ } else if (prevValue < target && value > target) {
+ // target is in between of the previous and the current
+ return sidePreference == 'before' ? Math.max(0, middle - 1) : middle;
+ } else if (value < target && nextValue > target) {
+ // target is in between of the current and the next
+ return sidePreference == 'before' ? middle : Math.min(orderedItems.length - 1, middle + 1);
+ } else {
+ // didnt find the target, we need to change our boundaries.
+ if (value < target) {
+ // it is too small --> increase low
+ low = middle + 1;
+ } else {
+ // it is too big --> decrease high
+ high = middle - 1;
+ }
+ }
+ iteration++;
+ }
+
+ // didnt find anything. Return -1.
+ return -1;
+ };
+
+ /*
+ * Easing Functions - inspired from http://gizma.com/easing/
+ * only considering the t value for the range [0, 1] => [0, 1]
+ * https://gist.github.com/gre/1650294
+ */
+ exports.easingFunctions = {
+ // no easing, no acceleration
+ linear: function linear(t) {
+ return t;
+ },
+ // accelerating from zero velocity
+ easeInQuad: function easeInQuad(t) {
+ return t * t;
+ },
+ // decelerating to zero velocity
+ easeOutQuad: function easeOutQuad(t) {
+ return t * (2 - t);
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutQuad: function easeInOutQuad(t) {
+ return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+ },
+ // accelerating from zero velocity
+ easeInCubic: function easeInCubic(t) {
+ return t * t * t;
+ },
+ // decelerating to zero velocity
+ easeOutCubic: function easeOutCubic(t) {
+ return --t * t * t + 1;
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutCubic: function easeInOutCubic(t) {
+ return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
+ },
+ // accelerating from zero velocity
+ easeInQuart: function easeInQuart(t) {
+ return t * t * t * t;
+ },
+ // decelerating to zero velocity
+ easeOutQuart: function easeOutQuart(t) {
+ return 1 - --t * t * t * t;
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutQuart: function easeInOutQuart(t) {
+ return t < .5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;
+ },
+ // accelerating from zero velocity
+ easeInQuint: function easeInQuint(t) {
+ return t * t * t * t * t;
+ },
+ // decelerating to zero velocity
+ easeOutQuint: function easeOutQuint(t) {
+ return 1 + --t * t * t * t * t;
+ },
+ // acceleration until halfway, then deceleration
+ easeInOutQuint: function easeInOutQuint(t) {
+ return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t;
+ }
+ };
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // first check if moment.js is already loaded in the browser window, if so,
+ // use this instance. Else, load via commonjs.
+ 'use strict';
+
+ module.exports = typeof window !== 'undefined' && window['moment'] || __webpack_require__(3);
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(module) {//! moment.js
+ //! version : 2.10.6
+ //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+ //! license : MIT
+ //! momentjs.com
+
+ (function (global, factory) {
+ true ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ global.moment = factory()
+ }(this, function () { 'use strict';
+
+ var hookCallback;
+
+ function utils_hooks__hooks () {
+ return hookCallback.apply(null, arguments);
+ }
+
+ // This is done to register the method called with moment()
+ // without creating circular dependencies.
+ function setHookCallback (callback) {
+ hookCallback = callback;
+ }
+
+ function isArray(input) {
+ return Object.prototype.toString.call(input) === '[object Array]';
+ }
+
+ function isDate(input) {
+ return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
+ }
+
+ function map(arr, fn) {
+ var res = [], i;
+ for (i = 0; i < arr.length; ++i) {
+ res.push(fn(arr[i], i));
+ }
+ return res;
+ }
+
+ function hasOwnProp(a, b) {
+ return Object.prototype.hasOwnProperty.call(a, b);
+ }
+
+ function extend(a, b) {
+ for (var i in b) {
+ if (hasOwnProp(b, i)) {
+ a[i] = b[i];
+ }
+ }
+
+ if (hasOwnProp(b, 'toString')) {
+ a.toString = b.toString;
+ }
+
+ if (hasOwnProp(b, 'valueOf')) {
+ a.valueOf = b.valueOf;
+ }
+
+ return a;
+ }
+
+ function create_utc__createUTC (input, format, locale, strict) {
+ return createLocalOrUTC(input, format, locale, strict, true).utc();
+ }
+
+ function defaultParsingFlags() {
+ // We need to deep clone this object.
+ return {
+ empty : false,
+ unusedTokens : [],
+ unusedInput : [],
+ overflow : -2,
+ charsLeftOver : 0,
+ nullInput : false,
+ invalidMonth : null,
+ invalidFormat : false,
+ userInvalidated : false,
+ iso : false
+ };
+ }
+
+ function getParsingFlags(m) {
+ if (m._pf == null) {
+ m._pf = defaultParsingFlags();
+ }
+ return m._pf;
+ }
+
+ function valid__isValid(m) {
+ if (m._isValid == null) {
+ var flags = getParsingFlags(m);
+ m._isValid = !isNaN(m._d.getTime()) &&
+ flags.overflow < 0 &&
+ !flags.empty &&
+ !flags.invalidMonth &&
+ !flags.invalidWeekday &&
+ !flags.nullInput &&
+ !flags.invalidFormat &&
+ !flags.userInvalidated;
+
+ if (m._strict) {
+ m._isValid = m._isValid &&
+ flags.charsLeftOver === 0 &&
+ flags.unusedTokens.length === 0 &&
+ flags.bigHour === undefined;
+ }
+ }
+ return m._isValid;
+ }
+
+ function valid__createInvalid (flags) {
+ var m = create_utc__createUTC(NaN);
+ if (flags != null) {
+ extend(getParsingFlags(m), flags);
+ }
+ else {
+ getParsingFlags(m).userInvalidated = true;
+ }
+
+ return m;
+ }
+
+ var momentProperties = utils_hooks__hooks.momentProperties = [];
+
+ function copyConfig(to, from) {
+ var i, prop, val;
+
+ if (typeof from._isAMomentObject !== 'undefined') {
+ to._isAMomentObject = from._isAMomentObject;
+ }
+ if (typeof from._i !== 'undefined') {
+ to._i = from._i;
+ }
+ if (typeof from._f !== 'undefined') {
+ to._f = from._f;
+ }
+ if (typeof from._l !== 'undefined') {
+ to._l = from._l;
+ }
+ if (typeof from._strict !== 'undefined') {
+ to._strict = from._strict;
+ }
+ if (typeof from._tzm !== 'undefined') {
+ to._tzm = from._tzm;
+ }
+ if (typeof from._isUTC !== 'undefined') {
+ to._isUTC = from._isUTC;
+ }
+ if (typeof from._offset !== 'undefined') {
+ to._offset = from._offset;
+ }
+ if (typeof from._pf !== 'undefined') {
+ to._pf = getParsingFlags(from);
+ }
+ if (typeof from._locale !== 'undefined') {
+ to._locale = from._locale;
+ }
+
+ if (momentProperties.length > 0) {
+ for (i in momentProperties) {
+ prop = momentProperties[i];
+ val = from[prop];
+ if (typeof val !== 'undefined') {
+ to[prop] = val;
+ }
+ }
+ }
+
+ return to;
+ }
+
+ var updateInProgress = false;
+
+ // Moment prototype object
+ function Moment(config) {
+ copyConfig(this, config);
+ this._d = new Date(config._d != null ? config._d.getTime() : NaN);
+ // Prevent infinite loop in case updateOffset creates new moment
+ // objects.
+ if (updateInProgress === false) {
+ updateInProgress = true;
+ utils_hooks__hooks.updateOffset(this);
+ updateInProgress = false;
+ }
+ }
+
+ function isMoment (obj) {
+ return obj instanceof Moment || (obj != null && obj._isAMomentObject != null);
+ }
+
+ function absFloor (number) {
+ if (number < 0) {
+ return Math.ceil(number);
+ } else {
+ return Math.floor(number);
+ }
+ }
+
+ function toInt(argumentForCoercion) {
+ var coercedNumber = +argumentForCoercion,
+ value = 0;
+
+ if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+ value = absFloor(coercedNumber);
+ }
+
+ return value;
+ }
+
+ function compareArrays(array1, array2, dontConvert) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if ((dontConvert && array1[i] !== array2[i]) ||
+ (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+ }
+
+ function Locale() {
+ }
+
+ var locales = {};
+ var globalLocale;
+
+ function normalizeLocale(key) {
+ return key ? key.toLowerCase().replace('_', '-') : key;
+ }
+
+ // pick the locale from the array
+ // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+ // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+ function chooseLocale(names) {
+ var i = 0, j, next, locale, split;
+
+ while (i < names.length) {
+ split = normalizeLocale(names[i]).split('-');
+ j = split.length;
+ next = normalizeLocale(names[i + 1]);
+ next = next ? next.split('-') : null;
+ while (j > 0) {
+ locale = loadLocale(split.slice(0, j).join('-'));
+ if (locale) {
+ return locale;
+ }
+ if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+ //the next array item is better than a shallower substring of this one
+ break;
+ }
+ j--;
+ }
+ i++;
+ }
+ return null;
+ }
+
+ function loadLocale(name) {
+ var oldLocale = null;
+ // TODO: Find a better way to register and load all the locales in Node
+ if (!locales[name] && typeof module !== 'undefined' &&
+ module && module.exports) {
+ try {
+ oldLocale = globalLocale._abbr;
+ !(function webpackMissingModule() { var e = new Error("Cannot find module \"./locale\""); e.code = 'MODULE_NOT_FOUND'; throw e; }());
+ // because defineLocale currently also sets the global locale, we
+ // want to undo that for lazy loaded locales
+ locale_locales__getSetGlobalLocale(oldLocale);
+ } catch (e) { }
+ }
+ return locales[name];
+ }
+
+ // This function will load locale and then set the global locale. If
+ // no arguments are passed in, it will simply return the current global
+ // locale key.
+ function locale_locales__getSetGlobalLocale (key, values) {
+ var data;
+ if (key) {
+ if (typeof values === 'undefined') {
+ data = locale_locales__getLocale(key);
+ }
+ else {
+ data = defineLocale(key, values);
+ }
+
+ if (data) {
+ // moment.duration._locale = moment._locale = data;
+ globalLocale = data;
+ }
+ }
+
+ return globalLocale._abbr;
+ }
+
+ function defineLocale (name, values) {
+ if (values !== null) {
+ values.abbr = name;
+ locales[name] = locales[name] || new Locale();
+ locales[name].set(values);
+
+ // backwards compat for now: also set the locale
+ locale_locales__getSetGlobalLocale(name);
+
+ return locales[name];
+ } else {
+ // useful for testing
+ delete locales[name];
+ return null;
+ }
+ }
+
+ // returns locale data
+ function locale_locales__getLocale (key) {
+ var locale;
+
+ if (key && key._locale && key._locale._abbr) {
+ key = key._locale._abbr;
+ }
+
+ if (!key) {
+ return globalLocale;
+ }
+
+ if (!isArray(key)) {
+ //short-circuit everything else
+ locale = loadLocale(key);
+ if (locale) {
+ return locale;
+ }
+ key = [key];
+ }
+
+ return chooseLocale(key);
+ }
+
+ var aliases = {};
+
+ function addUnitAlias (unit, shorthand) {
+ var lowerCase = unit.toLowerCase();
+ aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit;
+ }
+
+ function normalizeUnits(units) {
+ return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined;
+ }
+
+ function normalizeObjectUnits(inputObject) {
+ var normalizedInput = {},
+ normalizedProp,
+ prop;
+
+ for (prop in inputObject) {
+ if (hasOwnProp(inputObject, prop)) {
+ normalizedProp = normalizeUnits(prop);
+ if (normalizedProp) {
+ normalizedInput[normalizedProp] = inputObject[prop];
+ }
+ }
+ }
+
+ return normalizedInput;
+ }
+
+ function makeGetSet (unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ get_set__set(this, unit, value);
+ utils_hooks__hooks.updateOffset(this, keepTime);
+ return this;
+ } else {
+ return get_set__get(this, unit);
+ }
+ };
+ }
+
+ function get_set__get (mom, unit) {
+ return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
+ }
+
+ function get_set__set (mom, unit, value) {
+ return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+ }
+
+ // MOMENTS
+
+ function getSet (units, value) {
+ var unit;
+ if (typeof units === 'object') {
+ for (unit in units) {
+ this.set(unit, units[unit]);
+ }
+ } else {
+ units = normalizeUnits(units);
+ if (typeof this[units] === 'function') {
+ return this[units](value);
+ }
+ }
+ return this;
+ }
+
+ function zeroFill(number, targetLength, forceSign) {
+ var absNumber = '' + Math.abs(number),
+ zerosToFill = targetLength - absNumber.length,
+ sign = number >= 0;
+ return (sign ? (forceSign ? '+' : '') : '-') +
+ Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber;
+ }
+
+ var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;
+
+ var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g;
+
+ var formatFunctions = {};
+
+ var formatTokenFunctions = {};
+
+ // token: 'M'
+ // padded: ['MM', 2]
+ // ordinal: 'Mo'
+ // callback: function () { this.month() + 1 }
+ function addFormatToken (token, padded, ordinal, callback) {
+ var func = callback;
+ if (typeof callback === 'string') {
+ func = function () {
+ return this[callback]();
+ };
+ }
+ if (token) {
+ formatTokenFunctions[token] = func;
+ }
+ if (padded) {
+ formatTokenFunctions[padded[0]] = function () {
+ return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
+ };
+ }
+ if (ordinal) {
+ formatTokenFunctions[ordinal] = function () {
+ return this.localeData().ordinal(func.apply(this, arguments), token);
+ };
+ }
+ }
+
+ function removeFormattingTokens(input) {
+ if (input.match(/\[[\s\S]/)) {
+ return input.replace(/^\[|\]$/g, '');
+ }
+ return input.replace(/\\/g, '');
+ }
+
+ function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = '';
+ for (i = 0; i < length; i++) {
+ output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
+
+ // format date using native date object
+ function formatMoment(m, format) {
+ if (!m.isValid()) {
+ return m.localeData().invalidDate();
+ }
+
+ format = expandFormat(format, m.localeData());
+ formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format);
+
+ return formatFunctions[format](m);
+ }
+
+ function expandFormat(format, locale) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return locale.longDateFormat(input) || input;
+ }
+
+ localFormattingTokens.lastIndex = 0;
+ while (i >= 0 && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ localFormattingTokens.lastIndex = 0;
+ i -= 1;
+ }
+
+ return format;
+ }
+
+ var match1 = /\d/; // 0 - 9
+ var match2 = /\d\d/; // 00 - 99
+ var match3 = /\d{3}/; // 000 - 999
+ var match4 = /\d{4}/; // 0000 - 9999
+ var match6 = /[+-]?\d{6}/; // -999999 - 999999
+ var match1to2 = /\d\d?/; // 0 - 99
+ var match1to3 = /\d{1,3}/; // 0 - 999
+ var match1to4 = /\d{1,4}/; // 0 - 9999
+ var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999
+
+ var matchUnsigned = /\d+/; // 0 - inf
+ var matchSigned = /[+-]?\d+/; // -inf - inf
+
+ var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z
+
+ var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123
+
+ // any word (or two) characters or numbers including two/three word month in arabic.
+ var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
+
+ var regexes = {};
+
+ function isFunction (sth) {
+ // https://github.com/moment/moment/issues/2325
+ return typeof sth === 'function' &&
+ Object.prototype.toString.call(sth) === '[object Function]';
+ }
+
+
+ function addRegexToken (token, regex, strictRegex) {
+ regexes[token] = isFunction(regex) ? regex : function (isStrict) {
+ return (isStrict && strictRegex) ? strictRegex : regex;
+ };
+ }
+
+ function getParseRegexForToken (token, config) {
+ if (!hasOwnProp(regexes, token)) {
+ return new RegExp(unescapeFormat(token));
+ }
+
+ return regexes[token](config._strict, config._locale);
+ }
+
+ // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ function unescapeFormat(s) {
+ return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+ return p1 || p2 || p3 || p4;
+ }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+ }
+
+ var tokens = {};
+
+ function addParseToken (token, callback) {
+ var i, func = callback;
+ if (typeof token === 'string') {
+ token = [token];
+ }
+ if (typeof callback === 'number') {
+ func = function (input, array) {
+ array[callback] = toInt(input);
+ };
+ }
+ for (i = 0; i < token.length; i++) {
+ tokens[token[i]] = func;
+ }
+ }
+
+ function addWeekParseToken (token, callback) {
+ addParseToken(token, function (input, array, config, token) {
+ config._w = config._w || {};
+ callback(input, config._w, config, token);
+ });
+ }
+
+ function addTimeToArrayFromToken(token, input, config) {
+ if (input != null && hasOwnProp(tokens, token)) {
+ tokens[token](input, config._a, config, token);
+ }
+ }
+
+ var YEAR = 0;
+ var MONTH = 1;
+ var DATE = 2;
+ var HOUR = 3;
+ var MINUTE = 4;
+ var SECOND = 5;
+ var MILLISECOND = 6;
+
+ function daysInMonth(year, month) {
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+ }
+
+ // FORMATTING
+
+ addFormatToken('M', ['MM', 2], 'Mo', function () {
+ return this.month() + 1;
+ });
+
+ addFormatToken('MMM', 0, 0, function (format) {
+ return this.localeData().monthsShort(this, format);
+ });
+
+ addFormatToken('MMMM', 0, 0, function (format) {
+ return this.localeData().months(this, format);
+ });
+
+ // ALIASES
+
+ addUnitAlias('month', 'M');
+
+ // PARSING
+
+ addRegexToken('M', match1to2);
+ addRegexToken('MM', match1to2, match2);
+ addRegexToken('MMM', matchWord);
+ addRegexToken('MMMM', matchWord);
+
+ addParseToken(['M', 'MM'], function (input, array) {
+ array[MONTH] = toInt(input) - 1;
+ });
+
+ addParseToken(['MMM', 'MMMM'], function (input, array, config, token) {
+ var month = config._locale.monthsParse(input, token, config._strict);
+ // if we didn't find a month name, mark the date as invalid.
+ if (month != null) {
+ array[MONTH] = month;
+ } else {
+ getParsingFlags(config).invalidMonth = input;
+ }
+ });
+
+ // LOCALES
+
+ var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_');
+ function localeMonths (m) {
+ return this._months[m.month()];
+ }
+
+ var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_');
+ function localeMonthsShort (m) {
+ return this._monthsShort[m.month()];
+ }
+
+ function localeMonthsParse (monthName, format, strict) {
+ var i, mom, regex;
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ this._longMonthsParse = [];
+ this._shortMonthsParse = [];
+ }
+
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ mom = create_utc__createUTC([2000, i]);
+ if (strict && !this._longMonthsParse[i]) {
+ this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i');
+ this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i');
+ }
+ if (!strict && !this._monthsParse[i]) {
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) {
+ return i;
+ } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) {
+ return i;
+ } else if (!strict && this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+ }
+
+ // MOMENTS
+
+ function setMonth (mom, value) {
+ var dayOfMonth;
+
+ // TODO: Move this out of here!
+ if (typeof value === 'string') {
+ value = mom.localeData().monthsParse(value);
+ // TODO: Another silent failure?
+ if (typeof value !== 'number') {
+ return mom;
+ }
+ }
+
+ dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value));
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+ return mom;
+ }
+
+ function getSetMonth (value) {
+ if (value != null) {
+ setMonth(this, value);
+ utils_hooks__hooks.updateOffset(this, true);
+ return this;
+ } else {
+ return get_set__get(this, 'Month');
+ }
+ }
+
+ function getDaysInMonth () {
+ return daysInMonth(this.year(), this.month());
+ }
+
+ function checkOverflow (m) {
+ var overflow;
+ var a = m._a;
+
+ if (a && getParsingFlags(m).overflow === -2) {
+ overflow =
+ a[MONTH] < 0 || a[MONTH] > 11 ? MONTH :
+ a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE :
+ a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR :
+ a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE :
+ a[SECOND] < 0 || a[SECOND] > 59 ? SECOND :
+ a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND :
+ -1;
+
+ if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+ overflow = DATE;
+ }
+
+ getParsingFlags(m).overflow = overflow;
+ }
+
+ return m;
+ }
+
+ function warn(msg) {
+ if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) {
+ console.warn('Deprecation warning: ' + msg);
+ }
+ }
+
+ function deprecate(msg, fn) {
+ var firstTime = true;
+
+ return extend(function () {
+ if (firstTime) {
+ warn(msg + '\n' + (new Error()).stack);
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
+ }
+
+ var deprecations = {};
+
+ function deprecateSimple(name, msg) {
+ if (!deprecations[name]) {
+ warn(msg);
+ deprecations[name] = true;
+ }
+ }
+
+ utils_hooks__hooks.suppressDeprecationWarnings = false;
+
+ var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
+
+ var isoDates = [
+ ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
+ ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
+ ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
+ ['GGGG-[W]WW', /\d{4}-W\d{2}/],
+ ['YYYY-DDD', /\d{4}-\d{3}/]
+ ];
+
+ // iso time formats and regexes
+ var isoTimes = [
+ ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
+ ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+ ['HH:mm', /(T| )\d\d:\d\d/],
+ ['HH', /(T| )\d\d/]
+ ];
+
+ var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i;
+
+ // date from iso format
+ function configFromISO(config) {
+ var i, l,
+ string = config._i,
+ match = from_string__isoRegex.exec(string);
+
+ if (match) {
+ getParsingFlags(config).iso = true;
+ for (i = 0, l = isoDates.length; i < l; i++) {
+ if (isoDates[i][1].exec(string)) {
+ config._f = isoDates[i][0];
+ break;
+ }
+ }
+ for (i = 0, l = isoTimes.length; i < l; i++) {
+ if (isoTimes[i][1].exec(string)) {
+ // match[6] should be 'T' or space
+ config._f += (match[6] || ' ') + isoTimes[i][0];
+ break;
+ }
+ }
+ if (string.match(matchOffset)) {
+ config._f += 'Z';
+ }
+ configFromStringAndFormat(config);
+ } else {
+ config._isValid = false;
+ }
+ }
+
+ // date from iso format or fallback
+ function configFromString(config) {
+ var matched = aspNetJsonRegex.exec(config._i);
+
+ if (matched !== null) {
+ config._d = new Date(+matched[1]);
+ return;
+ }
+
+ configFromISO(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ utils_hooks__hooks.createFromInputFallback(config);
+ }
+ }
+
+ utils_hooks__hooks.createFromInputFallback = deprecate(
+ 'moment construction falls back to js Date. This is ' +
+ 'discouraged and will be removed in upcoming major ' +
+ 'release. Please refer to ' +
+ 'https://github.com/moment/moment/issues/1407 for more info.',
+ function (config) {
+ config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+ }
+ );
+
+ function createDate (y, m, d, h, M, s, ms) {
+ //can't just apply() to create a date:
+ //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
+ var date = new Date(y, m, d, h, M, s, ms);
+
+ //the date constructor doesn't accept years < 1970
+ if (y < 1970) {
+ date.setFullYear(y);
+ }
+ return date;
+ }
+
+ function createUTCDate (y) {
+ var date = new Date(Date.UTC.apply(null, arguments));
+ if (y < 1970) {
+ date.setUTCFullYear(y);
+ }
+ return date;
+ }
+
+ addFormatToken(0, ['YY', 2], 0, function () {
+ return this.year() % 100;
+ });
+
+ addFormatToken(0, ['YYYY', 4], 0, 'year');
+ addFormatToken(0, ['YYYYY', 5], 0, 'year');
+ addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');
+
+ // ALIASES
+
+ addUnitAlias('year', 'y');
+
+ // PARSING
+
+ addRegexToken('Y', matchSigned);
+ addRegexToken('YY', match1to2, match2);
+ addRegexToken('YYYY', match1to4, match4);
+ addRegexToken('YYYYY', match1to6, match6);
+ addRegexToken('YYYYYY', match1to6, match6);
+
+ addParseToken(['YYYYY', 'YYYYYY'], YEAR);
+ addParseToken('YYYY', function (input, array) {
+ array[YEAR] = input.length === 2 ? utils_hooks__hooks.parseTwoDigitYear(input) : toInt(input);
+ });
+ addParseToken('YY', function (input, array) {
+ array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input);
+ });
+
+ // HELPERS
+
+ function daysInYear(year) {
+ return isLeapYear(year) ? 366 : 365;
+ }
+
+ function isLeapYear(year) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ }
+
+ // HOOKS
+
+ utils_hooks__hooks.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+ };
+
+ // MOMENTS
+
+ var getSetYear = makeGetSet('FullYear', false);
+
+ function getIsLeapYear () {
+ return isLeapYear(this.year());
+ }
+
+ addFormatToken('w', ['ww', 2], 'wo', 'week');
+ addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek');
+
+ // ALIASES
+
+ addUnitAlias('week', 'w');
+ addUnitAlias('isoWeek', 'W');
+
+ // PARSING
+
+ addRegexToken('w', match1to2);
+ addRegexToken('ww', match1to2, match2);
+ addRegexToken('W', match1to2);
+ addRegexToken('WW', match1to2, match2);
+
+ addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) {
+ week[token.substr(0, 1)] = toInt(input);
+ });
+
+ // HELPERS
+
+ // firstDayOfWeek 0 = sun, 6 = sat
+ // the day of the week that starts the week
+ // (usually sunday or monday)
+ // firstDayOfWeekOfYear 0 = sun, 6 = sat
+ // the first week is the week that contains the first
+ // of this day of the week
+ // (eg. ISO weeks use thursday (4))
+ function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+ var end = firstDayOfWeekOfYear - firstDayOfWeek,
+ daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
+ adjustedMoment;
+
+
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
+ }
+
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
+ }
+
+ adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd');
+ return {
+ week: Math.ceil(adjustedMoment.dayOfYear() / 7),
+ year: adjustedMoment.year()
+ };
+ }
+
+ // LOCALES
+
+ function localeWeek (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy).week;
+ }
+
+ var defaultLocaleWeek = {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+ };
+
+ function localeFirstDayOfWeek () {
+ return this._week.dow;
+ }
+
+ function localeFirstDayOfYear () {
+ return this._week.doy;
+ }
+
+ // MOMENTS
+
+ function getSetWeek (input) {
+ var week = this.localeData().week(this);
+ return input == null ? week : this.add((input - week) * 7, 'd');
+ }
+
+ function getSetISOWeek (input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add((input - week) * 7, 'd');
+ }
+
+ addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear');
+
+ // ALIASES
+
+ addUnitAlias('dayOfYear', 'DDD');
+
+ // PARSING
+
+ addRegexToken('DDD', match1to3);
+ addRegexToken('DDDD', match3);
+ addParseToken(['DDD', 'DDDD'], function (input, array, config) {
+ config._dayOfYear = toInt(input);
+ });
+
+ // HELPERS
+
+ //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+ function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
+ var week1Jan = 6 + firstDayOfWeek - firstDayOfWeekOfYear, janX = createUTCDate(year, 0, 1 + week1Jan), d = janX.getUTCDay(), dayOfYear;
+ if (d < firstDayOfWeek) {
+ d += 7;
+ }
+
+ weekday = weekday != null ? 1 * weekday : firstDayOfWeek;
+
+ dayOfYear = 1 + week1Jan + 7 * (week - 1) - d + weekday;
+
+ return {
+ year: dayOfYear > 0 ? year : year - 1,
+ dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
+ };
+ }
+
+ // MOMENTS
+
+ function getSetDayOfYear (input) {
+ var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
+ }
+
+ // Pick the first defined of two or three arguments.
+ function defaults(a, b, c) {
+ if (a != null) {
+ return a;
+ }
+ if (b != null) {
+ return b;
+ }
+ return c;
+ }
+
+ function currentDateArray(config) {
+ var now = new Date();
+ if (config._useUTC) {
+ return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()];
+ }
+ return [now.getFullYear(), now.getMonth(), now.getDate()];
+ }
+
+ // convert an array to a date.
+ // the array should mirror the parameters below
+ // note: all values past the year are optional and will default to the lowest possible value.
+ // [year, month, day , hour, minute, second, millisecond]
+ function configFromArray (config) {
+ var i, date, input = [], currentDate, yearToUse;
+
+ if (config._d) {
+ return;
+ }
+
+ currentDate = currentDateArray(config);
+
+ //compute day of the year from weeks and weekdays
+ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+ dayOfYearFromWeekInfo(config);
+ }
+
+ //if the day of the year is set, figure out what it is
+ if (config._dayOfYear) {
+ yearToUse = defaults(config._a[YEAR], currentDate[YEAR]);
+
+ if (config._dayOfYear > daysInYear(yearToUse)) {
+ getParsingFlags(config)._overflowDayOfYear = true;
+ }
+
+ date = createUTCDate(yearToUse, 0, config._dayOfYear);
+ config._a[MONTH] = date.getUTCMonth();
+ config._a[DATE] = date.getUTCDate();
+ }
+
+ // Default to current date.
+ // * if no year, month, day of month are given, default to today
+ // * if day of month is given, default month and year
+ // * if month is given, default only year
+ // * if year is given, don't default anything
+ for (i = 0; i < 3 && config._a[i] == null; ++i) {
+ config._a[i] = input[i] = currentDate[i];
+ }
+
+ // Zero out whatever was not defaulted, including time
+ for (; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // Check for 24:00:00.000
+ if (config._a[HOUR] === 24 &&
+ config._a[MINUTE] === 0 &&
+ config._a[SECOND] === 0 &&
+ config._a[MILLISECOND] === 0) {
+ config._nextDay = true;
+ config._a[HOUR] = 0;
+ }
+
+ config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
+ // Apply timezone offset from input. The actual utcOffset can be changed
+ // with parseZone.
+ if (config._tzm != null) {
+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+ }
+
+ if (config._nextDay) {
+ config._a[HOUR] = 24;
+ }
+ }
+
+ function dayOfYearFromWeekInfo(config) {
+ var w, weekYear, week, weekday, dow, doy, temp;
+
+ w = config._w;
+ if (w.GG != null || w.W != null || w.E != null) {
+ dow = 1;
+ doy = 4;
+
+ // TODO: We need to take the current isoWeekYear, but that depends on
+ // how we interpret now (local, utc, fixed offset). So create
+ // a now version of current config (take local/utc/offset flags, and
+ // create now).
+ weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year);
+ week = defaults(w.W, 1);
+ weekday = defaults(w.E, 1);
+ } else {
+ dow = config._locale._week.dow;
+ doy = config._locale._week.doy;
+
+ weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year);
+ week = defaults(w.w, 1);
+
+ if (w.d != null) {
+ // weekday -- low day numbers are considered next week
+ weekday = w.d;
+ if (weekday < dow) {
+ ++week;
+ }
+ } else if (w.e != null) {
+ // local weekday -- counting starts from begining of week
+ weekday = w.e + dow;
+ } else {
+ // default to begining of week
+ weekday = dow;
+ }
+ }
+ temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
+
+ config._a[YEAR] = temp.year;
+ config._dayOfYear = temp.dayOfYear;
+ }
+
+ utils_hooks__hooks.ISO_8601 = function () {};
+
+ // date from string and format string
+ function configFromStringAndFormat(config) {
+ // TODO: Move this to another part of the creation flow to prevent circular deps
+ if (config._f === utils_hooks__hooks.ISO_8601) {
+ configFromISO(config);
+ return;
+ }
+
+ config._a = [];
+ getParsingFlags(config).empty = true;
+
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var string = '' + config._i,
+ i, parsedInput, tokens, token, skipped,
+ stringLength = string.length,
+ totalParsedInputLength = 0;
+
+ tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
+
+ for (i = 0; i < tokens.length; i++) {
+ token = tokens[i];
+ parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+ if (parsedInput) {
+ skipped = string.substr(0, string.indexOf(parsedInput));
+ if (skipped.length > 0) {
+ getParsingFlags(config).unusedInput.push(skipped);
+ }
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ totalParsedInputLength += parsedInput.length;
+ }
+ // don't parse if it's not a known token
+ if (formatTokenFunctions[token]) {
+ if (parsedInput) {
+ getParsingFlags(config).empty = false;
+ }
+ else {
+ getParsingFlags(config).unusedTokens.push(token);
+ }
+ addTimeToArrayFromToken(token, parsedInput, config);
+ }
+ else if (config._strict && !parsedInput) {
+ getParsingFlags(config).unusedTokens.push(token);
+ }
+ }
+
+ // add remaining unparsed input length to the string
+ getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
+ if (string.length > 0) {
+ getParsingFlags(config).unusedInput.push(string);
+ }
+
+ // clear _12h flag if hour is <= 12
+ if (getParsingFlags(config).bigHour === true &&
+ config._a[HOUR] <= 12 &&
+ config._a[HOUR] > 0) {
+ getParsingFlags(config).bigHour = undefined;
+ }
+ // handle meridiem
+ config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);
+
+ configFromArray(config);
+ checkOverflow(config);
+ }
+
+
+ function meridiemFixWrap (locale, hour, meridiem) {
+ var isPm;
+
+ if (meridiem == null) {
+ // nothing to do
+ return hour;
+ }
+ if (locale.meridiemHour != null) {
+ return locale.meridiemHour(hour, meridiem);
+ } else if (locale.isPM != null) {
+ // Fallback
+ isPm = locale.isPM(meridiem);
+ if (isPm && hour < 12) {
+ hour += 12;
+ }
+ if (!isPm && hour === 12) {
+ hour = 0;
+ }
+ return hour;
+ } else {
+ // this is not supposed to happen
+ return hour;
+ }
+ }
+
+ function configFromStringAndArray(config) {
+ var tempConfig,
+ bestMoment,
+
+ scoreToBeat,
+ i,
+ currentScore;
+
+ if (config._f.length === 0) {
+ getParsingFlags(config).invalidFormat = true;
+ config._d = new Date(NaN);
+ return;
+ }
+
+ for (i = 0; i < config._f.length; i++) {
+ currentScore = 0;
+ tempConfig = copyConfig({}, config);
+ if (config._useUTC != null) {
+ tempConfig._useUTC = config._useUTC;
+ }
+ tempConfig._f = config._f[i];
+ configFromStringAndFormat(tempConfig);
+
+ if (!valid__isValid(tempConfig)) {
+ continue;
+ }
+
+ // if there is any input that was not parsed add a penalty for that format
+ currentScore += getParsingFlags(tempConfig).charsLeftOver;
+
+ //or tokens
+ currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10;
+
+ getParsingFlags(tempConfig).score = currentScore;
+
+ if (scoreToBeat == null || currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempConfig;
+ }
+ }
+
+ extend(config, bestMoment || tempConfig);
+ }
+
+ function configFromObject(config) {
+ if (config._d) {
+ return;
+ }
+
+ var i = normalizeObjectUnits(config._i);
+ config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond];
+
+ configFromArray(config);
+ }
+
+ function createFromConfig (config) {
+ var res = new Moment(checkOverflow(prepareConfig(config)));
+ if (res._nextDay) {
+ // Adding is smart enough around DST
+ res.add(1, 'd');
+ res._nextDay = undefined;
+ }
+
+ return res;
+ }
+
+ function prepareConfig (config) {
+ var input = config._i,
+ format = config._f;
+
+ config._locale = config._locale || locale_locales__getLocale(config._l);
+
+ if (input === null || (format === undefined && input === '')) {
+ return valid__createInvalid({nullInput: true});
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = config._locale.preparse(input);
+ }
+
+ if (isMoment(input)) {
+ return new Moment(checkOverflow(input));
+ } else if (isArray(format)) {
+ configFromStringAndArray(config);
+ } else if (format) {
+ configFromStringAndFormat(config);
+ } else if (isDate(input)) {
+ config._d = input;
+ } else {
+ configFromInput(config);
+ }
+
+ return config;
+ }
+
+ function configFromInput(config) {
+ var input = config._i;
+ if (input === undefined) {
+ config._d = new Date();
+ } else if (isDate(input)) {
+ config._d = new Date(+input);
+ } else if (typeof input === 'string') {
+ configFromString(config);
+ } else if (isArray(input)) {
+ config._a = map(input.slice(0), function (obj) {
+ return parseInt(obj, 10);
+ });
+ configFromArray(config);
+ } else if (typeof(input) === 'object') {
+ configFromObject(config);
+ } else if (typeof(input) === 'number') {
+ // from milliseconds
+ config._d = new Date(input);
+ } else {
+ utils_hooks__hooks.createFromInputFallback(config);
+ }
+ }
+
+ function createLocalOrUTC (input, format, locale, strict, isUTC) {
+ var c = {};
+
+ if (typeof(locale) === 'boolean') {
+ strict = locale;
+ locale = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c._isAMomentObject = true;
+ c._useUTC = c._isUTC = isUTC;
+ c._l = locale;
+ c._i = input;
+ c._f = format;
+ c._strict = strict;
+
+ return createFromConfig(c);
+ }
+
+ function local__createLocal (input, format, locale, strict) {
+ return createLocalOrUTC(input, format, locale, strict, false);
+ }
+
+ var prototypeMin = deprecate(
+ 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548',
+ function () {
+ var other = local__createLocal.apply(null, arguments);
+ return other < this ? this : other;
+ }
+ );
+
+ var prototypeMax = deprecate(
+ 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548',
+ function () {
+ var other = local__createLocal.apply(null, arguments);
+ return other > this ? this : other;
+ }
+ );
+
+ // Pick a moment m from moments so that m[fn](other) is true for all
+ // other. This relies on the function fn to be transitive.
+ //
+ // moments should either be an array of moment objects or an array, whose
+ // first element is an array of moment objects.
+ function pickBy(fn, moments) {
+ var res, i;
+ if (moments.length === 1 && isArray(moments[0])) {
+ moments = moments[0];
+ }
+ if (!moments.length) {
+ return local__createLocal();
+ }
+ res = moments[0];
+ for (i = 1; i < moments.length; ++i) {
+ if (!moments[i].isValid() || moments[i][fn](res)) {
+ res = moments[i];
+ }
+ }
+ return res;
+ }
+
+ // TODO: Use [].sort instead?
+ function min () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isBefore', args);
+ }
+
+ function max () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isAfter', args);
+ }
+
+ function Duration (duration) {
+ var normalizedInput = normalizeObjectUnits(duration),
+ years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
+ months = normalizedInput.month || 0,
+ weeks = normalizedInput.week || 0,
+ days = normalizedInput.day || 0,
+ hours = normalizedInput.hour || 0,
+ minutes = normalizedInput.minute || 0,
+ seconds = normalizedInput.second || 0,
+ milliseconds = normalizedInput.millisecond || 0;
+
+ // representation for dateAddRemove
+ this._milliseconds = +milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 36e5; // 1000 * 60 * 60
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = +days +
+ weeks * 7;
+ // It is impossible translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = +months +
+ quarters * 3 +
+ years * 12;
+
+ this._data = {};
+
+ this._locale = locale_locales__getLocale();
+
+ this._bubble();
+ }
+
+ function isDuration (obj) {
+ return obj instanceof Duration;
+ }
+
+ function offset (token, separator) {
+ addFormatToken(token, 0, 0, function () {
+ var offset = this.utcOffset();
+ var sign = '+';
+ if (offset < 0) {
+ offset = -offset;
+ sign = '-';
+ }
+ return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2);
+ });
+ }
+
+ offset('Z', ':');
+ offset('ZZ', '');
+
+ // PARSING
+
+ addRegexToken('Z', matchOffset);
+ addRegexToken('ZZ', matchOffset);
+ addParseToken(['Z', 'ZZ'], function (input, array, config) {
+ config._useUTC = true;
+ config._tzm = offsetFromString(input);
+ });
+
+ // HELPERS
+
+ // timezone chunker
+ // '+10:00' > ['10', '00']
+ // '-1530' > ['-15', '30']
+ var chunkOffset = /([\+\-]|\d\d)/gi;
+
+ function offsetFromString(string) {
+ var matches = ((string || '').match(matchOffset) || []);
+ var chunk = matches[matches.length - 1] || [];
+ var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0];
+ var minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+ return parts[0] === '+' ? minutes : -minutes;
+ }
+
+ // Return a moment from input, that is local/utc/zone equivalent to model.
+ function cloneWithOffset(input, model) {
+ var res, diff;
+ if (model._isUTC) {
+ res = model.clone();
+ diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res);
+ // Use low-level api, because this fn is low-level api.
+ res._d.setTime(+res._d + diff);
+ utils_hooks__hooks.updateOffset(res, false);
+ return res;
+ } else {
+ return local__createLocal(input).local();
+ }
+ }
+
+ function getDateOffset (m) {
+ // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+ // https://github.com/moment/moment/pull/1871
+ return -Math.round(m._d.getTimezoneOffset() / 15) * 15;
+ }
+
+ // HOOKS
+
+ // This function will be called whenever a moment is mutated.
+ // It is intended to keep the offset in sync with the timezone.
+ utils_hooks__hooks.updateOffset = function () {};
+
+ // MOMENTS
+
+ // keepLocalTime = true means only change the timezone, without
+ // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+ // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+ // +0200, so we adjust the time as needed, to be valid.
+ //
+ // Keeping the time actually adds/subtracts (one hour)
+ // from the actual represented time. That is why we call updateOffset
+ // a second time. In case it wants us to change the offset again
+ // _changeInProgress == true case, then we have to adjust, because
+ // there is no such time in the given timezone.
+ function getSetOffset (input, keepLocalTime) {
+ var offset = this._offset || 0,
+ localAdjust;
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = offsetFromString(input);
+ }
+ if (Math.abs(input) < 16) {
+ input = input * 60;
+ }
+ if (!this._isUTC && keepLocalTime) {
+ localAdjust = getDateOffset(this);
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (localAdjust != null) {
+ this.add(localAdjust, 'm');
+ }
+ if (offset !== input) {
+ if (!keepLocalTime || this._changeInProgress) {
+ add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false);
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ utils_hooks__hooks.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
+ }
+ return this;
+ } else {
+ return this._isUTC ? offset : getDateOffset(this);
+ }
+ }
+
+ function getSetZone (input, keepLocalTime) {
+ if (input != null) {
+ if (typeof input !== 'string') {
+ input = -input;
+ }
+
+ this.utcOffset(input, keepLocalTime);
+
+ return this;
+ } else {
+ return -this.utcOffset();
+ }
+ }
+
+ function setOffsetToUTC (keepLocalTime) {
+ return this.utcOffset(0, keepLocalTime);
+ }
+
+ function setOffsetToLocal (keepLocalTime) {
+ if (this._isUTC) {
+ this.utcOffset(0, keepLocalTime);
+ this._isUTC = false;
+
+ if (keepLocalTime) {
+ this.subtract(getDateOffset(this), 'm');
+ }
+ }
+ return this;
+ }
+
+ function setOffsetToParsedOffset () {
+ if (this._tzm) {
+ this.utcOffset(this._tzm);
+ } else if (typeof this._i === 'string') {
+ this.utcOffset(offsetFromString(this._i));
+ }
+ return this;
+ }
+
+ function hasAlignedHourOffset (input) {
+ input = input ? local__createLocal(input).utcOffset() : 0;
+
+ return (this.utcOffset() - input) % 60 === 0;
+ }
+
+ function isDaylightSavingTime () {
+ return (
+ this.utcOffset() > this.clone().month(0).utcOffset() ||
+ this.utcOffset() > this.clone().month(5).utcOffset()
+ );
+ }
+
+ function isDaylightSavingTimeShifted () {
+ if (typeof this._isDSTShifted !== 'undefined') {
+ return this._isDSTShifted;
+ }
+
+ var c = {};
+
+ copyConfig(c, this);
+ c = prepareConfig(c);
+
+ if (c._a) {
+ var other = c._isUTC ? create_utc__createUTC(c._a) : local__createLocal(c._a);
+ this._isDSTShifted = this.isValid() &&
+ compareArrays(c._a, other.toArray()) > 0;
+ } else {
+ this._isDSTShifted = false;
+ }
+
+ return this._isDSTShifted;
+ }
+
+ function isLocal () {
+ return !this._isUTC;
+ }
+
+ function isUtcOffset () {
+ return this._isUTC;
+ }
+
+ function isUtc () {
+ return this._isUTC && this._offset === 0;
+ }
+
+ var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/;
+
+ // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+ // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+ var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;
+
+ function create__createDuration (input, key) {
+ var duration = input,
+ // matching against regexp is expensive, do it on demand
+ match = null,
+ sign,
+ ret,
+ diffRes;
+
+ if (isDuration(input)) {
+ duration = {
+ ms : input._milliseconds,
+ d : input._days,
+ M : input._months
+ };
+ } else if (typeof input === 'number') {
+ duration = {};
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ } else if (!!(match = aspNetRegex.exec(input))) {
+ sign = (match[1] === '-') ? -1 : 1;
+ duration = {
+ y : 0,
+ d : toInt(match[DATE]) * sign,
+ h : toInt(match[HOUR]) * sign,
+ m : toInt(match[MINUTE]) * sign,
+ s : toInt(match[SECOND]) * sign,
+ ms : toInt(match[MILLISECOND]) * sign
+ };
+ } else if (!!(match = create__isoRegex.exec(input))) {
+ sign = (match[1] === '-') ? -1 : 1;
+ duration = {
+ y : parseIso(match[2], sign),
+ M : parseIso(match[3], sign),
+ d : parseIso(match[4], sign),
+ h : parseIso(match[5], sign),
+ m : parseIso(match[6], sign),
+ s : parseIso(match[7], sign),
+ w : parseIso(match[8], sign)
+ };
+ } else if (duration == null) {// checks for null or undefined
+ duration = {};
+ } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) {
+ diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to));
+
+ duration = {};
+ duration.ms = diffRes.milliseconds;
+ duration.M = diffRes.months;
+ }
+
+ ret = new Duration(duration);
+
+ if (isDuration(input) && hasOwnProp(input, '_locale')) {
+ ret._locale = input._locale;
+ }
+
+ return ret;
+ }
+
+ create__createDuration.fn = Duration.prototype;
+
+ function parseIso (inp, sign) {
+ // We'd normally use ~~inp for this, but unfortunately it also
+ // converts floats to ints.
+ // inp may be undefined, so careful calling replace on it.
+ var res = inp && parseFloat(inp.replace(',', '.'));
+ // apply sign while we're at it
+ return (isNaN(res) ? 0 : res) * sign;
+ }
+
+ function positiveMomentsDifference(base, other) {
+ var res = {milliseconds: 0, months: 0};
+
+ res.months = other.month() - base.month() +
+ (other.year() - base.year()) * 12;
+ if (base.clone().add(res.months, 'M').isAfter(other)) {
+ --res.months;
+ }
+
+ res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
+
+ return res;
+ }
+
+ function momentsDifference(base, other) {
+ var res;
+ other = cloneWithOffset(other, base);
+ if (base.isBefore(other)) {
+ res = positiveMomentsDifference(base, other);
+ } else {
+ res = positiveMomentsDifference(other, base);
+ res.milliseconds = -res.milliseconds;
+ res.months = -res.months;
+ }
+
+ return res;
+ }
+
+ function createAdder(direction, name) {
+ return function (val, period) {
+ var dur, tmp;
+ //invert the arguments, but complain about it
+ if (period !== null && !isNaN(+period)) {
+ deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).');
+ tmp = val; val = period; period = tmp;
+ }
+
+ val = typeof val === 'string' ? +val : val;
+ dur = create__createDuration(val, period);
+ add_subtract__addSubtract(this, dur, direction);
+ return this;
+ };
+ }
+
+ function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = duration._days,
+ months = duration._months;
+ updateOffset = updateOffset == null ? true : updateOffset;
+
+ if (milliseconds) {
+ mom._d.setTime(+mom._d + milliseconds * isAdding);
+ }
+ if (days) {
+ get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding);
+ }
+ if (months) {
+ setMonth(mom, get_set__get(mom, 'Month') + months * isAdding);
+ }
+ if (updateOffset) {
+ utils_hooks__hooks.updateOffset(mom, days || months);
+ }
+ }
+
+ var add_subtract__add = createAdder(1, 'add');
+ var add_subtract__subtract = createAdder(-1, 'subtract');
+
+ function moment_calendar__calendar (time, formats) {
+ // We want to compare the start of today, vs this.
+ // Getting start-of-today depends on whether we're local/utc/offset or not.
+ var now = time || local__createLocal(),
+ sod = cloneWithOffset(now, this).startOf('day'),
+ diff = this.diff(sod, 'days', true),
+ format = diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+ return this.format(formats && formats[format] || this.localeData().calendar(format, this, local__createLocal(now)));
+ }
+
+ function clone () {
+ return new Moment(this);
+ }
+
+ function isAfter (input, units) {
+ var inputMs;
+ units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
+ if (units === 'millisecond') {
+ input = isMoment(input) ? input : local__createLocal(input);
+ return +this > +input;
+ } else {
+ inputMs = isMoment(input) ? +input : +local__createLocal(input);
+ return inputMs < +this.clone().startOf(units);
+ }
+ }
+
+ function isBefore (input, units) {
+ var inputMs;
+ units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond');
+ if (units === 'millisecond') {
+ input = isMoment(input) ? input : local__createLocal(input);
+ return +this < +input;
+ } else {
+ inputMs = isMoment(input) ? +input : +local__createLocal(input);
+ return +this.clone().endOf(units) < inputMs;
+ }
+ }
+
+ function isBetween (from, to, units) {
+ return this.isAfter(from, units) && this.isBefore(to, units);
+ }
+
+ function isSame (input, units) {
+ var inputMs;
+ units = normalizeUnits(units || 'millisecond');
+ if (units === 'millisecond') {
+ input = isMoment(input) ? input : local__createLocal(input);
+ return +this === +input;
+ } else {
+ inputMs = +local__createLocal(input);
+ return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units));
+ }
+ }
+
+ function diff (input, units, asFloat) {
+ var that = cloneWithOffset(input, this),
+ zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4,
+ delta, output;
+
+ units = normalizeUnits(units);
+
+ if (units === 'year' || units === 'month' || units === 'quarter') {
+ output = monthDiff(this, that);
+ if (units === 'quarter') {
+ output = output / 3;
+ } else if (units === 'year') {
+ output = output / 12;
+ }
+ } else {
+ delta = this - that;
+ output = units === 'second' ? delta / 1e3 : // 1000
+ units === 'minute' ? delta / 6e4 : // 1000 * 60
+ units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+ units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+ delta;
+ }
+ return asFloat ? output : absFloor(output);
+ }
+
+ function monthDiff (a, b) {
+ // difference in months
+ var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()),
+ // b is in (anchor - 1 month, anchor + 1 month)
+ anchor = a.clone().add(wholeMonthDiff, 'months'),
+ anchor2, adjust;
+
+ if (b - anchor < 0) {
+ anchor2 = a.clone().add(wholeMonthDiff - 1, 'months');
+ // linear across the month
+ adjust = (b - anchor) / (anchor - anchor2);
+ } else {
+ anchor2 = a.clone().add(wholeMonthDiff + 1, 'months');
+ // linear across the month
+ adjust = (b - anchor) / (anchor2 - anchor);
+ }
+
+ return -(wholeMonthDiff + adjust);
+ }
+
+ utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ';
+
+ function toString () {
+ return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+ }
+
+ function moment_format__toISOString () {
+ var m = this.clone().utc();
+ if (0 < m.year() && m.year() <= 9999) {
+ if ('function' === typeof Date.prototype.toISOString) {
+ // native implementation is ~50x faster, use it when we can
+ return this.toDate().toISOString();
+ } else {
+ return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ }
+ } else {
+ return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ }
+ }
+
+ function format (inputString) {
+ var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat);
+ return this.localeData().postformat(output);
+ }
+
+ function from (time, withoutSuffix) {
+ if (!this.isValid()) {
+ return this.localeData().invalidDate();
+ }
+ return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
+ }
+
+ function fromNow (withoutSuffix) {
+ return this.from(local__createLocal(), withoutSuffix);
+ }
+
+ function to (time, withoutSuffix) {
+ if (!this.isValid()) {
+ return this.localeData().invalidDate();
+ }
+ return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix);
+ }
+
+ function toNow (withoutSuffix) {
+ return this.to(local__createLocal(), withoutSuffix);
+ }
+
+ function locale (key) {
+ var newLocaleData;
+
+ if (key === undefined) {
+ return this._locale._abbr;
+ } else {
+ newLocaleData = locale_locales__getLocale(key);
+ if (newLocaleData != null) {
+ this._locale = newLocaleData;
+ }
+ return this;
+ }
+ }
+
+ var lang = deprecate(
+ 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+ function (key) {
+ if (key === undefined) {
+ return this.localeData();
+ } else {
+ return this.locale(key);
+ }
+ }
+ );
+
+ function localeData () {
+ return this._locale;
+ }
+
+ function startOf (units) {
+ units = normalizeUnits(units);
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'quarter':
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'isoWeek':
+ case 'day':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ }
+
+ // weeks are a special case
+ if (units === 'week') {
+ this.weekday(0);
+ }
+ if (units === 'isoWeek') {
+ this.isoWeekday(1);
+ }
+
+ // quarters are also special
+ if (units === 'quarter') {
+ this.month(Math.floor(this.month() / 3) * 3);
+ }
+
+ return this;
+ }
+
+ function endOf (units) {
+ units = normalizeUnits(units);
+ if (units === undefined || units === 'millisecond') {
+ return this;
+ }
+ return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
+ }
+
+ function to_type__valueOf () {
+ return +this._d - ((this._offset || 0) * 60000);
+ }
+
+ function unix () {
+ return Math.floor(+this / 1000);
+ }
+
+ function toDate () {
+ return this._offset ? new Date(+this) : this._d;
+ }
+
+ function toArray () {
+ var m = this;
+ return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()];
+ }
+
+ function toObject () {
+ var m = this;
+ return {
+ years: m.year(),
+ months: m.month(),
+ date: m.date(),
+ hours: m.hours(),
+ minutes: m.minutes(),
+ seconds: m.seconds(),
+ milliseconds: m.milliseconds()
+ };
+ }
+
+ function moment_valid__isValid () {
+ return valid__isValid(this);
+ }
+
+ function parsingFlags () {
+ return extend({}, getParsingFlags(this));
+ }
+
+ function invalidAt () {
+ return getParsingFlags(this).overflow;
+ }
+
+ addFormatToken(0, ['gg', 2], 0, function () {
+ return this.weekYear() % 100;
+ });
+
+ addFormatToken(0, ['GG', 2], 0, function () {
+ return this.isoWeekYear() % 100;
+ });
+
+ function addWeekYearFormatToken (token, getter) {
+ addFormatToken(0, [token, token.length], 0, getter);
+ }
+
+ addWeekYearFormatToken('gggg', 'weekYear');
+ addWeekYearFormatToken('ggggg', 'weekYear');
+ addWeekYearFormatToken('GGGG', 'isoWeekYear');
+ addWeekYearFormatToken('GGGGG', 'isoWeekYear');
+
+ // ALIASES
+
+ addUnitAlias('weekYear', 'gg');
+ addUnitAlias('isoWeekYear', 'GG');
+
+ // PARSING
+
+ addRegexToken('G', matchSigned);
+ addRegexToken('g', matchSigned);
+ addRegexToken('GG', match1to2, match2);
+ addRegexToken('gg', match1to2, match2);
+ addRegexToken('GGGG', match1to4, match4);
+ addRegexToken('gggg', match1to4, match4);
+ addRegexToken('GGGGG', match1to6, match6);
+ addRegexToken('ggggg', match1to6, match6);
+
+ addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) {
+ week[token.substr(0, 2)] = toInt(input);
+ });
+
+ addWeekParseToken(['gg', 'GG'], function (input, week, config, token) {
+ week[token] = utils_hooks__hooks.parseTwoDigitYear(input);
+ });
+
+ // HELPERS
+
+ function weeksInYear(year, dow, doy) {
+ return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week;
+ }
+
+ // MOMENTS
+
+ function getSetWeekYear (input) {
+ var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year;
+ return input == null ? year : this.add((input - year), 'y');
+ }
+
+ function getSetISOWeekYear (input) {
+ var year = weekOfYear(this, 1, 4).year;
+ return input == null ? year : this.add((input - year), 'y');
+ }
+
+ function getISOWeeksInYear () {
+ return weeksInYear(this.year(), 1, 4);
+ }
+
+ function getWeeksInYear () {
+ var weekInfo = this.localeData()._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+ }
+
+ addFormatToken('Q', 0, 0, 'quarter');
+
+ // ALIASES
+
+ addUnitAlias('quarter', 'Q');
+
+ // PARSING
+
+ addRegexToken('Q', match1);
+ addParseToken('Q', function (input, array) {
+ array[MONTH] = (toInt(input) - 1) * 3;
+ });
+
+ // MOMENTS
+
+ function getSetQuarter (input) {
+ return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+ }
+
+ addFormatToken('D', ['DD', 2], 'Do', 'date');
+
+ // ALIASES
+
+ addUnitAlias('date', 'D');
+
+ // PARSING
+
+ addRegexToken('D', match1to2);
+ addRegexToken('DD', match1to2, match2);
+ addRegexToken('Do', function (isStrict, locale) {
+ return isStrict ? locale._ordinalParse : locale._ordinalParseLenient;
+ });
+
+ addParseToken(['D', 'DD'], DATE);
+ addParseToken('Do', function (input, array) {
+ array[DATE] = toInt(input.match(match1to2)[0], 10);
+ });
+
+ // MOMENTS
+
+ var getSetDayOfMonth = makeGetSet('Date', true);
+
+ addFormatToken('d', 0, 'do', 'day');
+
+ addFormatToken('dd', 0, 0, function (format) {
+ return this.localeData().weekdaysMin(this, format);
+ });
+
+ addFormatToken('ddd', 0, 0, function (format) {
+ return this.localeData().weekdaysShort(this, format);
+ });
+
+ addFormatToken('dddd', 0, 0, function (format) {
+ return this.localeData().weekdays(this, format);
+ });
+
+ addFormatToken('e', 0, 0, 'weekday');
+ addFormatToken('E', 0, 0, 'isoWeekday');
+
+ // ALIASES
+
+ addUnitAlias('day', 'd');
+ addUnitAlias('weekday', 'e');
+ addUnitAlias('isoWeekday', 'E');
+
+ // PARSING
+
+ addRegexToken('d', match1to2);
+ addRegexToken('e', match1to2);
+ addRegexToken('E', match1to2);
+ addRegexToken('dd', matchWord);
+ addRegexToken('ddd', matchWord);
+ addRegexToken('dddd', matchWord);
+
+ addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) {
+ var weekday = config._locale.weekdaysParse(input);
+ // if we didn't get a weekday name, mark the date as invalid
+ if (weekday != null) {
+ week.d = weekday;
+ } else {
+ getParsingFlags(config).invalidWeekday = input;
+ }
+ });
+
+ addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) {
+ week[token] = toInt(input);
+ });
+
+ // HELPERS
+
+ function parseWeekday(input, locale) {
+ if (typeof input !== 'string') {
+ return input;
+ }
+
+ if (!isNaN(input)) {
+ return parseInt(input, 10);
+ }
+
+ input = locale.weekdaysParse(input);
+ if (typeof input === 'number') {
+ return input;
+ }
+
+ return null;
+ }
+
+ // LOCALES
+
+ var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_');
+ function localeWeekdays (m) {
+ return this._weekdays[m.day()];
+ }
+
+ var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_');
+ function localeWeekdaysShort (m) {
+ return this._weekdaysShort[m.day()];
+ }
+
+ var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_');
+ function localeWeekdaysMin (m) {
+ return this._weekdaysMin[m.day()];
+ }
+
+ function localeWeekdaysParse (weekdayName) {
+ var i, mom, regex;
+
+ this._weekdaysParse = this._weekdaysParse || [];
+
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ if (!this._weekdaysParse[i]) {
+ mom = local__createLocal([2000, 1]).day(i);
+ regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+ }
+
+ // MOMENTS
+
+ function getSetDayOfWeek (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ if (input != null) {
+ input = parseWeekday(input, this.localeData());
+ return this.add(input - day, 'd');
+ } else {
+ return day;
+ }
+ }
+
+ function getSetLocaleDayOfWeek (input) {
+ var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+ return input == null ? weekday : this.add(input - weekday, 'd');
+ }
+
+ function getSetISODayOfWeek (input) {
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
+ }
+
+ addFormatToken('H', ['HH', 2], 0, 'hour');
+ addFormatToken('h', ['hh', 2], 0, function () {
+ return this.hours() % 12 || 12;
+ });
+
+ function meridiem (token, lowercase) {
+ addFormatToken(token, 0, 0, function () {
+ return this.localeData().meridiem(this.hours(), this.minutes(), lowercase);
+ });
+ }
+
+ meridiem('a', true);
+ meridiem('A', false);
+
+ // ALIASES
+
+ addUnitAlias('hour', 'h');
+
+ // PARSING
+
+ function matchMeridiem (isStrict, locale) {
+ return locale._meridiemParse;
+ }
+
+ addRegexToken('a', matchMeridiem);
+ addRegexToken('A', matchMeridiem);
+ addRegexToken('H', match1to2);
+ addRegexToken('h', match1to2);
+ addRegexToken('HH', match1to2, match2);
+ addRegexToken('hh', match1to2, match2);
+
+ addParseToken(['H', 'HH'], HOUR);
+ addParseToken(['a', 'A'], function (input, array, config) {
+ config._isPm = config._locale.isPM(input);
+ config._meridiem = input;
+ });
+ addParseToken(['h', 'hh'], function (input, array, config) {
+ array[HOUR] = toInt(input);
+ getParsingFlags(config).bigHour = true;
+ });
+
+ // LOCALES
+
+ function localeIsPM (input) {
+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+ // Using charAt should be more compatible.
+ return ((input + '').toLowerCase().charAt(0) === 'p');
+ }
+
+ var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i;
+ function localeMeridiem (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+ }
+
+
+ // MOMENTS
+
+ // Setting the hour should keep the time, because the user explicitly
+ // specified which hour he wants. So trying to maintain the same hour (in
+ // a new timezone) makes sense. Adding/subtracting hours does not follow
+ // this rule.
+ var getSetHour = makeGetSet('Hours', true);
+
+ addFormatToken('m', ['mm', 2], 0, 'minute');
+
+ // ALIASES
+
+ addUnitAlias('minute', 'm');
+
+ // PARSING
+
+ addRegexToken('m', match1to2);
+ addRegexToken('mm', match1to2, match2);
+ addParseToken(['m', 'mm'], MINUTE);
+
+ // MOMENTS
+
+ var getSetMinute = makeGetSet('Minutes', false);
+
+ addFormatToken('s', ['ss', 2], 0, 'second');
+
+ // ALIASES
+
+ addUnitAlias('second', 's');
+
+ // PARSING
+
+ addRegexToken('s', match1to2);
+ addRegexToken('ss', match1to2, match2);
+ addParseToken(['s', 'ss'], SECOND);
+
+ // MOMENTS
+
+ var getSetSecond = makeGetSet('Seconds', false);
+
+ addFormatToken('S', 0, 0, function () {
+ return ~~(this.millisecond() / 100);
+ });
+
+ addFormatToken(0, ['SS', 2], 0, function () {
+ return ~~(this.millisecond() / 10);
+ });
+
+ addFormatToken(0, ['SSS', 3], 0, 'millisecond');
+ addFormatToken(0, ['SSSS', 4], 0, function () {
+ return this.millisecond() * 10;
+ });
+ addFormatToken(0, ['SSSSS', 5], 0, function () {
+ return this.millisecond() * 100;
+ });
+ addFormatToken(0, ['SSSSSS', 6], 0, function () {
+ return this.millisecond() * 1000;
+ });
+ addFormatToken(0, ['SSSSSSS', 7], 0, function () {
+ return this.millisecond() * 10000;
+ });
+ addFormatToken(0, ['SSSSSSSS', 8], 0, function () {
+ return this.millisecond() * 100000;
+ });
+ addFormatToken(0, ['SSSSSSSSS', 9], 0, function () {
+ return this.millisecond() * 1000000;
+ });
+
+
+ // ALIASES
+
+ addUnitAlias('millisecond', 'ms');
+
+ // PARSING
+
+ addRegexToken('S', match1to3, match1);
+ addRegexToken('SS', match1to3, match2);
+ addRegexToken('SSS', match1to3, match3);
+
+ var token;
+ for (token = 'SSSS'; token.length <= 9; token += 'S') {
+ addRegexToken(token, matchUnsigned);
+ }
+
+ function parseMs(input, array) {
+ array[MILLISECOND] = toInt(('0.' + input) * 1000);
+ }
+
+ for (token = 'S'; token.length <= 9; token += 'S') {
+ addParseToken(token, parseMs);
+ }
+ // MOMENTS
+
+ var getSetMillisecond = makeGetSet('Milliseconds', false);
+
+ addFormatToken('z', 0, 0, 'zoneAbbr');
+ addFormatToken('zz', 0, 0, 'zoneName');
+
+ // MOMENTS
+
+ function getZoneAbbr () {
+ return this._isUTC ? 'UTC' : '';
+ }
+
+ function getZoneName () {
+ return this._isUTC ? 'Coordinated Universal Time' : '';
+ }
+
+ var momentPrototype__proto = Moment.prototype;
+
+ momentPrototype__proto.add = add_subtract__add;
+ momentPrototype__proto.calendar = moment_calendar__calendar;
+ momentPrototype__proto.clone = clone;
+ momentPrototype__proto.diff = diff;
+ momentPrototype__proto.endOf = endOf;
+ momentPrototype__proto.format = format;
+ momentPrototype__proto.from = from;
+ momentPrototype__proto.fromNow = fromNow;
+ momentPrototype__proto.to = to;
+ momentPrototype__proto.toNow = toNow;
+ momentPrototype__proto.get = getSet;
+ momentPrototype__proto.invalidAt = invalidAt;
+ momentPrototype__proto.isAfter = isAfter;
+ momentPrototype__proto.isBefore = isBefore;
+ momentPrototype__proto.isBetween = isBetween;
+ momentPrototype__proto.isSame = isSame;
+ momentPrototype__proto.isValid = moment_valid__isValid;
+ momentPrototype__proto.lang = lang;
+ momentPrototype__proto.locale = locale;
+ momentPrototype__proto.localeData = localeData;
+ momentPrototype__proto.max = prototypeMax;
+ momentPrototype__proto.min = prototypeMin;
+ momentPrototype__proto.parsingFlags = parsingFlags;
+ momentPrototype__proto.set = getSet;
+ momentPrototype__proto.startOf = startOf;
+ momentPrototype__proto.subtract = add_subtract__subtract;
+ momentPrototype__proto.toArray = toArray;
+ momentPrototype__proto.toObject = toObject;
+ momentPrototype__proto.toDate = toDate;
+ momentPrototype__proto.toISOString = moment_format__toISOString;
+ momentPrototype__proto.toJSON = moment_format__toISOString;
+ momentPrototype__proto.toString = toString;
+ momentPrototype__proto.unix = unix;
+ momentPrototype__proto.valueOf = to_type__valueOf;
+
+ // Year
+ momentPrototype__proto.year = getSetYear;
+ momentPrototype__proto.isLeapYear = getIsLeapYear;
+
+ // Week Year
+ momentPrototype__proto.weekYear = getSetWeekYear;
+ momentPrototype__proto.isoWeekYear = getSetISOWeekYear;
+
+ // Quarter
+ momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter;
+
+ // Month
+ momentPrototype__proto.month = getSetMonth;
+ momentPrototype__proto.daysInMonth = getDaysInMonth;
+
+ // Week
+ momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek;
+ momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek;
+ momentPrototype__proto.weeksInYear = getWeeksInYear;
+ momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear;
+
+ // Day
+ momentPrototype__proto.date = getSetDayOfMonth;
+ momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek;
+ momentPrototype__proto.weekday = getSetLocaleDayOfWeek;
+ momentPrototype__proto.isoWeekday = getSetISODayOfWeek;
+ momentPrototype__proto.dayOfYear = getSetDayOfYear;
+
+ // Hour
+ momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour;
+
+ // Minute
+ momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute;
+
+ // Second
+ momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond;
+
+ // Millisecond
+ momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond;
+
+ // Offset
+ momentPrototype__proto.utcOffset = getSetOffset;
+ momentPrototype__proto.utc = setOffsetToUTC;
+ momentPrototype__proto.local = setOffsetToLocal;
+ momentPrototype__proto.parseZone = setOffsetToParsedOffset;
+ momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset;
+ momentPrototype__proto.isDST = isDaylightSavingTime;
+ momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted;
+ momentPrototype__proto.isLocal = isLocal;
+ momentPrototype__proto.isUtcOffset = isUtcOffset;
+ momentPrototype__proto.isUtc = isUtc;
+ momentPrototype__proto.isUTC = isUtc;
+
+ // Timezone
+ momentPrototype__proto.zoneAbbr = getZoneAbbr;
+ momentPrototype__proto.zoneName = getZoneName;
+
+ // Deprecations
+ momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth);
+ momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth);
+ momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear);
+ momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone);
+
+ var momentPrototype = momentPrototype__proto;
+
+ function moment__createUnix (input) {
+ return local__createLocal(input * 1000);
+ }
+
+ function moment__createInZone () {
+ return local__createLocal.apply(null, arguments).parseZone();
+ }
+
+ var defaultCalendar = {
+ sameDay : '[Today at] LT',
+ nextDay : '[Tomorrow at] LT',
+ nextWeek : 'dddd [at] LT',
+ lastDay : '[Yesterday at] LT',
+ lastWeek : '[Last] dddd [at] LT',
+ sameElse : 'L'
+ };
+
+ function locale_calendar__calendar (key, mom, now) {
+ var output = this._calendar[key];
+ return typeof output === 'function' ? output.call(mom, now) : output;
+ }
+
+ var defaultLongDateFormat = {
+ LTS : 'h:mm:ss A',
+ LT : 'h:mm A',
+ L : 'MM/DD/YYYY',
+ LL : 'MMMM D, YYYY',
+ LLL : 'MMMM D, YYYY h:mm A',
+ LLLL : 'dddd, MMMM D, YYYY h:mm A'
+ };
+
+ function longDateFormat (key) {
+ var format = this._longDateFormat[key],
+ formatUpper = this._longDateFormat[key.toUpperCase()];
+
+ if (format || !formatUpper) {
+ return format;
+ }
+
+ this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+
+ return this._longDateFormat[key];
+ }
+
+ var defaultInvalidDate = 'Invalid date';
+
+ function invalidDate () {
+ return this._invalidDate;
+ }
+
+ var defaultOrdinal = '%d';
+ var defaultOrdinalParse = /\d{1,2}/;
+
+ function ordinal (number) {
+ return this._ordinal.replace('%d', number);
+ }
+
+ function preParsePostFormat (string) {
+ return string;
+ }
+
+ var defaultRelativeTime = {
+ future : 'in %s',
+ past : '%s ago',
+ s : 'a few seconds',
+ m : 'a minute',
+ mm : '%d minutes',
+ h : 'an hour',
+ hh : '%d hours',
+ d : 'a day',
+ dd : '%d days',
+ M : 'a month',
+ MM : '%d months',
+ y : 'a year',
+ yy : '%d years'
+ };
+
+ function relative__relativeTime (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (typeof output === 'function') ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+ }
+
+ function pastFuture (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+ }
+
+ function locale_set__set (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (typeof prop === 'function') {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ // Lenient ordinal parsing accepts just a number in addition to
+ // number + (possibly) stuff coming from _ordinalParseLenient.
+ this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source);
+ }
+
+ var prototype__proto = Locale.prototype;
+
+ prototype__proto._calendar = defaultCalendar;
+ prototype__proto.calendar = locale_calendar__calendar;
+ prototype__proto._longDateFormat = defaultLongDateFormat;
+ prototype__proto.longDateFormat = longDateFormat;
+ prototype__proto._invalidDate = defaultInvalidDate;
+ prototype__proto.invalidDate = invalidDate;
+ prototype__proto._ordinal = defaultOrdinal;
+ prototype__proto.ordinal = ordinal;
+ prototype__proto._ordinalParse = defaultOrdinalParse;
+ prototype__proto.preparse = preParsePostFormat;
+ prototype__proto.postformat = preParsePostFormat;
+ prototype__proto._relativeTime = defaultRelativeTime;
+ prototype__proto.relativeTime = relative__relativeTime;
+ prototype__proto.pastFuture = pastFuture;
+ prototype__proto.set = locale_set__set;
+
+ // Month
+ prototype__proto.months = localeMonths;
+ prototype__proto._months = defaultLocaleMonths;
+ prototype__proto.monthsShort = localeMonthsShort;
+ prototype__proto._monthsShort = defaultLocaleMonthsShort;
+ prototype__proto.monthsParse = localeMonthsParse;
+
+ // Week
+ prototype__proto.week = localeWeek;
+ prototype__proto._week = defaultLocaleWeek;
+ prototype__proto.firstDayOfYear = localeFirstDayOfYear;
+ prototype__proto.firstDayOfWeek = localeFirstDayOfWeek;
+
+ // Day of Week
+ prototype__proto.weekdays = localeWeekdays;
+ prototype__proto._weekdays = defaultLocaleWeekdays;
+ prototype__proto.weekdaysMin = localeWeekdaysMin;
+ prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin;
+ prototype__proto.weekdaysShort = localeWeekdaysShort;
+ prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort;
+ prototype__proto.weekdaysParse = localeWeekdaysParse;
+
+ // Hours
+ prototype__proto.isPM = localeIsPM;
+ prototype__proto._meridiemParse = defaultLocaleMeridiemParse;
+ prototype__proto.meridiem = localeMeridiem;
+
+ function lists__get (format, index, field, setter) {
+ var locale = locale_locales__getLocale();
+ var utc = create_utc__createUTC().set(setter, index);
+ return locale[field](utc, format);
+ }
+
+ function list (format, index, field, count, setter) {
+ if (typeof format === 'number') {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+
+ if (index != null) {
+ return lists__get(format, index, field, setter);
+ }
+
+ var i;
+ var out = [];
+ for (i = 0; i < count; i++) {
+ out[i] = lists__get(format, i, field, setter);
+ }
+ return out;
+ }
+
+ function lists__listMonths (format, index) {
+ return list(format, index, 'months', 12, 'month');
+ }
+
+ function lists__listMonthsShort (format, index) {
+ return list(format, index, 'monthsShort', 12, 'month');
+ }
+
+ function lists__listWeekdays (format, index) {
+ return list(format, index, 'weekdays', 7, 'day');
+ }
+
+ function lists__listWeekdaysShort (format, index) {
+ return list(format, index, 'weekdaysShort', 7, 'day');
+ }
+
+ function lists__listWeekdaysMin (format, index) {
+ return list(format, index, 'weekdaysMin', 7, 'day');
+ }
+
+ locale_locales__getSetGlobalLocale('en', {
+ ordinalParse: /\d{1,2}(th|st|nd|rd)/,
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (toInt(number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ }
+ });
+
+ // Side effect imports
+ utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale);
+ utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale);
+
+ var mathAbs = Math.abs;
+
+ function duration_abs__abs () {
+ var data = this._data;
+
+ this._milliseconds = mathAbs(this._milliseconds);
+ this._days = mathAbs(this._days);
+ this._months = mathAbs(this._months);
+
+ data.milliseconds = mathAbs(data.milliseconds);
+ data.seconds = mathAbs(data.seconds);
+ data.minutes = mathAbs(data.minutes);
+ data.hours = mathAbs(data.hours);
+ data.months = mathAbs(data.months);
+ data.years = mathAbs(data.years);
+
+ return this;
+ }
+
+ function duration_add_subtract__addSubtract (duration, input, value, direction) {
+ var other = create__createDuration(input, value);
+
+ duration._milliseconds += direction * other._milliseconds;
+ duration._days += direction * other._days;
+ duration._months += direction * other._months;
+
+ return duration._bubble();
+ }
+
+ // supports only 2.0-style add(1, 's') or add(duration)
+ function duration_add_subtract__add (input, value) {
+ return duration_add_subtract__addSubtract(this, input, value, 1);
+ }
+
+ // supports only 2.0-style subtract(1, 's') or subtract(duration)
+ function duration_add_subtract__subtract (input, value) {
+ return duration_add_subtract__addSubtract(this, input, value, -1);
+ }
+
+ function absCeil (number) {
+ if (number < 0) {
+ return Math.floor(number);
+ } else {
+ return Math.ceil(number);
+ }
+ }
+
+ function bubble () {
+ var milliseconds = this._milliseconds;
+ var days = this._days;
+ var months = this._months;
+ var data = this._data;
+ var seconds, minutes, hours, years, monthsFromDays;
+
+ // if we have a mix of positive and negative values, bubble down first
+ // check: https://github.com/moment/moment/issues/2166
+ if (!((milliseconds >= 0 && days >= 0 && months >= 0) ||
+ (milliseconds <= 0 && days <= 0 && months <= 0))) {
+ milliseconds += absCeil(monthsToDays(months) + days) * 864e5;
+ days = 0;
+ months = 0;
+ }
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+
+ seconds = absFloor(milliseconds / 1000);
+ data.seconds = seconds % 60;
+
+ minutes = absFloor(seconds / 60);
+ data.minutes = minutes % 60;
+
+ hours = absFloor(minutes / 60);
+ data.hours = hours % 24;
+
+ days += absFloor(hours / 24);
+
+ // convert days to months
+ monthsFromDays = absFloor(daysToMonths(days));
+ months += monthsFromDays;
+ days -= absCeil(monthsToDays(monthsFromDays));
+
+ // 12 months -> 1 year
+ years = absFloor(months / 12);
+ months %= 12;
+
+ data.days = days;
+ data.months = months;
+ data.years = years;
+
+ return this;
+ }
+
+ function daysToMonths (days) {
+ // 400 years have 146097 days (taking into account leap year rules)
+ // 400 years have 12 months === 4800
+ return days * 4800 / 146097;
+ }
+
+ function monthsToDays (months) {
+ // the reverse of daysToMonths
+ return months * 146097 / 4800;
+ }
+
+ function as (units) {
+ var days;
+ var months;
+ var milliseconds = this._milliseconds;
+
+ units = normalizeUnits(units);
+
+ if (units === 'month' || units === 'year') {
+ days = this._days + milliseconds / 864e5;
+ months = this._months + daysToMonths(days);
+ return units === 'month' ? months : months / 12;
+ } else {
+ // handle milliseconds separately because of floating point math errors (issue #1867)
+ days = this._days + Math.round(monthsToDays(this._months));
+ switch (units) {
+ case 'week' : return days / 7 + milliseconds / 6048e5;
+ case 'day' : return days + milliseconds / 864e5;
+ case 'hour' : return days * 24 + milliseconds / 36e5;
+ case 'minute' : return days * 1440 + milliseconds / 6e4;
+ case 'second' : return days * 86400 + milliseconds / 1000;
+ // Math.floor prevents floating point math errors here
+ case 'millisecond': return Math.floor(days * 864e5) + milliseconds;
+ default: throw new Error('Unknown unit ' + units);
+ }
+ }
+ }
+
+ // TODO: Use this.as('ms')?
+ function duration_as__valueOf () {
+ return (
+ this._milliseconds +
+ this._days * 864e5 +
+ (this._months % 12) * 2592e6 +
+ toInt(this._months / 12) * 31536e6
+ );
+ }
+
+ function makeAs (alias) {
+ return function () {
+ return this.as(alias);
+ };
+ }
+
+ var asMilliseconds = makeAs('ms');
+ var asSeconds = makeAs('s');
+ var asMinutes = makeAs('m');
+ var asHours = makeAs('h');
+ var asDays = makeAs('d');
+ var asWeeks = makeAs('w');
+ var asMonths = makeAs('M');
+ var asYears = makeAs('y');
+
+ function duration_get__get (units) {
+ units = normalizeUnits(units);
+ return this[units + 's']();
+ }
+
+ function makeGetter(name) {
+ return function () {
+ return this._data[name];
+ };
+ }
+
+ var milliseconds = makeGetter('milliseconds');
+ var seconds = makeGetter('seconds');
+ var minutes = makeGetter('minutes');
+ var hours = makeGetter('hours');
+ var days = makeGetter('days');
+ var months = makeGetter('months');
+ var years = makeGetter('years');
+
+ function weeks () {
+ return absFloor(this.days() / 7);
+ }
+
+ var round = Math.round;
+ var thresholds = {
+ s: 45, // seconds to minute
+ m: 45, // minutes to hour
+ h: 22, // hours to day
+ d: 26, // days to month
+ M: 11 // months to year
+ };
+
+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+ return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+ }
+
+ function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) {
+ var duration = create__createDuration(posNegDuration).abs();
+ var seconds = round(duration.as('s'));
+ var minutes = round(duration.as('m'));
+ var hours = round(duration.as('h'));
+ var days = round(duration.as('d'));
+ var months = round(duration.as('M'));
+ var years = round(duration.as('y'));
+
+ var a = seconds < thresholds.s && ['s', seconds] ||
+ minutes === 1 && ['m'] ||
+ minutes < thresholds.m && ['mm', minutes] ||
+ hours === 1 && ['h'] ||
+ hours < thresholds.h && ['hh', hours] ||
+ days === 1 && ['d'] ||
+ days < thresholds.d && ['dd', days] ||
+ months === 1 && ['M'] ||
+ months < thresholds.M && ['MM', months] ||
+ years === 1 && ['y'] || ['yy', years];
+
+ a[2] = withoutSuffix;
+ a[3] = +posNegDuration > 0;
+ a[4] = locale;
+ return substituteTimeAgo.apply(null, a);
+ }
+
+ // This function allows you to set a threshold for relative time strings
+ function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) {
+ if (thresholds[threshold] === undefined) {
+ return false;
+ }
+ if (limit === undefined) {
+ return thresholds[threshold];
+ }
+ thresholds[threshold] = limit;
+ return true;
+ }
+
+ function humanize (withSuffix) {
+ var locale = this.localeData();
+ var output = duration_humanize__relativeTime(this, !withSuffix, locale);
+
+ if (withSuffix) {
+ output = locale.pastFuture(+this, output);
+ }
+
+ return locale.postformat(output);
+ }
+
+ var iso_string__abs = Math.abs;
+
+ function iso_string__toISOString() {
+ // for ISO strings we do not use the normal bubbling rules:
+ // * milliseconds bubble up until they become hours
+ // * days do not bubble at all
+ // * months bubble up until they become years
+ // This is because there is no context-free conversion between hours and days
+ // (think of clock changes)
+ // and also not between days and months (28-31 days per month)
+ var seconds = iso_string__abs(this._milliseconds) / 1000;
+ var days = iso_string__abs(this._days);
+ var months = iso_string__abs(this._months);
+ var minutes, hours, years;
+
+ // 3600 seconds -> 60 minutes -> 1 hour
+ minutes = absFloor(seconds / 60);
+ hours = absFloor(minutes / 60);
+ seconds %= 60;
+ minutes %= 60;
+
+ // 12 months -> 1 year
+ years = absFloor(months / 12);
+ months %= 12;
+
+
+ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+ var Y = years;
+ var M = months;
+ var D = days;
+ var h = hours;
+ var m = minutes;
+ var s = seconds;
+ var total = this.asSeconds();
+
+ if (!total) {
+ // this is the same as C#'s (Noda) and python (isodate)...
+ // but not other JS (goog.date)
+ return 'P0D';
+ }
+
+ return (total < 0 ? '-' : '') +
+ 'P' +
+ (Y ? Y + 'Y' : '') +
+ (M ? M + 'M' : '') +
+ (D ? D + 'D' : '') +
+ ((h || m || s) ? 'T' : '') +
+ (h ? h + 'H' : '') +
+ (m ? m + 'M' : '') +
+ (s ? s + 'S' : '');
+ }
+
+ var duration_prototype__proto = Duration.prototype;
+
+ duration_prototype__proto.abs = duration_abs__abs;
+ duration_prototype__proto.add = duration_add_subtract__add;
+ duration_prototype__proto.subtract = duration_add_subtract__subtract;
+ duration_prototype__proto.as = as;
+ duration_prototype__proto.asMilliseconds = asMilliseconds;
+ duration_prototype__proto.asSeconds = asSeconds;
+ duration_prototype__proto.asMinutes = asMinutes;
+ duration_prototype__proto.asHours = asHours;
+ duration_prototype__proto.asDays = asDays;
+ duration_prototype__proto.asWeeks = asWeeks;
+ duration_prototype__proto.asMonths = asMonths;
+ duration_prototype__proto.asYears = asYears;
+ duration_prototype__proto.valueOf = duration_as__valueOf;
+ duration_prototype__proto._bubble = bubble;
+ duration_prototype__proto.get = duration_get__get;
+ duration_prototype__proto.milliseconds = milliseconds;
+ duration_prototype__proto.seconds = seconds;
+ duration_prototype__proto.minutes = minutes;
+ duration_prototype__proto.hours = hours;
+ duration_prototype__proto.days = days;
+ duration_prototype__proto.weeks = weeks;
+ duration_prototype__proto.months = months;
+ duration_prototype__proto.years = years;
+ duration_prototype__proto.humanize = humanize;
+ duration_prototype__proto.toISOString = iso_string__toISOString;
+ duration_prototype__proto.toString = iso_string__toISOString;
+ duration_prototype__proto.toJSON = iso_string__toISOString;
+ duration_prototype__proto.locale = locale;
+ duration_prototype__proto.localeData = localeData;
+
+ // Deprecations
+ duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString);
+ duration_prototype__proto.lang = lang;
+
+ // Side effect imports
+
+ addFormatToken('X', 0, 0, 'unix');
+ addFormatToken('x', 0, 0, 'valueOf');
+
+ // PARSING
+
+ addRegexToken('x', matchSigned);
+ addRegexToken('X', matchTimestamp);
+ addParseToken('X', function (input, array, config) {
+ config._d = new Date(parseFloat(input, 10) * 1000);
+ });
+ addParseToken('x', function (input, array, config) {
+ config._d = new Date(toInt(input));
+ });
+
+ // Side effect imports
+
+
+ utils_hooks__hooks.version = '2.10.6';
+
+ setHookCallback(local__createLocal);
+
+ utils_hooks__hooks.fn = momentPrototype;
+ utils_hooks__hooks.min = min;
+ utils_hooks__hooks.max = max;
+ utils_hooks__hooks.utc = create_utc__createUTC;
+ utils_hooks__hooks.unix = moment__createUnix;
+ utils_hooks__hooks.months = lists__listMonths;
+ utils_hooks__hooks.isDate = isDate;
+ utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale;
+ utils_hooks__hooks.invalid = valid__createInvalid;
+ utils_hooks__hooks.duration = create__createDuration;
+ utils_hooks__hooks.isMoment = isMoment;
+ utils_hooks__hooks.weekdays = lists__listWeekdays;
+ utils_hooks__hooks.parseZone = moment__createInZone;
+ utils_hooks__hooks.localeData = locale_locales__getLocale;
+ utils_hooks__hooks.isDuration = isDuration;
+ utils_hooks__hooks.monthsShort = lists__listMonthsShort;
+ utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin;
+ utils_hooks__hooks.defineLocale = defineLocale;
+ utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort;
+ utils_hooks__hooks.normalizeUnits = normalizeUnits;
+ utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold;
+
+ var _moment = utils_hooks__hooks;
+
+ return _moment;
+
+ }));
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)(module)))
+
+/***/ },
+/* 4 */
+/***/ function(module, exports) {
+
+ module.exports = function(module) {
+ if(!module.webpackPolyfill) {
+ module.deprecate = function() {};
+ module.paths = [];
+ // module.parent = undefined by default
+ module.children = [];
+ module.webpackPolyfill = 1;
+ }
+ return module;
+ }
+
+
+/***/ },
+/* 5 */
+/***/ function(module, exports) {
+
+ function webpackContext(req) {
+ throw new Error("Cannot find module '" + req + "'.");
+ }
+ webpackContext.keys = function() { return []; };
+ webpackContext.resolve = webpackContext;
+ module.exports = webpackContext;
+ webpackContext.id = 5;
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports) {
+
+ /* WEBPACK VAR INJECTION */(function(global) {'use strict';
+
+ var _rng;
+
+ var globalVar = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : null;
+
+ if (globalVar && globalVar.crypto && crypto.getRandomValues) {
+ // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
+ // Moderately fast, high quality
+ var _rnds8 = new Uint8Array(16);
+ _rng = function whatwgRNG() {
+ crypto.getRandomValues(_rnds8);
+ return _rnds8;
+ };
+ }
+
+ if (!_rng) {
+ // Math.random()-based (RNG)
+ //
+ // If all else fails, use Math.random(). It's fast, but is of unspecified
+ // quality.
+ var _rnds = new Array(16);
+ _rng = function () {
+ for (var i = 0, r; i < 16; i++) {
+ if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
+ _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
+ }
+
+ return _rnds;
+ };
+ }
+
+ // uuid.js
+ //
+ // Copyright (c) 2010-2012 Robert Kieffer
+ // MIT License - http://opensource.org/licenses/mit-license.php
+
+ // Unique ID creation requires a high quality random # generator. We feature
+ // detect to determine the best RNG source, normalizing to a function that
+ // returns 128-bits of randomness, since that's what's usually required
+
+ //var _rng = require('./rng');
+
+ // Maps for number <-> hex string conversion
+ var _byteToHex = [];
+ var _hexToByte = {};
+ for (var i = 0; i < 256; i++) {
+ _byteToHex[i] = (i + 0x100).toString(16).substr(1);
+ _hexToByte[_byteToHex[i]] = i;
+ }
+
+ // **`parse()` - Parse a UUID into it's component bytes**
+ function parse(s, buf, offset) {
+ var i = buf && offset || 0,
+ ii = 0;
+
+ buf = buf || [];
+ s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) {
+ if (ii < 16) {
+ // Don't overflow!
+ buf[i + ii++] = _hexToByte[oct];
+ }
+ });
+
+ // Zero out remaining bytes if string was short
+ while (ii < 16) {
+ buf[i + ii++] = 0;
+ }
+
+ return buf;
+ }
+
+ // **`unparse()` - Convert UUID byte array (ala parse()) into a string**
+ function unparse(buf, offset) {
+ var i = offset || 0,
+ bth = _byteToHex;
+ return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]];
+ }
+
+ // **`v1()` - Generate time-based UUID**
+ //
+ // Inspired by https://github.com/LiosK/UUID.js
+ // and http://docs.python.org/library/uuid.html
+
+ // random #'s we need to init node and clockseq
+ var _seedBytes = _rng();
+
+ // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
+ var _nodeId = [_seedBytes[0] | 0x01, _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]];
+
+ // Per 4.2.2, randomize (14 bit) clockseq
+ var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
+
+ // Previous uuid creation time
+ var _lastMSecs = 0,
+ _lastNSecs = 0;
+
+ // See https://github.com/broofa/node-uuid for API details
+ function v1(options, buf, offset) {
+ var i = buf && offset || 0;
+ var b = buf || [];
+
+ options = options || {};
+
+ var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
+
+ // UUID timestamps are 100 nano-second units since the Gregorian epoch,
+ // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
+ // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
+ // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
+ var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
+
+ // Per 4.2.1.2, use count of uuid's generated during the current clock
+ // cycle to simulate higher resolution clock
+ var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
+
+ // Time since last uuid creation (in msecs)
+ var dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000;
+
+ // Per 4.2.1.2, Bump clockseq on clock regression
+ if (dt < 0 && options.clockseq === undefined) {
+ clockseq = clockseq + 1 & 0x3fff;
+ }
+
+ // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
+ // time interval
+ if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
+ nsecs = 0;
+ }
+
+ // Per 4.2.1.2 Throw error if too many uuids are requested
+ if (nsecs >= 10000) {
+ throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
+ }
+
+ _lastMSecs = msecs;
+ _lastNSecs = nsecs;
+ _clockseq = clockseq;
+
+ // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
+ msecs += 12219292800000;
+
+ // `time_low`
+ var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
+ b[i++] = tl >>> 24 & 0xff;
+ b[i++] = tl >>> 16 & 0xff;
+ b[i++] = tl >>> 8 & 0xff;
+ b[i++] = tl & 0xff;
+
+ // `time_mid`
+ var tmh = msecs / 0x100000000 * 10000 & 0xfffffff;
+ b[i++] = tmh >>> 8 & 0xff;
+ b[i++] = tmh & 0xff;
+
+ // `time_high_and_version`
+ b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
+ b[i++] = tmh >>> 16 & 0xff;
+
+ // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
+ b[i++] = clockseq >>> 8 | 0x80;
+
+ // `clock_seq_low`
+ b[i++] = clockseq & 0xff;
+
+ // `node`
+ var node = options.node || _nodeId;
+ for (var n = 0; n < 6; n++) {
+ b[i + n] = node[n];
+ }
+
+ return buf ? buf : unparse(b);
+ }
+
+ // **`v4()` - Generate random UUID**
+
+ // See https://github.com/broofa/node-uuid for API details
+ function v4(options, buf, offset) {
+ // Deprecated - 'format' argument, as supported in v1.2
+ var i = buf && offset || 0;
+
+ if (typeof options == 'string') {
+ buf = options == 'binary' ? new Array(16) : null;
+ options = null;
+ }
+ options = options || {};
+
+ var rnds = options.random || (options.rng || _rng)();
+
+ // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
+ rnds[6] = rnds[6] & 0x0f | 0x40;
+ rnds[8] = rnds[8] & 0x3f | 0x80;
+
+ // Copy bytes to buffer, if provided
+ if (buf) {
+ for (var ii = 0; ii < 16; ii++) {
+ buf[i + ii] = rnds[ii];
+ }
+ }
+
+ return buf || unparse(rnds);
+ }
+
+ // Export public API
+ var uuid = v4;
+ uuid.v1 = v1;
+ uuid.v4 = v4;
+ uuid.parse = parse;
+ uuid.unparse = unparse;
+
+ module.exports = uuid;
+ /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }())))
+
+/***/ },
+/* 7 */
+/***/ function(module, exports) {
+
+ // DOM utility methods
+
+ /**
+ * this prepares the JSON container for allocating SVG elements
+ * @param JSONcontainer
+ * @private
+ */
+ 'use strict';
+
+ exports.prepareElements = function (JSONcontainer) {
+ // cleanup the redundant svgElements;
+ for (var elementType in JSONcontainer) {
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
+ JSONcontainer[elementType].used = [];
+ }
+ }
+ };
+
+ /**
+ * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
+ * which to remove the redundant elements.
+ *
+ * @param JSONcontainer
+ * @private
+ */
+ exports.cleanupElements = function (JSONcontainer) {
+ // cleanup the redundant svgElements;
+ for (var elementType in JSONcontainer) {
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ if (JSONcontainer[elementType].redundant) {
+ for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
+ JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
+ }
+ JSONcontainer[elementType].redundant = [];
+ }
+ }
+ }
+ };
+
+ /**
+ * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+ * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+ *
+ * @param elementType
+ * @param JSONcontainer
+ * @param svgContainer
+ * @returns {*}
+ * @private
+ */
+ exports.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
+ var element;
+ // allocate SVG element, if it doesnt yet exist, create one.
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ // this element has been created before
+ // check if there is an redundant element
+ if (JSONcontainer[elementType].redundant.length > 0) {
+ element = JSONcontainer[elementType].redundant[0];
+ JSONcontainer[elementType].redundant.shift();
+ } else {
+ // create a new element and add it to the SVG
+ element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+ svgContainer.appendChild(element);
+ }
+ } else {
+ // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+ element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+ JSONcontainer[elementType] = { used: [], redundant: [] };
+ svgContainer.appendChild(element);
+ }
+ JSONcontainer[elementType].used.push(element);
+ return element;
+ };
+
+ /**
+ * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+ * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+ *
+ * @param elementType
+ * @param JSONcontainer
+ * @param DOMContainer
+ * @returns {*}
+ * @private
+ */
+ exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, insertBefore) {
+ var element;
+ // allocate DOM element, if it doesnt yet exist, create one.
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ // this element has been created before
+ // check if there is an redundant element
+ if (JSONcontainer[elementType].redundant.length > 0) {
+ element = JSONcontainer[elementType].redundant[0];
+ JSONcontainer[elementType].redundant.shift();
+ } else {
+ // create a new element and add it to the SVG
+ element = document.createElement(elementType);
+ if (insertBefore !== undefined) {
+ DOMContainer.insertBefore(element, insertBefore);
+ } else {
+ DOMContainer.appendChild(element);
+ }
+ }
+ } else {
+ // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+ element = document.createElement(elementType);
+ JSONcontainer[elementType] = { used: [], redundant: [] };
+ if (insertBefore !== undefined) {
+ DOMContainer.insertBefore(element, insertBefore);
+ } else {
+ DOMContainer.appendChild(element);
+ }
+ }
+ JSONcontainer[elementType].used.push(element);
+ return element;
+ };
+
+ /**
+ * Draw a point object. This is a separate function because it can also be called by the legend.
+ * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
+ * as well.
+ *
+ * @param x
+ * @param y
+ * @param groupTemplate: A template containing the necessary information to draw the datapoint e.g., {style: 'circle', size: 5, className: 'className' }
+ * @param JSONcontainer
+ * @param svgContainer
+ * @param labelObj
+ * @returns {*}
+ */
+ exports.drawPoint = function (x, y, groupTemplate, JSONcontainer, svgContainer, labelObj) {
+ var point;
+ if (groupTemplate.style == 'circle') {
+ point = exports.getSVGElement('circle', JSONcontainer, svgContainer);
+ point.setAttributeNS(null, 'cx', x);
+ point.setAttributeNS(null, 'cy', y);
+ point.setAttributeNS(null, 'r', 0.5 * groupTemplate.size);
+ } else {
+ point = exports.getSVGElement('rect', JSONcontainer, svgContainer);
+ point.setAttributeNS(null, 'x', x - 0.5 * groupTemplate.size);
+ point.setAttributeNS(null, 'y', y - 0.5 * groupTemplate.size);
+ point.setAttributeNS(null, 'width', groupTemplate.size);
+ point.setAttributeNS(null, 'height', groupTemplate.size);
+ }
+
+ if (groupTemplate.styles !== undefined) {
+ point.setAttributeNS(null, 'style', groupTemplate.styles);
+ }
+ point.setAttributeNS(null, 'class', groupTemplate.className + ' vis-point');
+ //handle label
+
+ if (labelObj) {
+ var label = exports.getSVGElement('text', JSONcontainer, svgContainer);
+ if (labelObj.xOffset) {
+ x = x + labelObj.xOffset;
+ }
+
+ if (labelObj.yOffset) {
+ y = y + labelObj.yOffset;
+ }
+ if (labelObj.content) {
+ label.textContent = labelObj.content;
+ }
+
+ if (labelObj.className) {
+ label.setAttributeNS(null, 'class', labelObj.className + ' vis-label');
+ }
+ label.setAttributeNS(null, 'x', x);
+ label.setAttributeNS(null, 'y', y);
+ }
+
+ return point;
+ };
+
+ /**
+ * draw a bar SVG element centered on the X coordinate
+ *
+ * @param x
+ * @param y
+ * @param className
+ */
+ exports.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer, style) {
+ if (height != 0) {
+ if (height < 0) {
+ height *= -1;
+ y -= height;
+ }
+ var rect = exports.getSVGElement('rect', JSONcontainer, svgContainer);
+ rect.setAttributeNS(null, 'x', x - 0.5 * width);
+ rect.setAttributeNS(null, 'y', y);
+ rect.setAttributeNS(null, 'width', width);
+ rect.setAttributeNS(null, 'height', height);
+ rect.setAttributeNS(null, 'class', className);
+ if (style) {
+ rect.setAttributeNS(null, 'style', style);
+ }
+ }
+ };
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var util = __webpack_require__(1);
+ var Queue = __webpack_require__(9);
+
+ /**
+ * DataSet
+ *
+ * Usage:
+ * var dataSet = new DataSet({
+ * fieldId: '_id',
+ * type: {
+ * // ...
+ * }
+ * });
+ *
+ * dataSet.add(item);
+ * dataSet.add(data);
+ * dataSet.update(item);
+ * dataSet.update(data);
+ * dataSet.remove(id);
+ * dataSet.remove(ids);
+ * var data = dataSet.get();
+ * var data = dataSet.get(id);
+ * var data = dataSet.get(ids);
+ * var data = dataSet.get(ids, options, data);
+ * dataSet.clear();
+ *
+ * A data set can:
+ * - add/remove/update data
+ * - gives triggers upon changes in the data
+ * - can import/export data in various data formats
+ *
+ * @param {Array} [data] Optional array with initial data
+ * @param {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.<String, String} type
+ * A map with field names as key,
+ * and the field type as value.
+ * {Object} queue Queue changes to the DataSet,
+ * flush them all at once.
+ * Queue options:
+ * - {number} delay Delay in ms, null by default
+ * - {number} max Maximum number of entries in the queue, Infinity by default
+ * @constructor DataSet
+ */
+ // TODO: add a DataSet constructor DataSet(data, options)
+ function DataSet(data, options) {
+ // correctly read optional arguments
+ if (data && !Array.isArray(data)) {
+ options = data;
+ data = null;
+ }
+
+ this._options = options || {};
+ this._data = {}; // map with data indexed by id
+ this.length = 0; // number of items in the DataSet
+ this._fieldId = this._options.fieldId || 'id'; // name of the field containing id
+ this._type = {}; // internal field types (NOTE: this can differ from this._options.type)
+
+ // all variants of a Date are internally stored as Date, so we can convert
+ // from everything to everything (also from ISODate to Number for example)
+ if (this._options.type) {
+ for (var field in this._options.type) {
+ if (this._options.type.hasOwnProperty(field)) {
+ var value = this._options.type[field];
+ if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
+ this._type[field] = 'Date';
+ } else {
+ this._type[field] = value;
+ }
+ }
+ }
+ }
+
+ // TODO: deprecated since version 1.1.1 (or 2.0.0?)
+ if (this._options.convert) {
+ throw new Error('Option "convert" is deprecated. Use "type" instead.');
+ }
+
+ this._subscribers = {}; // event subscribers
+
+ // add initial data when provided
+ if (data) {
+ this.add(data);
+ }
+
+ this.setOptions(options);
+ }
+
+ /**
+ * @param {Object} [options] Available options:
+ * {Object} queue Queue changes to the DataSet,
+ * flush them all at once.
+ * Queue options:
+ * - {number} delay Delay in ms, null by default
+ * - {number} max Maximum number of entries in the queue, Infinity by default
+ * @param options
+ */
+ DataSet.prototype.setOptions = function (options) {
+ if (options && options.queue !== undefined) {
+ if (options.queue === false) {
+ // delete queue if loaded
+ if (this._queue) {
+ this._queue.destroy();
+ delete this._queue;
+ }
+ } else {
+ // create queue and update its options
+ if (!this._queue) {
+ this._queue = Queue.extend(this, {
+ replace: ['add', 'update', 'remove']
+ });
+ }
+
+ if (typeof options.queue === 'object') {
+ this._queue.setOptions(options.queue);
+ }
+ }
+ }
+ };
+
+ /**
+ * Subscribe to an event, add an event listener
+ * @param {String} event Event name. Available events: 'put', 'update',
+ * 'remove'
+ * @param {function} callback Callback method. Called with three parameters:
+ * {String} event
+ * {Object | null} params
+ * {String | Number} senderId
+ */
+ DataSet.prototype.on = function (event, callback) {
+ var subscribers = this._subscribers[event];
+ if (!subscribers) {
+ subscribers = [];
+ this._subscribers[event] = subscribers;
+ }
+
+ subscribers.push({
+ callback: callback
+ });
+ };
+
+ // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0)
+ DataSet.prototype.subscribe = function () {
+ throw new Error('DataSet.subscribe is deprecated. Use DataSet.on instead.');
+ };
+
+ /**
+ * Unsubscribe from an event, remove an event listener
+ * @param {String} event
+ * @param {function} callback
+ */
+ DataSet.prototype.off = function (event, callback) {
+ var subscribers = this._subscribers[event];
+ if (subscribers) {
+ this._subscribers[event] = subscribers.filter(function (listener) {
+ return listener.callback != callback;
+ });
+ }
+ };
+
+ // TODO: remove this deprecated function some day (replaced with `on` since version 0.5, deprecated since v4.0)
+ DataSet.prototype.unsubscribe = function () {
+ throw new Error('DataSet.unsubscribe is deprecated. Use DataSet.off instead.');
+ };
+
+ /**
+ * Trigger an event
+ * @param {String} event
+ * @param {Object | null} params
+ * @param {String} [senderId] Optional id of the sender.
+ * @private
+ */
+ DataSet.prototype._trigger = function (event, params, senderId) {
+ if (event == '*') {
+ throw new Error('Cannot trigger event *');
+ }
+
+ var subscribers = [];
+ if (event in this._subscribers) {
+ subscribers = subscribers.concat(this._subscribers[event]);
+ }
+ if ('*' in this._subscribers) {
+ subscribers = subscribers.concat(this._subscribers['*']);
+ }
+
+ for (var i = 0; i < subscribers.length; i++) {
+ var subscriber = subscribers[i];
+ if (subscriber.callback) {
+ subscriber.callback(event, params, senderId || null);
+ }
+ }
+ };
+
+ /**
+ * Add data.
+ * Adding an item will fail when there already is an item with the same id.
+ * @param {Object | Array} data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} addedIds Array with the ids of the added items
+ */
+ DataSet.prototype.add = function (data, senderId) {
+ var addedIds = [],
+ id,
+ me = this;
+
+ if (Array.isArray(data)) {
+ // Array
+ for (var i = 0, len = data.length; i < len; i++) {
+ id = me._addItem(data[i]);
+ addedIds.push(id);
+ }
+ } else if (data instanceof Object) {
+ // Single item
+ id = me._addItem(data);
+ addedIds.push(id);
+ } else {
+ throw new Error('Unknown dataType');
+ }
+
+ if (addedIds.length) {
+ this._trigger('add', { items: addedIds }, senderId);
+ }
+
+ return addedIds;
+ };
+
+ /**
+ * Update existing items. When an item does not exist, it will be created
+ * @param {Object | Array} data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} updatedIds The ids of the added or updated items
+ */
+ DataSet.prototype.update = function (data, senderId) {
+ var addedIds = [];
+ var updatedIds = [];
+ var updatedData = [];
+ var me = this;
+ var fieldId = me._fieldId;
+
+ var addOrUpdate = function addOrUpdate(item) {
+ var id = item[fieldId];
+ if (me._data[id]) {
+ // update item
+ id = me._updateItem(item);
+ updatedIds.push(id);
+ updatedData.push(item);
+ } else {
+ // add new item
+ id = me._addItem(item);
+ addedIds.push(id);
+ }
+ };
+
+ if (Array.isArray(data)) {
+ // Array
+ for (var i = 0, len = data.length; i < len; i++) {
+ addOrUpdate(data[i]);
+ }
+ } else if (data instanceof Object) {
+ // Single item
+ addOrUpdate(data);
+ } else {
+ throw new Error('Unknown dataType');
+ }
+
+ if (addedIds.length) {
+ this._trigger('add', { items: addedIds }, senderId);
+ }
+ if (updatedIds.length) {
+ this._trigger('update', { items: updatedIds, data: updatedData }, senderId);
+ }
+
+ return addedIds.concat(updatedIds);
+ };
+
+ /**
+ * Get a data item or multiple items.
+ *
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ *
+ * get(id: Number | String)
+ * get(id: Number | String, options: Object)
+ *
+ * get(ids: Number[] | String[])
+ * get(ids: Number[] | String[], options: Object)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [returnType] Type of data to be returned.
+ * Can be 'Array' (default) or 'Object'.
+ * {Object.<String, String>} [type]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by a field name or custom sort function.
+ * @throws Error
+ */
+ DataSet.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var id, ids, options;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number') {
+ // get(id [, options])
+ id = arguments[0];
+ options = arguments[1];
+ } else if (firstType == 'Array') {
+ // get(ids [, options])
+ ids = arguments[0];
+ options = arguments[1];
+ } else {
+ // get([, options])
+ options = arguments[0];
+ }
+
+ // determine the return type
+ var returnType;
+ if (options && options.returnType) {
+ var allowedValues = ['Array', 'Object'];
+ returnType = allowedValues.indexOf(options.returnType) == -1 ? 'Array' : options.returnType;
+ } else {
+ returnType = 'Array';
+ }
+
+ // build options
+ var type = options && options.type || this._options.type;
+ var filter = options && options.filter;
+ var items = [],
+ item,
+ itemId,
+ i,
+ len;
+
+ // convert items
+ if (id != undefined) {
+ // return a single item
+ item = me._getItem(id, type);
+ if (filter && !filter(item)) {
+ item = null;
+ }
+ } else if (ids != undefined) {
+ // return a subset of items
+ for (i = 0, len = ids.length; i < len; i++) {
+ item = me._getItem(ids[i], type);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ } else {
+ // return all items
+ for (itemId in this._data) {
+ if (this._data.hasOwnProperty(itemId)) {
+ item = me._getItem(itemId, type);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+ }
+
+ // order the results
+ if (options && options.order && id == undefined) {
+ this._sort(items, options.order);
+ }
+
+ // filter fields of the items
+ if (options && options.fields) {
+ var fields = options.fields;
+ if (id != undefined) {
+ item = this._filterFields(item, fields);
+ } else {
+ for (i = 0, len = items.length; i < len; i++) {
+ items[i] = this._filterFields(items[i], fields);
+ }
+ }
+ }
+
+ // return the results
+ if (returnType == 'Object') {
+ var result = {};
+ for (i = 0; i < items.length; i++) {
+ result[items[i].id] = items[i];
+ }
+ return result;
+ } else {
+ if (id != undefined) {
+ // a single item
+ return item;
+ } else {
+ // just return our array
+ return items;
+ }
+ }
+ };
+
+ /**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+ DataSet.prototype.getIds = function (options) {
+ var data = this._data,
+ filter = options && options.filter,
+ order = options && options.order,
+ type = options && options.type || this._options.type,
+ i,
+ len,
+ id,
+ item,
+ items,
+ ids = [];
+
+ if (filter) {
+ // get filtered items
+ if (order) {
+ // create ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this._fieldId];
+ }
+ } else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (filter(item)) {
+ ids.push(item[this._fieldId]);
+ }
+ }
+ }
+ }
+ } else {
+ // get all items
+ if (order) {
+ // create an ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ items.push(data[id]);
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this._fieldId];
+ }
+ } else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = data[id];
+ ids.push(item[this._fieldId]);
+ }
+ }
+ }
+ }
+
+ return ids;
+ };
+
+ /**
+ * Returns the DataSet itself. Is overwritten for example by the DataView,
+ * which returns the DataSet it is connected to instead.
+ */
+ DataSet.prototype.getDataSet = function () {
+ return this;
+ };
+
+ /**
+ * Execute a callback function for every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.<String, String>} [type]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ */
+ DataSet.prototype.forEach = function (callback, options) {
+ var filter = options && options.filter,
+ type = options && options.type || this._options.type,
+ data = this._data,
+ item,
+ id;
+
+ if (options && options.order) {
+ // execute forEach on ordered list
+ var items = this.get(options);
+
+ for (var i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ id = item[this._fieldId];
+ callback(item, id);
+ }
+ } else {
+ // unordered
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (!filter || filter(item)) {
+ callback(item, id);
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Map every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.<String, String>} [type]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Object[]} mappedItems
+ */
+ DataSet.prototype.map = function (callback, options) {
+ var filter = options && options.filter,
+ type = options && options.type || this._options.type,
+ mappedItems = [],
+ data = this._data,
+ item;
+
+ // convert and filter items
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (!filter || filter(item)) {
+ mappedItems.push(callback(item, id));
+ }
+ }
+ }
+
+ // order items
+ if (options && options.order) {
+ this._sort(mappedItems, options.order);
+ }
+
+ return mappedItems;
+ };
+
+ /**
+ * Filter the fields of an item
+ * @param {Object | null} item
+ * @param {String[]} fields Field names
+ * @return {Object | null} filteredItem or null if no item is provided
+ * @private
+ */
+ DataSet.prototype._filterFields = function (item, fields) {
+ if (!item) {
+ // item is null
+ return item;
+ }
+
+ var filteredItem = {};
+
+ if (Array.isArray(fields)) {
+ for (var field in item) {
+ if (item.hasOwnProperty(field) && fields.indexOf(field) != -1) {
+ filteredItem[field] = item[field];
+ }
+ }
+ } else {
+ for (var field in item) {
+ if (item.hasOwnProperty(field) && fields.hasOwnProperty(field)) {
+ filteredItem[fields[field]] = item[field];
+ }
+ }
+ }
+
+ return filteredItem;
+ };
+
+ /**
+ * Sort the provided array with items
+ * @param {Object[]} items
+ * @param {String | function} order A field name or custom sort function.
+ * @private
+ */
+ DataSet.prototype._sort = function (items, order) {
+ if (util.isString(order)) {
+ // order by provided field name
+ var name = order; // field name
+ items.sort(function (a, b) {
+ var av = a[name];
+ var bv = b[name];
+ return av > bv ? 1 : av < bv ? -1 : 0;
+ });
+ } else if (typeof order === 'function') {
+ // order by sort function
+ items.sort(order);
+ }
+ // TODO: extend order by an Object {field:String, direction:String}
+ // where direction can be 'asc' or 'desc'
+ else {
+ throw new TypeError('Order must be a function or a string');
+ }
+ };
+
+ /**
+ * Remove an object by pointer or by id
+ * @param {String | Number | Object | Array} id Object or id, or an array with
+ * objects or ids to be removed
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds
+ */
+ DataSet.prototype.remove = function (id, senderId) {
+ var removedIds = [],
+ i,
+ len,
+ removedId;
+
+ if (Array.isArray(id)) {
+ for (i = 0, len = id.length; i < len; i++) {
+ removedId = this._remove(id[i]);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+ } else {
+ removedId = this._remove(id);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+
+ if (removedIds.length) {
+ this._trigger('remove', { items: removedIds }, senderId);
+ }
+
+ return removedIds;
+ };
+
+ /**
+ * Remove an item by its id
+ * @param {Number | String | Object} id id or item
+ * @returns {Number | String | null} id
+ * @private
+ */
+ DataSet.prototype._remove = function (id) {
+ if (util.isNumber(id) || util.isString(id)) {
+ if (this._data[id]) {
+ delete this._data[id];
+ this.length--;
+ return id;
+ }
+ } else if (id instanceof Object) {
+ var itemId = id[this._fieldId];
+ if (itemId && this._data[itemId]) {
+ delete this._data[itemId];
+ this.length--;
+ return itemId;
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Clear the data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds The ids of all removed items
+ */
+ DataSet.prototype.clear = function (senderId) {
+ var ids = Object.keys(this._data);
+
+ this._data = {};
+ this.length = 0;
+
+ this._trigger('remove', { items: ids }, senderId);
+
+ return ids;
+ };
+
+ /**
+ * Find the item with maximum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+ DataSet.prototype.max = function (field) {
+ var data = this._data,
+ max = null,
+ maxField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!max || itemField > maxField)) {
+ max = item;
+ maxField = itemField;
+ }
+ }
+ }
+
+ return max;
+ };
+
+ /**
+ * Find the item with minimum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+ DataSet.prototype.min = function (field) {
+ var data = this._data,
+ min = null,
+ minField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!min || itemField < minField)) {
+ min = item;
+ minField = itemField;
+ }
+ }
+ }
+
+ return min;
+ };
+
+ /**
+ * Find all distinct values of a specified field
+ * @param {String} field
+ * @return {Array} values Array containing all distinct values. If data items
+ * do not contain the specified field are ignored.
+ * The returned array is unordered.
+ */
+ DataSet.prototype.distinct = function (field) {
+ var data = this._data;
+ var values = [];
+ var fieldType = this._options.type && this._options.type[field] || null;
+ var count = 0;
+ var i;
+
+ for (var prop in data) {
+ if (data.hasOwnProperty(prop)) {
+ var item = data[prop];
+ var value = item[field];
+ var exists = false;
+ for (i = 0; i < count; i++) {
+ if (values[i] == value) {
+ exists = true;
+ break;
+ }
+ }
+ if (!exists && value !== undefined) {
+ values[count] = value;
+ count++;
+ }
+ }
+ }
+
+ if (fieldType) {
+ for (i = 0; i < values.length; i++) {
+ values[i] = util.convert(values[i], fieldType);
+ }
+ }
+
+ return values;
+ };
+
+ /**
+ * Add a single item. Will fail when an item with the same id already exists.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+ DataSet.prototype._addItem = function (item) {
+ var id = item[this._fieldId];
+
+ if (id != undefined) {
+ // check whether this id is already taken
+ if (this._data[id]) {
+ // item already exists
+ throw new Error('Cannot add item: item with id ' + id + ' already exists');
+ }
+ } else {
+ // generate an id
+ id = util.randomUUID();
+ item[this._fieldId] = id;
+ }
+
+ var d = {};
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this._type[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+ this._data[id] = d;
+ this.length++;
+
+ return id;
+ };
+
+ /**
+ * Get an item. Fields can be converted to a specific type
+ * @param {String} id
+ * @param {Object.<String, String>} [types] field types to convert
+ * @return {Object | null} item
+ * @private
+ */
+ DataSet.prototype._getItem = function (id, types) {
+ var field, value;
+
+ // get the item from the dataset
+ var raw = this._data[id];
+ if (!raw) {
+ return null;
+ }
+
+ // convert the items field types
+ var converted = {};
+ if (types) {
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ converted[field] = util.convert(value, types[field]);
+ }
+ }
+ } else {
+ // no field types specified, no converting needed
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ converted[field] = value;
+ }
+ }
+ }
+ return converted;
+ };
+
+ /**
+ * Update a single item: merge with existing item.
+ * Will fail when the item has no id, or when there does not exist an item
+ * with the same id.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+ DataSet.prototype._updateItem = function (item) {
+ var id = item[this._fieldId];
+ if (id == undefined) {
+ throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this._data[id];
+ if (!d) {
+ // item doesn't exist
+ throw new Error('Cannot update item: no item with id ' + id + ' found');
+ }
+
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this._type[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+
+ return id;
+ };
+
+ module.exports = DataSet;
+
+/***/ },
+/* 9 */
+/***/ function(module, exports) {
+
+ /**
+ * A queue
+ * @param {Object} options
+ * Available options:
+ * - delay: number When provided, the queue will be flushed
+ * automatically after an inactivity of this delay
+ * in milliseconds.
+ * Default value is null.
+ * - max: number When the queue exceeds the given maximum number
+ * of entries, the queue is flushed automatically.
+ * Default value of max is Infinity.
+ * @constructor
+ */
+ 'use strict';
+
+ function Queue(options) {
+ // options
+ this.delay = null;
+ this.max = Infinity;
+
+ // properties
+ this._queue = [];
+ this._timeout = null;
+ this._extended = null;
+
+ this.setOptions(options);
+ }
+
+ /**
+ * Update the configuration of the queue
+ * @param {Object} options
+ * Available options:
+ * - delay: number When provided, the queue will be flushed
+ * automatically after an inactivity of this delay
+ * in milliseconds.
+ * Default value is null.
+ * - max: number When the queue exceeds the given maximum number
+ * of entries, the queue is flushed automatically.
+ * Default value of max is Infinity.
+ * @param options
+ */
+ Queue.prototype.setOptions = function (options) {
+ if (options && typeof options.delay !== 'undefined') {
+ this.delay = options.delay;
+ }
+ if (options && typeof options.max !== 'undefined') {
+ this.max = options.max;
+ }
+
+ this._flushIfNeeded();
+ };
+
+ /**
+ * Extend an object with queuing functionality.
+ * The object will be extended with a function flush, and the methods provided
+ * in options.replace will be replaced with queued ones.
+ * @param {Object} object
+ * @param {Object} options
+ * Available options:
+ * - replace: Array.<string>
+ * A list with method names of the methods
+ * on the object to be replaced with queued ones.
+ * - delay: number When provided, the queue will be flushed
+ * automatically after an inactivity of this delay
+ * in milliseconds.
+ * Default value is null.
+ * - max: number When the queue exceeds the given maximum number
+ * of entries, the queue is flushed automatically.
+ * Default value of max is Infinity.
+ * @return {Queue} Returns the created queue
+ */
+ Queue.extend = function (object, options) {
+ var queue = new Queue(options);
+
+ if (object.flush !== undefined) {
+ throw new Error('Target object already has a property flush');
+ }
+ object.flush = function () {
+ queue.flush();
+ };
+
+ var methods = [{
+ name: 'flush',
+ original: undefined
+ }];
+
+ if (options && options.replace) {
+ for (var i = 0; i < options.replace.length; i++) {
+ var name = options.replace[i];
+ methods.push({
+ name: name,
+ original: object[name]
+ });
+ queue.replace(object, name);
+ }
+ }
+
+ queue._extended = {
+ object: object,
+ methods: methods
+ };
+
+ return queue;
+ };
+
+ /**
+ * Destroy the queue. The queue will first flush all queued actions, and in
+ * case it has extended an object, will restore the original object.
+ */
+ Queue.prototype.destroy = function () {
+ this.flush();
+
+ if (this._extended) {
+ var object = this._extended.object;
+ var methods = this._extended.methods;
+ for (var i = 0; i < methods.length; i++) {
+ var method = methods[i];
+ if (method.original) {
+ object[method.name] = method.original;
+ } else {
+ delete object[method.name];
+ }
+ }
+ this._extended = null;
+ }
+ };
+
+ /**
+ * Replace a method on an object with a queued version
+ * @param {Object} object Object having the method
+ * @param {string} method The method name
+ */
+ Queue.prototype.replace = function (object, method) {
+ var me = this;
+ var original = object[method];
+ if (!original) {
+ throw new Error('Method ' + method + ' undefined');
+ }
+
+ object[method] = function () {
+ // create an Array with the arguments
+ var args = [];
+ for (var i = 0; i < arguments.length; i++) {
+ args[i] = arguments[i];
+ }
+
+ // add this call to the queue
+ me.queue({
+ args: args,
+ fn: original,
+ context: this
+ });
+ };
+ };
+
+ /**
+ * Queue a call
+ * @param {function | {fn: function, args: Array} | {fn: function, args: Array, context: Object}} entry
+ */
+ Queue.prototype.queue = function (entry) {
+ if (typeof entry === 'function') {
+ this._queue.push({ fn: entry });
+ } else {
+ this._queue.push(entry);
+ }
+
+ this._flushIfNeeded();
+ };
+
+ /**
+ * Check whether the queue needs to be flushed
+ * @private
+ */
+ Queue.prototype._flushIfNeeded = function () {
+ // flush when the maximum is exceeded.
+ if (this._queue.length > this.max) {
+ this.flush();
+ }
+
+ // flush after a period of inactivity when a delay is configured
+ clearTimeout(this._timeout);
+ if (this.queue.length > 0 && typeof this.delay === 'number') {
+ var me = this;
+ this._timeout = setTimeout(function () {
+ me.flush();
+ }, this.delay);
+ }
+ };
+
+ /**
+ * Flush all queued calls
+ */
+ Queue.prototype.flush = function () {
+ while (this._queue.length > 0) {
+ var entry = this._queue.shift();
+ entry.fn.apply(entry.context || entry.fn, entry.args || []);
+ }
+ };
+
+ module.exports = Queue;
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(8);
+
+ /**
+ * DataView
+ *
+ * a dataview offers a filtered view on a dataset or an other dataview.
+ *
+ * @param {DataSet | DataView} data
+ * @param {Object} [options] Available options: see method get
+ *
+ * @constructor DataView
+ */
+ function DataView(data, options) {
+ this._data = null;
+ this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
+ this.length = 0; // number of items in the DataView
+ this._options = options || {};
+ this._fieldId = 'id'; // name of the field containing id
+ this._subscribers = {}; // event subscribers
+
+ var me = this;
+ this.listener = function () {
+ me._onEvent.apply(me, arguments);
+ };
+
+ this.setData(data);
+ }
+
+ // TODO: implement a function .config() to dynamically update things like configured filter
+ // and trigger changes accordingly
+
+ /**
+ * Set a data source for the view
+ * @param {DataSet | DataView} data
+ */
+ DataView.prototype.setData = function (data) {
+ var ids, i, len;
+
+ if (this._data) {
+ // unsubscribe from current dataset
+ if (this._data.off) {
+ this._data.off('*', this.listener);
+ }
+
+ // trigger a remove of all items in memory
+ ids = [];
+ for (var id in this._ids) {
+ if (this._ids.hasOwnProperty(id)) {
+ ids.push(id);
+ }
+ }
+ this._ids = {};
+ this.length = 0;
+ this._trigger('remove', { items: ids });
+ }
+
+ this._data = data;
+
+ if (this._data) {
+ // update fieldId
+ this._fieldId = this._options.fieldId || this._data && this._data.options && this._data.options.fieldId || 'id';
+
+ // trigger an add of all added items
+ ids = this._data.getIds({ filter: this._options && this._options.filter });
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ this._ids[id] = true;
+ }
+ this.length = ids.length;
+ this._trigger('add', { items: ids });
+
+ // subscribe to new dataset
+ if (this._data.on) {
+ this._data.on('*', this.listener);
+ }
+ }
+ };
+
+ /**
+ * Refresh the DataView. Useful when the DataView has a filter function
+ * containing a variable parameter.
+ */
+ DataView.prototype.refresh = function () {
+ var id;
+ var ids = this._data.getIds({ filter: this._options && this._options.filter });
+ var newIds = {};
+ var added = [];
+ var removed = [];
+
+ // check for additions
+ for (var i = 0; i < ids.length; i++) {
+ id = ids[i];
+ newIds[id] = true;
+ if (!this._ids[id]) {
+ added.push(id);
+ this._ids[id] = true;
+ this.length++;
+ }
+ }
+
+ // check for removals
+ for (id in this._ids) {
+ if (this._ids.hasOwnProperty(id)) {
+ if (!newIds[id]) {
+ removed.push(id);
+ delete this._ids[id];
+ this.length--;
+ }
+ }
+ }
+
+ // trigger events
+ if (added.length) {
+ this._trigger('add', { items: added });
+ }
+ if (removed.length) {
+ this._trigger('remove', { items: removed });
+ }
+ };
+
+ /**
+ * Get data from the data view
+ *
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ * get(options: Object, data: Array | DataTable)
+ *
+ * get(id: Number)
+ * get(id: Number, options: Object)
+ * get(id: Number, options: Object, data: Array | DataTable)
+ *
+ * get(ids: Number[])
+ * get(ids: Number[], options: Object)
+ * get(ids: Number[], options: Object, data: Array | DataTable)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [type] Type of data to be returned. Can
+ * be 'DataTable' or 'Array' (default)
+ * {Object.<String, String>} [convert]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ * @param args
+ */
+ DataView.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
+ // get(id(s) [, options] [, data])
+ ids = arguments[0]; // can be a single id or an array with ids
+ options = arguments[1];
+ data = arguments[2];
+ } else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // extend the options with the default options and provided options
+ var viewOptions = util.extend({}, this._options, options);
+
+ // create a combined filter method when needed
+ if (this._options.filter && options && options.filter) {
+ viewOptions.filter = function (item) {
+ return me._options.filter(item) && options.filter(item);
+ };
+ }
+
+ // build up the call to the linked data set
+ var getArguments = [];
+ if (ids != undefined) {
+ getArguments.push(ids);
+ }
+ getArguments.push(viewOptions);
+ getArguments.push(data);
+
+ return this._data && this._data.get.apply(this._data, getArguments);
+ };
+
+ /**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+ DataView.prototype.getIds = function (options) {
+ var ids;
+
+ if (this._data) {
+ var defaultFilter = this._options.filter;
+ var filter;
+
+ if (options && options.filter) {
+ if (defaultFilter) {
+ filter = function (item) {
+ return defaultFilter(item) && options.filter(item);
+ };
+ } else {
+ filter = options.filter;
+ }
+ } else {
+ filter = defaultFilter;
+ }
+
+ ids = this._data.getIds({
+ filter: filter,
+ order: options && options.order
+ });
+ } else {
+ ids = [];
+ }
+
+ return ids;
+ };
+
+ /**
+ * Get the DataSet to which this DataView is connected. In case there is a chain
+ * of multiple DataViews, the root DataSet of this chain is returned.
+ * @return {DataSet} dataSet
+ */
+ DataView.prototype.getDataSet = function () {
+ var dataSet = this;
+ while (dataSet instanceof DataView) {
+ dataSet = dataSet._data;
+ }
+ return dataSet || null;
+ };
+
+ /**
+ * Event listener. Will propagate all events from the connected data set to
+ * the subscribers of the DataView, but will filter the items and only trigger
+ * when there are changes in the filtered data set.
+ * @param {String} event
+ * @param {Object | null} params
+ * @param {String} senderId
+ * @private
+ */
+ DataView.prototype._onEvent = function (event, params, senderId) {
+ var i, len, id, item;
+ var ids = params && params.items;
+ var data = this._data;
+ var updatedData = [];
+ var added = [];
+ var updated = [];
+ var removed = [];
+
+ if (ids && data) {
+ switch (event) {
+ case 'add':
+ // filter the ids of the added items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+ if (item) {
+ this._ids[id] = true;
+ added.push(id);
+ }
+ }
+
+ break;
+
+ case 'update':
+ // determine the event from the views viewpoint: an updated
+ // item can be added, updated, or removed from this view.
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+
+ if (item) {
+ if (this._ids[id]) {
+ updated.push(id);
+ updatedData.push(params.data[i]);
+ } else {
+ this._ids[id] = true;
+ added.push(id);
+ }
+ } else {
+ if (this._ids[id]) {
+ delete this._ids[id];
+ removed.push(id);
+ } else {}
+ }
+ }
+
+ break;
+
+ case 'remove':
+ // filter the ids of the removed items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ if (this._ids[id]) {
+ delete this._ids[id];
+ removed.push(id);
+ }
+ }
+
+ break;
+ }
+
+ this.length += added.length - removed.length;
+
+ if (added.length) {
+ this._trigger('add', { items: added }, senderId);
+ }
+ if (updated.length) {
+ this._trigger('update', { items: updated, data: updatedData }, senderId);
+ }
+ if (removed.length) {
+ this._trigger('remove', { items: removed }, senderId);
+ }
+ }
+ };
+
+ // copy subscription functionality from DataSet
+ DataView.prototype.on = DataSet.prototype.on;
+ DataView.prototype.off = DataSet.prototype.off;
+ DataView.prototype._trigger = DataSet.prototype._trigger;
+
+ // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
+ DataView.prototype.subscribe = DataView.prototype.on;
+ DataView.prototype.unsubscribe = DataView.prototype.off;
+
+ module.exports = DataView;
+
+ // nothing interesting for me :-(
+
+/***/ },
+/* 11 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var Emitter = __webpack_require__(12);
+ var DataSet = __webpack_require__(8);
+ var DataView = __webpack_require__(10);
+ var util = __webpack_require__(1);
+ var Point3d = __webpack_require__(13);
+ var Point2d = __webpack_require__(14);
+ var Camera = __webpack_require__(15);
+ var Filter = __webpack_require__(16);
+ var Slider = __webpack_require__(17);
+ var StepNumber = __webpack_require__(18);
+
+ /**
+ * @constructor Graph3d
+ * Graph3d displays data in 3d.
+ *
+ * Graph3d is developed in javascript as a Google Visualization Chart.
+ *
+ * @param {Element} container The DOM element in which the Graph3d will
+ * be created. Normally a div element.
+ * @param {DataSet | DataView | Array} [data]
+ * @param {Object} [options]
+ */
+ function Graph3d(container, data, options) {
+ if (!(this instanceof Graph3d)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
+ // create variables and set default values
+ this.containerElement = container;
+ this.width = '400px';
+ this.height = '400px';
+ this.margin = 10; // px
+ this.defaultXCenter = '55%';
+ this.defaultYCenter = '50%';
+
+ this.xLabel = 'x';
+ this.yLabel = 'y';
+ this.zLabel = 'z';
+
+ var passValueFn = function passValueFn(v) {
+ return v;
+ };
+ this.xValueLabel = passValueFn;
+ this.yValueLabel = passValueFn;
+ this.zValueLabel = passValueFn;
+
+ this.filterLabel = 'time';
+ this.legendLabel = 'value';
+
+ this.style = Graph3d.STYLE.DOT;
+ this.showPerspective = true;
+ this.showGrid = true;
+ this.keepAspectRatio = true;
+ this.showShadow = false;
+ this.showGrayBottom = false; // TODO: this does not work correctly
+ this.showTooltip = false;
+ this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
+
+ this.animationInterval = 1000; // milliseconds
+ this.animationPreload = false;
+
+ this.camera = new Camera();
+ this.camera.setArmRotation(1.0, 0.5);
+ this.camera.setArmLength(1.7);
+ this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
+
+ this.dataTable = null; // The original data table
+ this.dataPoints = null; // The table with point objects
+
+ // the column indexes
+ this.colX = undefined;
+ this.colY = undefined;
+ this.colZ = undefined;
+ this.colValue = undefined;
+ this.colFilter = undefined;
+
+ this.xMin = 0;
+ this.xStep = undefined; // auto by default
+ this.xMax = 1;
+ this.yMin = 0;
+ this.yStep = undefined; // auto by default
+ this.yMax = 1;
+ this.zMin = 0;
+ this.zStep = undefined; // auto by default
+ this.zMax = 1;
+ this.valueMin = 0;
+ this.valueMax = 1;
+ this.xBarWidth = 1;
+ this.yBarWidth = 1;
+ // TODO: customize axis range
+
+ // colors
+ this.axisColor = '#4D4D4D';
+ this.gridColor = '#D3D3D3';
+ this.dataColor = {
+ fill: '#7DC1FF',
+ stroke: '#3267D2',
+ strokeWidth: 1 // px
+ };
+
+ // create a frame and canvas
+ this.create();
+
+ // apply options (also when undefined)
+ this.setOptions(options);
+
+ // apply data
+ if (data) {
+ this.setData(data);
+ }
+ }
+
+ // Extend Graph3d with an Emitter mixin
+ Emitter(Graph3d.prototype);
+
+ /**
+ * Calculate the scaling values, dependent on the range in x, y, and z direction
+ */
+ Graph3d.prototype._setScale = function () {
+ this.scale = new Point3d(1 / (this.xMax - this.xMin), 1 / (this.yMax - this.yMin), 1 / (this.zMax - this.zMin));
+
+ // keep aspect ration between x and y scale if desired
+ if (this.keepAspectRatio) {
+ if (this.scale.x < this.scale.y) {
+ //noinspection JSSuspiciousNameCombination
+ this.scale.y = this.scale.x;
+ } else {
+ //noinspection JSSuspiciousNameCombination
+ this.scale.x = this.scale.y;
+ }
+ }
+
+ // scale the vertical axis
+ this.scale.z *= this.verticalRatio;
+ // TODO: can this be automated? verticalRatio?
+
+ // determine scale for (optional) value
+ this.scale.value = 1 / (this.valueMax - this.valueMin);
+
+ // position the camera arm
+ var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
+ var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
+ var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
+ this.camera.setArmLocation(xCenter, yCenter, zCenter);
+ };
+
+ /**
+ * Convert a 3D location to a 2D location on screen
+ * http://en.wikipedia.org/wiki/3D_projection
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
+ * @return {Point2d} point2d A 2D point with parameters x, y
+ */
+ Graph3d.prototype._convert3Dto2D = function (point3d) {
+ var translation = this._convertPointToTranslation(point3d);
+ return this._convertTranslationToScreen(translation);
+ };
+
+ /**
+ * Convert a 3D location its translation seen from the camera
+ * http://en.wikipedia.org/wiki/3D_projection
+ * @param {Point3d} point3d A 3D point with parameters x, y, z
+ * @return {Point3d} translation A 3D point with parameters x, y, z This is
+ * the translation of the point, seen from the
+ * camera
+ */
+ Graph3d.prototype._convertPointToTranslation = function (point3d) {
+ var ax = point3d.x * this.scale.x,
+ ay = point3d.y * this.scale.y,
+ az = point3d.z * this.scale.z,
+ cx = this.camera.getCameraLocation().x,
+ cy = this.camera.getCameraLocation().y,
+ cz = this.camera.getCameraLocation().z,
+
+ // calculate angles
+ sinTx = Math.sin(this.camera.getCameraRotation().x),
+ cosTx = Math.cos(this.camera.getCameraRotation().x),
+ sinTy = Math.sin(this.camera.getCameraRotation().y),
+ cosTy = Math.cos(this.camera.getCameraRotation().y),
+ sinTz = Math.sin(this.camera.getCameraRotation().z),
+ cosTz = Math.cos(this.camera.getCameraRotation().z),
+
+ // calculate translation
+ dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
+ dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax - cx)),
+ dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax - cx));
+
+ return new Point3d(dx, dy, dz);
+ };
+
+ /**
+ * Convert a translation point to a point on the screen
+ * @param {Point3d} translation A 3D point with parameters x, y, z This is
+ * the translation of the point, seen from the
+ * camera
+ * @return {Point2d} point2d A 2D point with parameters x, y
+ */
+ Graph3d.prototype._convertTranslationToScreen = function (translation) {
+ var ex = this.eye.x,
+ ey = this.eye.y,
+ ez = this.eye.z,
+ dx = translation.x,
+ dy = translation.y,
+ dz = translation.z;
+
+ // calculate position on screen from translation
+ var bx;
+ var by;
+ if (this.showPerspective) {
+ bx = (dx - ex) * (ez / dz);
+ by = (dy - ey) * (ez / dz);
+ } else {
+ bx = dx * -(ez / this.camera.getArmLength());
+ by = dy * -(ez / this.camera.getArmLength());
+ }
+
+ // shift and scale the point to the center of the screen
+ // use the width of the graph to scale both horizontally and vertically.
+ return new Point2d(this.xcenter + bx * this.frame.canvas.clientWidth, this.ycenter - by * this.frame.canvas.clientWidth);
+ };
+
+ /**
+ * Set the background styling for the graph
+ * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
+ */
+ Graph3d.prototype._setBackgroundColor = function (backgroundColor) {
+ var fill = 'white';
+ var stroke = 'gray';
+ var strokeWidth = 1;
+
+ if (typeof backgroundColor === 'string') {
+ fill = backgroundColor;
+ stroke = 'none';
+ strokeWidth = 0;
+ } else if (typeof backgroundColor === 'object') {
+ if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
+ if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
+ if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
+ } else if (backgroundColor === undefined) {} else {
+ throw 'Unsupported type of backgroundColor';
+ }
+
+ this.frame.style.backgroundColor = fill;
+ this.frame.style.borderColor = stroke;
+ this.frame.style.borderWidth = strokeWidth + 'px';
+ this.frame.style.borderStyle = 'solid';
+ };
+
+ /// enumerate the available styles
+ Graph3d.STYLE = {
+ BAR: 0,
+ BARCOLOR: 1,
+ BARSIZE: 2,
+ DOT: 3,
+ DOTLINE: 4,
+ DOTCOLOR: 5,
+ DOTSIZE: 6,
+ GRID: 7,
+ LINE: 8,
+ SURFACE: 9
+ };
+
+ /**
+ * Retrieve the style index from given styleName
+ * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
+ * @return {Number} styleNumber Enumeration value representing the style, or -1
+ * when not found
+ */
+ Graph3d.prototype._getStyleNumber = function (styleName) {
+ switch (styleName) {
+ case 'dot':
+ return Graph3d.STYLE.DOT;
+ case 'dot-line':
+ return Graph3d.STYLE.DOTLINE;
+ case 'dot-color':
+ return Graph3d.STYLE.DOTCOLOR;
+ case 'dot-size':
+ return Graph3d.STYLE.DOTSIZE;
+ case 'line':
+ return Graph3d.STYLE.LINE;
+ case 'grid':
+ return Graph3d.STYLE.GRID;
+ case 'surface':
+ return Graph3d.STYLE.SURFACE;
+ case 'bar':
+ return Graph3d.STYLE.BAR;
+ case 'bar-color':
+ return Graph3d.STYLE.BARCOLOR;
+ case 'bar-size':
+ return Graph3d.STYLE.BARSIZE;
+ }
+
+ return -1;
+ };
+
+ /**
+ * Determine the indexes of the data columns, based on the given style and data
+ * @param {DataSet} data
+ * @param {Number} style
+ */
+ Graph3d.prototype._determineColumnIndexes = function (data, style) {
+ if (this.style === Graph3d.STYLE.DOT || this.style === Graph3d.STYLE.DOTLINE || this.style === Graph3d.STYLE.LINE || this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE || this.style === Graph3d.STYLE.BAR) {
+ // 3 columns expected, and optionally a 4th with filter values
+ this.colX = 0;
+ this.colY = 1;
+ this.colZ = 2;
+ this.colValue = undefined;
+
+ if (data.getNumberOfColumns() > 3) {
+ this.colFilter = 3;
+ }
+ } else if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) {
+ // 4 columns expected, and optionally a 5th with filter values
+ this.colX = 0;
+ this.colY = 1;
+ this.colZ = 2;
+ this.colValue = 3;
+
+ if (data.getNumberOfColumns() > 4) {
+ this.colFilter = 4;
+ }
+ } else {
+ throw 'Unknown style "' + this.style + '"';
+ }
+ };
+
+ Graph3d.prototype.getNumberOfRows = function (data) {
+ return data.length;
+ };
+
+ Graph3d.prototype.getNumberOfColumns = function (data) {
+ var counter = 0;
+ for (var column in data[0]) {
+ if (data[0].hasOwnProperty(column)) {
+ counter++;
+ }
+ }
+ return counter;
+ };
+
+ Graph3d.prototype.getDistinctValues = function (data, column) {
+ var distinctValues = [];
+ for (var i = 0; i < data.length; i++) {
+ if (distinctValues.indexOf(data[i][column]) == -1) {
+ distinctValues.push(data[i][column]);
+ }
+ }
+ return distinctValues;
+ };
+
+ Graph3d.prototype.getColumnRange = function (data, column) {
+ var minMax = { min: data[0][column], max: data[0][column] };
+ for (var i = 0; i < data.length; i++) {
+ if (minMax.min > data[i][column]) {
+ minMax.min = data[i][column];
+ }
+ if (minMax.max < data[i][column]) {
+ minMax.max = data[i][column];
+ }
+ }
+ return minMax;
+ };
+
+ /**
+ * Initialize the data from the data table. Calculate minimum and maximum values
+ * and column index values
+ * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
+ * @param {Number} style Style Number
+ */
+ Graph3d.prototype._dataInitialize = function (rawData, style) {
+ var me = this;
+
+ // unsubscribe from the dataTable
+ if (this.dataSet) {
+ this.dataSet.off('*', this._onChange);
+ }
+
+ if (rawData === undefined) return;
+
+ if (Array.isArray(rawData)) {
+ rawData = new DataSet(rawData);
+ }
+
+ var data;
+ if (rawData instanceof DataSet || rawData instanceof DataView) {
+ data = rawData.get();
+ } else {
+ throw new Error('Array, DataSet, or DataView expected');
+ }
+
+ if (data.length == 0) return;
+
+ this.dataSet = rawData;
+ this.dataTable = data;
+
+ // subscribe to changes in the dataset
+ this._onChange = function () {
+ me.setData(me.dataSet);
+ };
+ this.dataSet.on('*', this._onChange);
+
+ // _determineColumnIndexes
+ // getNumberOfRows (points)
+ // getNumberOfColumns (x,y,z,v,t,t1,t2...)
+ // getDistinctValues (unique values?)
+ // getColumnRange
+
+ // determine the location of x,y,z,value,filter columns
+ this.colX = 'x';
+ this.colY = 'y';
+ this.colZ = 'z';
+ this.colValue = 'style';
+ this.colFilter = 'filter';
+
+ // check if a filter column is provided
+ if (data[0].hasOwnProperty('filter')) {
+ if (this.dataFilter === undefined) {
+ this.dataFilter = new Filter(rawData, this.colFilter, this);
+ this.dataFilter.setOnLoadCallback(function () {
+ me.redraw();
+ });
+ }
+ }
+
+ var withBars = this.style == Graph3d.STYLE.BAR || this.style == Graph3d.STYLE.BARCOLOR || this.style == Graph3d.STYLE.BARSIZE;
+
+ // determine barWidth from data
+ if (withBars) {
+ if (this.defaultXBarWidth !== undefined) {
+ this.xBarWidth = this.defaultXBarWidth;
+ } else {
+ var dataX = this.getDistinctValues(data, this.colX);
+ this.xBarWidth = dataX[1] - dataX[0] || 1;
+ }
+
+ if (this.defaultYBarWidth !== undefined) {
+ this.yBarWidth = this.defaultYBarWidth;
+ } else {
+ var dataY = this.getDistinctValues(data, this.colY);
+ this.yBarWidth = dataY[1] - dataY[0] || 1;
+ }
+ }
+
+ // calculate minimums and maximums
+ var xRange = this.getColumnRange(data, this.colX);
+ if (withBars) {
+ xRange.min -= this.xBarWidth / 2;
+ xRange.max += this.xBarWidth / 2;
+ }
+ this.xMin = this.defaultXMin !== undefined ? this.defaultXMin : xRange.min;
+ this.xMax = this.defaultXMax !== undefined ? this.defaultXMax : xRange.max;
+ if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
+ this.xStep = this.defaultXStep !== undefined ? this.defaultXStep : (this.xMax - this.xMin) / 5;
+
+ var yRange = this.getColumnRange(data, this.colY);
+ if (withBars) {
+ yRange.min -= this.yBarWidth / 2;
+ yRange.max += this.yBarWidth / 2;
+ }
+ this.yMin = this.defaultYMin !== undefined ? this.defaultYMin : yRange.min;
+ this.yMax = this.defaultYMax !== undefined ? this.defaultYMax : yRange.max;
+ if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
+ this.yStep = this.defaultYStep !== undefined ? this.defaultYStep : (this.yMax - this.yMin) / 5;
+
+ var zRange = this.getColumnRange(data, this.colZ);
+ this.zMin = this.defaultZMin !== undefined ? this.defaultZMin : zRange.min;
+ this.zMax = this.defaultZMax !== undefined ? this.defaultZMax : zRange.max;
+ if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
+ this.zStep = this.defaultZStep !== undefined ? this.defaultZStep : (this.zMax - this.zMin) / 5;
+
+ if (this.colValue !== undefined) {
+ var valueRange = this.getColumnRange(data, this.colValue);
+ this.valueMin = this.defaultValueMin !== undefined ? this.defaultValueMin : valueRange.min;
+ this.valueMax = this.defaultValueMax !== undefined ? this.defaultValueMax : valueRange.max;
+ if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
+ }
+
+ // set the scale dependent on the ranges.
+ this._setScale();
+ };
+
+ /**
+ * Filter the data based on the current filter
+ * @param {Array} data
+ * @return {Array} dataPoints Array with point objects which can be drawn on screen
+ */
+ Graph3d.prototype._getDataPoints = function (data) {
+ // TODO: store the created matrix dataPoints in the filters instead of reloading each time
+ var x, y, i, z, obj, point;
+
+ var dataPoints = [];
+
+ if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) {
+ // copy all values from the google data table to a matrix
+ // the provided values are supposed to form a grid of (x,y) positions
+
+ // create two lists with all present x and y values
+ var dataX = [];
+ var dataY = [];
+ for (i = 0; i < this.getNumberOfRows(data); i++) {
+ x = data[i][this.colX] || 0;
+ y = data[i][this.colY] || 0;
+
+ if (dataX.indexOf(x) === -1) {
+ dataX.push(x);
+ }
+ if (dataY.indexOf(y) === -1) {
+ dataY.push(y);
+ }
+ }
+
+ var sortNumber = function sortNumber(a, b) {
+ return a - b;
+ };
+ dataX.sort(sortNumber);
+ dataY.sort(sortNumber);
+
+ // create a grid, a 2d matrix, with all values.
+ var dataMatrix = []; // temporary data matrix
+ for (i = 0; i < data.length; i++) {
+ x = data[i][this.colX] || 0;
+ y = data[i][this.colY] || 0;
+ z = data[i][this.colZ] || 0;
+
+ var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
+ var yIndex = dataY.indexOf(y);
+
+ if (dataMatrix[xIndex] === undefined) {
+ dataMatrix[xIndex] = [];
+ }
+
+ var point3d = new Point3d();
+ point3d.x = x;
+ point3d.y = y;
+ point3d.z = z;
+
+ obj = {};
+ obj.point = point3d;
+ obj.trans = undefined;
+ obj.screen = undefined;
+ obj.bottom = new Point3d(x, y, this.zMin);
+
+ dataMatrix[xIndex][yIndex] = obj;
+
+ dataPoints.push(obj);
+ }
+
+ // fill in the pointers to the neighbors.
+ for (x = 0; x < dataMatrix.length; x++) {
+ for (y = 0; y < dataMatrix[x].length; y++) {
+ if (dataMatrix[x][y]) {
+ dataMatrix[x][y].pointRight = x < dataMatrix.length - 1 ? dataMatrix[x + 1][y] : undefined;
+ dataMatrix[x][y].pointTop = y < dataMatrix[x].length - 1 ? dataMatrix[x][y + 1] : undefined;
+ dataMatrix[x][y].pointCross = x < dataMatrix.length - 1 && y < dataMatrix[x].length - 1 ? dataMatrix[x + 1][y + 1] : undefined;
+ }
+ }
+ }
+ } else {
+ // 'dot', 'dot-line', etc.
+ // copy all values from the google data table to a list with Point3d objects
+ for (i = 0; i < data.length; i++) {
+ point = new Point3d();
+ point.x = data[i][this.colX] || 0;
+ point.y = data[i][this.colY] || 0;
+ point.z = data[i][this.colZ] || 0;
+
+ if (this.colValue !== undefined) {
+ point.value = data[i][this.colValue] || 0;
+ }
+
+ obj = {};
+ obj.point = point;
+ obj.bottom = new Point3d(point.x, point.y, this.zMin);
+ obj.trans = undefined;
+ obj.screen = undefined;
+
+ dataPoints.push(obj);
+ }
+ }
+
+ return dataPoints;
+ };
+
+ /**
+ * Create the main frame for the Graph3d.
+ * This function is executed once when a Graph3d object is created. The frame
+ * contains a canvas, and this canvas contains all objects like the axis and
+ * nodes.
+ */
+ Graph3d.prototype.create = function () {
+ // remove all elements from the container element.
+ while (this.containerElement.hasChildNodes()) {
+ this.containerElement.removeChild(this.containerElement.firstChild);
+ }
+
+ this.frame = document.createElement('div');
+ this.frame.style.position = 'relative';
+ this.frame.style.overflow = 'hidden';
+
+ // create the graph canvas (HTML canvas element)
+ this.frame.canvas = document.createElement('canvas');
+ this.frame.canvas.style.position = 'relative';
+ this.frame.appendChild(this.frame.canvas);
+ //if (!this.frame.canvas.getContext) {
+ {
+ var noCanvas = document.createElement('DIV');
+ noCanvas.style.color = 'red';
+ noCanvas.style.fontWeight = 'bold';
+ noCanvas.style.padding = '10px';
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
+ this.frame.canvas.appendChild(noCanvas);
+ }
+
+ this.frame.filter = document.createElement('div');
+ this.frame.filter.style.position = 'absolute';
+ this.frame.filter.style.bottom = '0px';
+ this.frame.filter.style.left = '0px';
+ this.frame.filter.style.width = '100%';
+ this.frame.appendChild(this.frame.filter);
+
+ // add event listeners to handle moving and zooming the contents
+ var me = this;
+ var onmousedown = function onmousedown(event) {
+ me._onMouseDown(event);
+ };
+ var ontouchstart = function ontouchstart(event) {
+ me._onTouchStart(event);
+ };
+ var onmousewheel = function onmousewheel(event) {
+ me._onWheel(event);
+ };
+ var ontooltip = function ontooltip(event) {
+ me._onTooltip(event);
+ };
+ // TODO: these events are never cleaned up... can give a 'memory leakage'
+
+ util.addEventListener(this.frame.canvas, 'keydown', onkeydown);
+ util.addEventListener(this.frame.canvas, 'mousedown', onmousedown);
+ util.addEventListener(this.frame.canvas, 'touchstart', ontouchstart);
+ util.addEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
+ util.addEventListener(this.frame.canvas, 'mousemove', ontooltip);
+
+ // add the new graph to the container element
+ this.containerElement.appendChild(this.frame);
+ };
+
+ /**
+ * Set a new size for the graph
+ * @param {string} width Width in pixels or percentage (for example '800px'
+ * or '50%')
+ * @param {string} height Height in pixels or percentage (for example '400px'
+ * or '30%')
+ */
+ Graph3d.prototype.setSize = function (width, height) {
+ this.frame.style.width = width;
+ this.frame.style.height = height;
+
+ this._resizeCanvas();
+ };
+
+ /**
+ * Resize the canvas to the current size of the frame
+ */
+ Graph3d.prototype._resizeCanvas = function () {
+ this.frame.canvas.style.width = '100%';
+ this.frame.canvas.style.height = '100%';
+
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
+
+ // adjust with for margin
+ this.frame.filter.style.width = this.frame.canvas.clientWidth - 2 * 10 + 'px';
+ };
+
+ /**
+ * Start animation
+ */
+ Graph3d.prototype.animationStart = function () {
+ if (!this.frame.filter || !this.frame.filter.slider) throw 'No animation available';
+
+ this.frame.filter.slider.play();
+ };
+
+ /**
+ * Stop animation
+ */
+ Graph3d.prototype.animationStop = function () {
+ if (!this.frame.filter || !this.frame.filter.slider) return;
+
+ this.frame.filter.slider.stop();
+ };
+
+ /**
+ * Resize the center position based on the current values in this.defaultXCenter
+ * and this.defaultYCenter (which are strings with a percentage or a value
+ * in pixels). The center positions are the variables this.xCenter
+ * and this.yCenter
+ */
+ Graph3d.prototype._resizeCenter = function () {
+ // calculate the horizontal center position
+ if (this.defaultXCenter.charAt(this.defaultXCenter.length - 1) === '%') {
+ this.xcenter = parseFloat(this.defaultXCenter) / 100 * this.frame.canvas.clientWidth;
+ } else {
+ this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
+ }
+
+ // calculate the vertical center position
+ if (this.defaultYCenter.charAt(this.defaultYCenter.length - 1) === '%') {
+ this.ycenter = parseFloat(this.defaultYCenter) / 100 * (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
+ } else {
+ this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
+ }
+ };
+
+ /**
+ * Set the rotation and distance of the camera
+ * @param {Object} pos An object with the camera position. The object
+ * contains three parameters:
+ * - horizontal {Number}
+ * The horizontal rotation, between 0 and 2*PI.
+ * Optional, can be left undefined.
+ * - vertical {Number}
+ * The vertical rotation, between 0 and 0.5*PI
+ * if vertical=0.5*PI, the graph is shown from the
+ * top. Optional, can be left undefined.
+ * - distance {Number}
+ * The (normalized) distance of the camera to the
+ * center of the graph, a value between 0.71 and 5.0.
+ * Optional, can be left undefined.
+ */
+ Graph3d.prototype.setCameraPosition = function (pos) {
+ if (pos === undefined) {
+ return;
+ }
+
+ if (pos.horizontal !== undefined && pos.vertical !== undefined) {
+ this.camera.setArmRotation(pos.horizontal, pos.vertical);
+ }
+
+ if (pos.distance !== undefined) {
+ this.camera.setArmLength(pos.distance);
+ }
+
+ this.redraw();
+ };
+
+ /**
+ * Retrieve the current camera rotation
+ * @return {object} An object with parameters horizontal, vertical, and
+ * distance
+ */
+ Graph3d.prototype.getCameraPosition = function () {
+ var pos = this.camera.getArmRotation();
+ pos.distance = this.camera.getArmLength();
+ return pos;
+ };
+
+ /**
+ * Load data into the 3D Graph
+ */
+ Graph3d.prototype._readData = function (data) {
+ // read the data
+ this._dataInitialize(data, this.style);
+
+ if (this.dataFilter) {
+ // apply filtering
+ this.dataPoints = this.dataFilter._getDataPoints();
+ } else {
+ // no filtering. load all data
+ this.dataPoints = this._getDataPoints(this.dataTable);
+ }
+
+ // draw the filter
+ this._redrawFilter();
+ };
+
+ /**
+ * Replace the dataset of the Graph3d
+ * @param {Array | DataSet | DataView} data
+ */
+ Graph3d.prototype.setData = function (data) {
+ this._readData(data);
+ this.redraw();
+
+ // start animation when option is true
+ if (this.animationAutoStart && this.dataFilter) {
+ this.animationStart();
+ }
+ };
+
+ /**
+ * Update the options. Options will be merged with current options
+ * @param {Object} options
+ */
+ Graph3d.prototype.setOptions = function (options) {
+ var cameraPosition = undefined;
+
+ this.animationStop();
+
+ if (options !== undefined) {
+ // retrieve parameter values
+ if (options.width !== undefined) this.width = options.width;
+ if (options.height !== undefined) this.height = options.height;
+
+ if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
+ if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
+
+ if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
+ if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
+ if (options.xLabel !== undefined) this.xLabel = options.xLabel;
+ if (options.yLabel !== undefined) this.yLabel = options.yLabel;
+ if (options.zLabel !== undefined) this.zLabel = options.zLabel;
+
+ if (options.xValueLabel !== undefined) this.xValueLabel = options.xValueLabel;
+ if (options.yValueLabel !== undefined) this.yValueLabel = options.yValueLabel;
+ if (options.zValueLabel !== undefined) this.zValueLabel = options.zValueLabel;
+
+ if (options.style !== undefined) {
+ var styleNumber = this._getStyleNumber(options.style);
+ if (styleNumber !== -1) {
+ this.style = styleNumber;
+ }
+ }
+ if (options.showGrid !== undefined) this.showGrid = options.showGrid;
+ if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
+ if (options.showShadow !== undefined) this.showShadow = options.showShadow;
+ if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
+ if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
+ if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
+ if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
+
+ if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
+ if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
+ if (options.animationAutoStart !== undefined) this.animationAutoStart = options.animationAutoStart;
+
+ if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
+ if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
+
+ if (options.xMin !== undefined) this.defaultXMin = options.xMin;
+ if (options.xStep !== undefined) this.defaultXStep = options.xStep;
+ if (options.xMax !== undefined) this.defaultXMax = options.xMax;
+ if (options.yMin !== undefined) this.defaultYMin = options.yMin;
+ if (options.yStep !== undefined) this.defaultYStep = options.yStep;
+ if (options.yMax !== undefined) this.defaultYMax = options.yMax;
+ if (options.zMin !== undefined) this.defaultZMin = options.zMin;
+ if (options.zStep !== undefined) this.defaultZStep = options.zStep;
+ if (options.zMax !== undefined) this.defaultZMax = options.zMax;
+ if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
+ if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
+ if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor);
+
+ if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
+
+ if (cameraPosition !== undefined) {
+ this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
+ this.camera.setArmLength(cameraPosition.distance);
+ }
+
+ // colors
+ if (options.axisColor !== undefined) this.axisColor = options.axisColor;
+ if (options.gridColor !== undefined) this.gridColor = options.gridColor;
+ if (options.dataColor) {
+ if (typeof options.dataColor === 'string') {
+ this.dataColor.fill = options.dataColor;
+ this.dataColor.stroke = options.dataColor;
+ } else {
+ if (options.dataColor.fill) {
+ this.dataColor.fill = options.dataColor.fill;
+ }
+ if (options.dataColor.stroke) {
+ this.dataColor.stroke = options.dataColor.stroke;
+ }
+ if (options.dataColor.strokeWidth !== undefined) {
+ this.dataColor.strokeWidth = options.dataColor.strokeWidth;
+ }
+ }
+ }
+ }
+
+ this.setSize(this.width, this.height);
+
+ // re-load the data
+ if (this.dataTable) {
+ this.setData(this.dataTable);
+ }
+
+ // start animation when option is true
+ if (this.animationAutoStart && this.dataFilter) {
+ this.animationStart();
+ }
+ };
+
+ /**
+ * Redraw the Graph.
+ */
+ Graph3d.prototype.redraw = function () {
+ if (this.dataPoints === undefined) {
+ throw 'Error: graph data not initialized';
+ }
+
+ this._resizeCanvas();
+ this._resizeCenter();
+ this._redrawSlider();
+ this._redrawClear();
+ this._redrawAxis();
+
+ if (this.style === Graph3d.STYLE.GRID || this.style === Graph3d.STYLE.SURFACE) {
+ this._redrawDataGrid();
+ } else if (this.style === Graph3d.STYLE.LINE) {
+ this._redrawDataLine();
+ } else if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) {
+ this._redrawDataBar();
+ } else {
+ // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
+ this._redrawDataDot();
+ }
+
+ this._redrawInfo();
+ this._redrawLegend();
+ };
+
+ /**
+ * Clear the canvas before redrawing
+ */
+ Graph3d.prototype._redrawClear = function () {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ };
+
+ /**
+ * Redraw the legend showing the colors
+ */
+ Graph3d.prototype._redrawLegend = function () {
+ var y;
+
+ if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) {
+
+ var dotSize = this.frame.clientWidth * 0.02;
+
+ var widthMin, widthMax;
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ widthMin = dotSize / 2; // px
+ widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
+ } else {
+ widthMin = 20; // px
+ widthMax = 20; // px
+ }
+
+ var height = Math.max(this.frame.clientHeight * 0.25, 100);
+ var top = this.margin;
+ var right = this.frame.clientWidth - this.margin;
+ var left = right - widthMax;
+ var bottom = top + height;
+ }
+
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ ctx.lineWidth = 1;
+ ctx.font = '14px arial'; // TODO: put in options
+
+ if (this.style === Graph3d.STYLE.DOTCOLOR) {
+ // draw the color bar
+ var ymin = 0;
+ var ymax = height; // Todo: make height customizable
+ for (y = ymin; y < ymax; y++) {
+ var f = (y - ymin) / (ymax - ymin);
+
+ //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
+ var hue = f * 240;
+ var color = this._hsv2rgb(hue, 1, 1);
+
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(left, top + y);
+ ctx.lineTo(right, top + y);
+ ctx.stroke();
+ }
+
+ ctx.strokeStyle = this.axisColor;
+ ctx.strokeRect(left, top, widthMax, height);
+ }
+
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ // draw border around color bar
+ ctx.strokeStyle = this.axisColor;
+ ctx.fillStyle = this.dataColor.fill;
+ ctx.beginPath();
+ ctx.moveTo(left, top);
+ ctx.lineTo(right, top);
+ ctx.lineTo(right - widthMax + widthMin, bottom);
+ ctx.lineTo(left, bottom);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
+
+ if (this.style === Graph3d.STYLE.DOTCOLOR || this.style === Graph3d.STYLE.DOTSIZE) {
+ // print values along the color bar
+ var gridLineLen = 5; // px
+ var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax - this.valueMin) / 5, true);
+ step.start();
+ if (step.getCurrent() < this.valueMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
+
+ ctx.beginPath();
+ ctx.moveTo(left - gridLineLen, y);
+ ctx.lineTo(left, y);
+ ctx.stroke();
+
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
+
+ step.next();
+ }
+
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'top';
+ var label = this.legendLabel;
+ ctx.fillText(label, right, bottom + this.margin);
+ }
+ };
+
+ /**
+ * Redraw the filter
+ */
+ Graph3d.prototype._redrawFilter = function () {
+ this.frame.filter.innerHTML = '';
+
+ if (this.dataFilter) {
+ var options = {
+ 'visible': this.showAnimationControls
+ };
+ var slider = new Slider(this.frame.filter, options);
+ this.frame.filter.slider = slider;
+
+ // TODO: css here is not nice here...
+ this.frame.filter.style.padding = '10px';
+ //this.frame.filter.style.backgroundColor = '#EFEFEF';
+
+ slider.setValues(this.dataFilter.values);
+ slider.setPlayInterval(this.animationInterval);
+
+ // create an event handler
+ var me = this;
+ var onchange = function onchange() {
+ var index = slider.getIndex();
+
+ me.dataFilter.selectValue(index);
+ me.dataPoints = me.dataFilter._getDataPoints();
+
+ me.redraw();
+ };
+ slider.setOnChangeCallback(onchange);
+ } else {
+ this.frame.filter.slider = undefined;
+ }
+ };
+
+ /**
+ * Redraw the slider
+ */
+ Graph3d.prototype._redrawSlider = function () {
+ if (this.frame.filter.slider !== undefined) {
+ this.frame.filter.slider.redraw();
+ }
+ };
+
+ /**
+ * Redraw common information
+ */
+ Graph3d.prototype._redrawInfo = function () {
+ if (this.dataFilter) {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+
+ ctx.font = '14px arial'; // TODO: put in options
+ ctx.lineStyle = 'gray';
+ ctx.fillStyle = 'gray';
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'top';
+
+ var x = this.margin;
+ var y = this.margin;
+ ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
+ }
+ };
+
+ /**
+ * Redraw the axis
+ */
+ Graph3d.prototype._redrawAxis = function () {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ from,
+ to,
+ step,
+ prettyStep,
+ text,
+ xText,
+ yText,
+ zText,
+ offset,
+ xOffset,
+ yOffset,
+ xMin2d,
+ xMax2d;
+
+ // TODO: get the actual rendered style of the containerElement
+ //ctx.font = this.containerElement.style.font;
+ ctx.font = 24 / this.camera.getArmLength() + 'px arial';
+
+ // calculate the length for the short grid lines
+ var gridLenX = 0.025 / this.scale.x;
+ var gridLenY = 0.025 / this.scale.y;
+ var textMargin = 5 / this.camera.getArmLength(); // px
+ var armAngle = this.camera.getArmRotation().horizontal;
+
+ // draw x-grid lines
+ ctx.lineWidth = 1;
+ prettyStep = this.defaultXStep === undefined;
+ step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.xMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ var x = step.getCurrent();
+
+ if (this.showGrid) {
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+ ctx.strokeStyle = this.gridColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ } else {
+ from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMin + gridLenX, this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
+ to = this._convert3Dto2D(new Point3d(x, this.yMax - gridLenX, this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+
+ yText = Math.cos(armAngle) > 0 ? this.yMin : this.yMax;
+ text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
+ if (Math.cos(armAngle * 2) > 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ text.y += textMargin;
+ } else if (Math.sin(armAngle * 2) < 0) {
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ } else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(' ' + this.xValueLabel(step.getCurrent()) + ' ', text.x, text.y);
+
+ step.next();
+ }
+
+ // draw y-grid lines
+ ctx.lineWidth = 1;
+ prettyStep = this.defaultYStep === undefined;
+ step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.yMin) {
+ step.next();
+ }
+ while (!step.end()) {
+ if (this.showGrid) {
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.gridColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ } else {
+ from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMin + gridLenY, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax - gridLenY, step.getCurrent(), this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ }
+
+ xText = Math.sin(armAngle) > 0 ? this.xMin : this.xMax;
+ text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
+ if (Math.cos(armAngle * 2) < 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ text.y += textMargin;
+ } else if (Math.sin(armAngle * 2) > 0) {
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ } else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(' ' + this.yValueLabel(step.getCurrent()) + ' ', text.x, text.y);
+
+ step.next();
+ }
+
+ // draw z-grid lines and axis
+ ctx.lineWidth = 1;
+ prettyStep = this.defaultZStep === undefined;
+ step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
+ step.start();
+ if (step.getCurrent() < this.zMin) {
+ step.next();
+ }
+ xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax;
+ yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax;
+ while (!step.end()) {
+ // TODO: make z-grid lines really 3d?
+ from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(from.x - textMargin, from.y);
+ ctx.stroke();
+
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(this.zValueLabel(step.getCurrent()) + ' ', from.x - 5, from.y);
+
+ step.next();
+ }
+ ctx.lineWidth = 1;
+ from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ // draw x-axis
+ ctx.lineWidth = 1;
+ // line at yMin
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(xMin2d.x, xMin2d.y);
+ ctx.lineTo(xMax2d.x, xMax2d.y);
+ ctx.stroke();
+ // line at ymax
+ xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+ xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(xMin2d.x, xMin2d.y);
+ ctx.lineTo(xMax2d.x, xMax2d.y);
+ ctx.stroke();
+
+ // draw y-axis
+ ctx.lineWidth = 1;
+ // line at xMin
+ from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+ // line at xMax
+ from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
+ to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
+ ctx.strokeStyle = this.axisColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(to.x, to.y);
+ ctx.stroke();
+
+ // draw x-label
+ var xLabel = this.xLabel;
+ if (xLabel.length > 0) {
+ yOffset = 0.1 / this.scale.y;
+ xText = (this.xMin + this.xMax) / 2;
+ yText = Math.cos(armAngle) > 0 ? this.yMin - yOffset : this.yMax + yOffset;
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ if (Math.cos(armAngle * 2) > 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ } else if (Math.sin(armAngle * 2) < 0) {
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ } else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(xLabel, text.x, text.y);
+ }
+
+ // draw y-label
+ var yLabel = this.yLabel;
+ if (yLabel.length > 0) {
+ xOffset = 0.1 / this.scale.x;
+ xText = Math.sin(armAngle) > 0 ? this.xMin - xOffset : this.xMax + xOffset;
+ yText = (this.yMin + this.yMax) / 2;
+ text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
+ if (Math.cos(armAngle * 2) < 0) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ } else if (Math.sin(armAngle * 2) > 0) {
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ } else {
+ ctx.textAlign = 'left';
+ ctx.textBaseline = 'middle';
+ }
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(yLabel, text.x, text.y);
+ }
+
+ // draw z-label
+ var zLabel = this.zLabel;
+ if (zLabel.length > 0) {
+ offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
+ xText = Math.cos(armAngle) > 0 ? this.xMin : this.xMax;
+ yText = Math.sin(armAngle) < 0 ? this.yMin : this.yMax;
+ zText = (this.zMin + this.zMax) / 2;
+ text = this._convert3Dto2D(new Point3d(xText, yText, zText));
+ ctx.textAlign = 'right';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = this.axisColor;
+ ctx.fillText(zLabel, text.x - offset, text.y);
+ }
+ };
+
+ /**
+ * Calculate the color based on the given value.
+ * @param {Number} H Hue, a value be between 0 and 360
+ * @param {Number} S Saturation, a value between 0 and 1
+ * @param {Number} V Value, a value between 0 and 1
+ */
+ Graph3d.prototype._hsv2rgb = function (H, S, V) {
+ var R, G, B, C, Hi, X;
+
+ C = V * S;
+ Hi = Math.floor(H / 60); // hi = 0,1,2,3,4,5
+ X = C * (1 - Math.abs(H / 60 % 2 - 1));
+
+ switch (Hi) {
+ case 0:
+ R = C;G = X;B = 0;break;
+ case 1:
+ R = X;G = C;B = 0;break;
+ case 2:
+ R = 0;G = C;B = X;break;
+ case 3:
+ R = 0;G = X;B = C;break;
+ case 4:
+ R = X;G = 0;B = C;break;
+ case 5:
+ R = C;G = 0;B = X;break;
+
+ default:
+ R = 0;G = 0;B = 0;break;
+ }
+
+ return 'RGB(' + parseInt(R * 255) + ',' + parseInt(G * 255) + ',' + parseInt(B * 255) + ')';
+ };
+
+ /**
+ * Draw all datapoints as a grid
+ * This function can be used when the style is 'grid'
+ */
+ Graph3d.prototype._redrawDataGrid = function () {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ point,
+ right,
+ top,
+ cross,
+ i,
+ topSideVisible,
+ fillStyle,
+ strokeStyle,
+ lineWidth,
+ h,
+ s,
+ v,
+ zAvg;
+
+ ctx.lineJoin = 'round';
+ ctx.lineCap = 'round';
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+ // calculate the translations and screen position of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+
+ // calculate the translation of the point at the bottom (needed for sorting)
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
+
+ // sort the points on depth of their (x,y) position (not on z)
+ var sortDepth = function sortDepth(a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
+
+ if (this.style === Graph3d.STYLE.SURFACE) {
+ for (i = 0; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ right = this.dataPoints[i].pointRight;
+ top = this.dataPoints[i].pointTop;
+ cross = this.dataPoints[i].pointCross;
+
+ if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
+
+ if (this.showGrayBottom || this.showShadow) {
+ // calculate the cross product of the two vectors from center
+ // to left and right, in order to know whether we are looking at the
+ // bottom or at the top side. We can also use the cross product
+ // for calculating light intensity
+ var aDiff = Point3d.subtract(cross.trans, point.trans);
+ var bDiff = Point3d.subtract(top.trans, right.trans);
+ var crossproduct = Point3d.crossProduct(aDiff, bDiff);
+ var len = crossproduct.length();
+ // FIXME: there is a bug with determining the surface side (shadow or colored)
+
+ topSideVisible = crossproduct.z > 0;
+ } else {
+ topSideVisible = true;
+ }
+
+ if (topSideVisible) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ s = 1; // saturation
+
+ if (this.showShadow) {
+ v = Math.min(1 + crossproduct.x / len / 2, 1); // value. TODO: scale
+ fillStyle = this._hsv2rgb(h, s, v);
+ strokeStyle = fillStyle;
+ } else {
+ v = 1;
+ fillStyle = this._hsv2rgb(h, s, v);
+ strokeStyle = this.axisColor; // TODO: should be customizable
+ }
+ } else {
+ fillStyle = 'gray';
+ strokeStyle = this.axisColor;
+ }
+
+ ctx.lineWidth = this._getStrokeWidth(point);
+ ctx.fillStyle = fillStyle;
+ ctx.strokeStyle = strokeStyle;
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(right.screen.x, right.screen.y);
+ ctx.lineTo(cross.screen.x, cross.screen.y);
+ ctx.lineTo(top.screen.x, top.screen.y);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke(); // TODO: only draw stroke when strokeWidth > 0
+ }
+ }
+ } else {
+ // grid style
+ for (i = 0; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ right = this.dataPoints[i].pointRight;
+ top = this.dataPoints[i].pointTop;
+
+ if (point !== undefined && right !== undefined) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + right.point.z) / 2;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+
+ ctx.lineWidth = this._getStrokeWidth(point) * 2;
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(right.screen.x, right.screen.y);
+ ctx.stroke();
+ }
+
+ if (point !== undefined && top !== undefined) {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ zAvg = (point.point.z + top.point.z) / 2;
+ h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+
+ ctx.lineWidth = this._getStrokeWidth(point) * 2;
+ ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+ ctx.lineTo(top.screen.x, top.screen.y);
+ ctx.stroke();
+ }
+ }
+ }
+ };
+
+ Graph3d.prototype._getStrokeWidth = function (point) {
+ if (point !== undefined) {
+ if (this.showPerspective) {
+ return 1 / -point.trans.z * this.dataColor.strokeWidth;
+ } else {
+ return -(this.eye.z / this.camera.getArmLength()) * this.dataColor.strokeWidth;
+ }
+ }
+
+ return this.dataColor.strokeWidth;
+ };
+
+ /**
+ * Draw all datapoints as dots.
+ * This function can be used when the style is 'dot' or 'dot-line'
+ */
+ Graph3d.prototype._redrawDataDot = function () {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ var i;
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+
+ // calculate the distance from the point at the bottom to the camera
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
+
+ // order the translated points by depth
+ var sortDepth = function sortDepth(a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
+
+ // draw the datapoints as colored circles
+ var dotSize = this.frame.clientWidth * 0.02; // px
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var point = this.dataPoints[i];
+
+ if (this.style === Graph3d.STYLE.DOTLINE) {
+ // draw a vertical line from the bottom to the graph value
+ //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
+ var from = this._convert3Dto2D(point.bottom);
+ ctx.lineWidth = 1;
+ ctx.strokeStyle = this.gridColor;
+ ctx.beginPath();
+ ctx.moveTo(from.x, from.y);
+ ctx.lineTo(point.screen.x, point.screen.y);
+ ctx.stroke();
+ }
+
+ // calculate radius for the circle
+ var size;
+ if (this.style === Graph3d.STYLE.DOTSIZE) {
+ size = dotSize / 2 + 2 * dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
+ } else {
+ size = dotSize;
+ }
+
+ var radius;
+ if (this.showPerspective) {
+ radius = size / -point.trans.z;
+ } else {
+ radius = size * -(this.eye.z / this.camera.getArmLength());
+ }
+ if (radius < 0) {
+ radius = 0;
+ }
+
+ var hue, color, borderColor;
+ if (this.style === Graph3d.STYLE.DOTCOLOR) {
+ // calculate the color based on the value
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ } else if (this.style === Graph3d.STYLE.DOTSIZE) {
+ color = this.dataColor.fill;
+ borderColor = this.dataColor.stroke;
+ } else {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+
+ // draw the circle
+ ctx.lineWidth = this._getStrokeWidth(point);
+ ctx.strokeStyle = borderColor;
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI * 2, true);
+ ctx.fill();
+ ctx.stroke();
+ }
+ };
+
+ /**
+ * Draw all datapoints as bars.
+ * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
+ */
+ Graph3d.prototype._redrawDataBar = function () {
+ var canvas = this.frame.canvas;
+ var ctx = canvas.getContext('2d');
+ var i, j, surface, corners;
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+
+ // calculate the distance from the point at the bottom to the camera
+ var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
+ this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
+ }
+
+ // order the translated points by depth
+ var sortDepth = function sortDepth(a, b) {
+ return b.dist - a.dist;
+ };
+ this.dataPoints.sort(sortDepth);
+
+ ctx.lineJoin = 'round';
+ ctx.lineCap = 'round';
+
+ // draw the datapoints as bars
+ var xWidth = this.xBarWidth / 2;
+ var yWidth = this.yBarWidth / 2;
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var point = this.dataPoints[i];
+
+ // determine color
+ var hue, color, borderColor;
+ if (this.style === Graph3d.STYLE.BARCOLOR) {
+ // calculate the color based on the value
+ hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ } else if (this.style === Graph3d.STYLE.BARSIZE) {
+ color = this.dataColor.fill;
+ borderColor = this.dataColor.stroke;
+ } else {
+ // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
+ hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
+ color = this._hsv2rgb(hue, 1, 1);
+ borderColor = this._hsv2rgb(hue, 1, 0.8);
+ }
+
+ // calculate size for the bar
+ if (this.style === Graph3d.STYLE.BARSIZE) {
+ xWidth = this.xBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+ yWidth = this.yBarWidth / 2 * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
+ }
+
+ // calculate all corner points
+ var me = this;
+ var point3d = point.point;
+ var top = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z) }];
+ var bottom = [{ point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin) }, { point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin) }, { point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin) }];
+
+ // calculate screen location of the points
+ top.forEach(function (obj) {
+ obj.screen = me._convert3Dto2D(obj.point);
+ });
+ bottom.forEach(function (obj) {
+ obj.screen = me._convert3Dto2D(obj.point);
+ });
+
+ // create five sides, calculate both corner points and center points
+ var surfaces = [{ corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point) }, { corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point) }, { corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point) }, { corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point) }, { corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point) }];
+ point.surfaces = surfaces;
+
+ // calculate the distance of each of the surface centers to the camera
+ for (j = 0; j < surfaces.length; j++) {
+ surface = surfaces[j];
+ var transCenter = this._convertPointToTranslation(surface.center);
+ surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
+ // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
+ // but the current solution is fast/simple and works in 99.9% of all cases
+ // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
+ }
+
+ // order the surfaces by their (translated) depth
+ surfaces.sort(function (a, b) {
+ var diff = b.dist - a.dist;
+ if (diff) return diff;
+
+ // if equal depth, sort the top surface last
+ if (a.corners === top) return 1;
+ if (b.corners === top) return -1;
+
+ // both are equal
+ return 0;
+ });
+
+ // draw the ordered surfaces
+ ctx.lineWidth = this._getStrokeWidth(point);
+ ctx.strokeStyle = borderColor;
+ ctx.fillStyle = color;
+ // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
+ for (j = 2; j < surfaces.length; j++) {
+ surface = surfaces[j];
+ corners = surface.corners;
+ ctx.beginPath();
+ ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
+ ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
+ ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
+ ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
+ ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
+ ctx.fill();
+ ctx.stroke();
+ }
+ }
+ };
+
+ /**
+ * Draw a line through all datapoints.
+ * This function can be used when the style is 'line'
+ */
+ Graph3d.prototype._redrawDataLine = function () {
+ var canvas = this.frame.canvas,
+ ctx = canvas.getContext('2d'),
+ point,
+ i;
+
+ if (this.dataPoints === undefined || this.dataPoints.length <= 0) return; // TODO: throw exception?
+
+ // calculate the translations of all points
+ for (i = 0; i < this.dataPoints.length; i++) {
+ var trans = this._convertPointToTranslation(this.dataPoints[i].point);
+ var screen = this._convertTranslationToScreen(trans);
+
+ this.dataPoints[i].trans = trans;
+ this.dataPoints[i].screen = screen;
+ }
+
+ // start the line
+ if (this.dataPoints.length > 0) {
+ point = this.dataPoints[0];
+
+ ctx.lineWidth = this._getStrokeWidth(point);
+ ctx.lineJoin = 'round';
+ ctx.lineCap = 'round';
+ ctx.strokeStyle = this.dataColor.stroke;
+ ctx.beginPath();
+ ctx.moveTo(point.screen.x, point.screen.y);
+
+ // draw the datapoints as colored circles
+ for (i = 1; i < this.dataPoints.length; i++) {
+ point = this.dataPoints[i];
+ ctx.lineTo(point.screen.x, point.screen.y);
+ }
+
+ // finish the line
+ ctx.stroke();
+ }
+ };
+
+ /**
+ * Start a moving operation inside the provided parent element
+ * @param {Event} event The event that occurred (required for
+ * retrieving the mouse position)
+ */
+ Graph3d.prototype._onMouseDown = function (event) {
+ event = event || window.event;
+
+ // check if mouse is still down (may be up when focus is lost for example
+ // in an iframe)
+ if (this.leftButtonDown) {
+ this._onMouseUp(event);
+ }
+
+ // only react on left mouse button down
+ this.leftButtonDown = event.which ? event.which === 1 : event.button === 1;
+ if (!this.leftButtonDown && !this.touchDown) return;
+
+ // get mouse position (different code for IE and all other browsers)
+ this.startMouseX = getMouseX(event);
+ this.startMouseY = getMouseY(event);
+
+ this.startStart = new Date(this.start);
+ this.startEnd = new Date(this.end);
+ this.startArmRotation = this.camera.getArmRotation();
+
+ this.frame.style.cursor = 'move';
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the graph, so we can
+ // remove the eventlisteners lateron in the function mouseUp()
+ var me = this;
+ this.onmousemove = function (event) {
+ me._onMouseMove(event);
+ };
+ this.onmouseup = function (event) {
+ me._onMouseUp(event);
+ };
+ util.addEventListener(document, 'mousemove', me.onmousemove);
+ util.addEventListener(document, 'mouseup', me.onmouseup);
+ util.preventDefault(event);
+ };
+
+ /**
+ * Perform moving operating.
+ * This function activated from within the funcion Graph.mouseDown().
+ * @param {Event} event Well, eehh, the event
+ */
+ Graph3d.prototype._onMouseMove = function (event) {
+ event = event || window.event;
+
+ // calculate change in mouse position
+ var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
+ var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
+
+ var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
+ var verticalNew = this.startArmRotation.vertical + diffY / 200;
+
+ var snapAngle = 4; // degrees
+ var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
+
+ // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
+ // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
+ if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
+ horizontalNew = Math.round(horizontalNew / Math.PI) * Math.PI - 0.001;
+ }
+ if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
+ horizontalNew = (Math.round(horizontalNew / Math.PI - 0.5) + 0.5) * Math.PI - 0.001;
+ }
+
+ // snap vertically to nice angles
+ if (Math.abs(Math.sin(verticalNew)) < snapValue) {
+ verticalNew = Math.round(verticalNew / Math.PI) * Math.PI;
+ }
+ if (Math.abs(Math.cos(verticalNew)) < snapValue) {
+ verticalNew = (Math.round(verticalNew / Math.PI - 0.5) + 0.5) * Math.PI;
+ }
+
+ this.camera.setArmRotation(horizontalNew, verticalNew);
+ this.redraw();
+
+ // fire a cameraPositionChange event
+ var parameters = this.getCameraPosition();
+ this.emit('cameraPositionChange', parameters);
+
+ util.preventDefault(event);
+ };
+
+ /**
+ * Stop moving operating.
+ * This function activated from within the funcion Graph.mouseDown().
+ * @param {event} event The event
+ */
+ Graph3d.prototype._onMouseUp = function (event) {
+ this.frame.style.cursor = 'auto';
+ this.leftButtonDown = false;
+
+ // remove event listeners here
+ util.removeEventListener(document, 'mousemove', this.onmousemove);
+ util.removeEventListener(document, 'mouseup', this.onmouseup);
+ util.preventDefault(event);
+ };
+
+ /**
+ * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
+ * @param {Event} event A mouse move event
+ */
+ Graph3d.prototype._onTooltip = function (event) {
+ var delay = 300; // ms
+ var boundingRect = this.frame.getBoundingClientRect();
+ var mouseX = getMouseX(event) - boundingRect.left;
+ var mouseY = getMouseY(event) - boundingRect.top;
+
+ if (!this.showTooltip) {
+ return;
+ }
+
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ }
+
+ // (delayed) display of a tooltip only if no mouse button is down
+ if (this.leftButtonDown) {
+ this._hideTooltip();
+ return;
+ }
+
+ if (this.tooltip && this.tooltip.dataPoint) {
+ // tooltip is currently visible
+ var dataPoint = this._dataPointFromXY(mouseX, mouseY);
+ if (dataPoint !== this.tooltip.dataPoint) {
+ // datapoint changed
+ if (dataPoint) {
+ this._showTooltip(dataPoint);
+ } else {
+ this._hideTooltip();
+ }
+ }
+ } else {
+ // tooltip is currently not visible
+ var me = this;
+ this.tooltipTimeout = setTimeout(function () {
+ me.tooltipTimeout = null;
+
+ // show a tooltip if we have a data point
+ var dataPoint = me._dataPointFromXY(mouseX, mouseY);
+ if (dataPoint) {
+ me._showTooltip(dataPoint);
+ }
+ }, delay);
+ }
+ };
+
+ /**
+ * Event handler for touchstart event on mobile devices
+ */
+ Graph3d.prototype._onTouchStart = function (event) {
+ this.touchDown = true;
+
+ var me = this;
+ this.ontouchmove = function (event) {
+ me._onTouchMove(event);
+ };
+ this.ontouchend = function (event) {
+ me._onTouchEnd(event);
+ };
+ util.addEventListener(document, 'touchmove', me.ontouchmove);
+ util.addEventListener(document, 'touchend', me.ontouchend);
+
+ this._onMouseDown(event);
+ };
+
+ /**
+ * Event handler for touchmove event on mobile devices
+ */
+ Graph3d.prototype._onTouchMove = function (event) {
+ this._onMouseMove(event);
+ };
+
+ /**
+ * Event handler for touchend event on mobile devices
+ */
+ Graph3d.prototype._onTouchEnd = function (event) {
+ this.touchDown = false;
+
+ util.removeEventListener(document, 'touchmove', this.ontouchmove);
+ util.removeEventListener(document, 'touchend', this.ontouchend);
+
+ this._onMouseUp(event);
+ };
+
+ /**
+ * Event handler for mouse wheel event, used to zoom the graph
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {event} event The event
+ */
+ Graph3d.prototype._onWheel = function (event) {
+ if (!event) /* For IE. */
+ event = window.event;
+
+ // retrieve delta
+ var delta = 0;
+ if (event.wheelDelta) {
+ /* IE/Opera. */
+ delta = event.wheelDelta / 120;
+ } else if (event.detail) {
+ /* Mozilla case. */
+ // In Mozilla, sign of delta is different than in IE.
+ // Also, delta is multiple of 3.
+ delta = -event.detail / 3;
+ }
+
+ // If delta is nonzero, handle it.
+ // Basically, delta is now positive if wheel was scrolled up,
+ // and negative, if wheel was scrolled down.
+ if (delta) {
+ var oldLength = this.camera.getArmLength();
+ var newLength = oldLength * (1 - delta / 10);
+
+ this.camera.setArmLength(newLength);
+ this.redraw();
+
+ this._hideTooltip();
+ }
+
+ // fire a cameraPositionChange event
+ var parameters = this.getCameraPosition();
+ this.emit('cameraPositionChange', parameters);
+
+ // Prevent default actions caused by mouse wheel.
+ // That might be ugly, but we handle scrolls somehow
+ // anyway, so don't bother here..
+ util.preventDefault(event);
+ };
+
+ /**
+ * Test whether a point lies inside given 2D triangle
+ * @param {Point2d} point
+ * @param {Point2d[]} triangle
+ * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
+ * @private
+ */
+ Graph3d.prototype._insideTriangle = function (point, triangle) {
+ var a = triangle[0],
+ b = triangle[1],
+ c = triangle[2];
+
+ function sign(x) {
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
+ }
+
+ var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
+ var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
+ var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
+
+ // each of the three signs must be either equal to each other or zero
+ return (as == 0 || bs == 0 || as == bs) && (bs == 0 || cs == 0 || bs == cs) && (as == 0 || cs == 0 || as == cs);
+ };
+
+ /**
+ * Find a data point close to given screen position (x, y)
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Object | null} The closest data point or null if not close to any data point
+ * @private
+ */
+ Graph3d.prototype._dataPointFromXY = function (x, y) {
+ var i,
+ distMax = 100,
+ // px
+ dataPoint = null,
+ closestDataPoint = null,
+ closestDist = null,
+ center = new Point2d(x, y);
+
+ if (this.style === Graph3d.STYLE.BAR || this.style === Graph3d.STYLE.BARCOLOR || this.style === Graph3d.STYLE.BARSIZE) {
+ // the data points are ordered from far away to closest
+ for (i = this.dataPoints.length - 1; i >= 0; i--) {
+ dataPoint = this.dataPoints[i];
+ var surfaces = dataPoint.surfaces;
+ if (surfaces) {
+ for (var s = surfaces.length - 1; s >= 0; s--) {
+ // split each surface in two triangles, and see if the center point is inside one of these
+ var surface = surfaces[s];
+ var corners = surface.corners;
+ var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
+ var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
+ if (this._insideTriangle(center, triangle1) || this._insideTriangle(center, triangle2)) {
+ // return immediately at the first hit
+ return dataPoint;
+ }
+ }
+ }
+ }
+ } else {
+ // find the closest data point, using distance to the center of the point on 2d screen
+ for (i = 0; i < this.dataPoints.length; i++) {
+ dataPoint = this.dataPoints[i];
+ var point = dataPoint.screen;
+ if (point) {
+ var distX = Math.abs(x - point.x);
+ var distY = Math.abs(y - point.y);
+ var dist = Math.sqrt(distX * distX + distY * distY);
+
+ if ((closestDist === null || dist < closestDist) && dist < distMax) {
+ closestDist = dist;
+ closestDataPoint = dataPoint;
+ }
+ }
+ }
+ }
+
+ return closestDataPoint;
+ };
+
+ /**
+ * Display a tooltip for given data point
+ * @param {Object} dataPoint
+ * @private
+ */
+ Graph3d.prototype._showTooltip = function (dataPoint) {
+ var content, line, dot;
+
+ if (!this.tooltip) {
+ content = document.createElement('div');
+ content.style.position = 'absolute';
+ content.style.padding = '10px';
+ content.style.border = '1px solid #4d4d4d';
+ content.style.color = '#1a1a1a';
+ content.style.background = 'rgba(255,255,255,0.7)';
+ content.style.borderRadius = '2px';
+ content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
+
+ line = document.createElement('div');
+ line.style.position = 'absolute';
+ line.style.height = '40px';
+ line.style.width = '0';
+ line.style.borderLeft = '1px solid #4d4d4d';
+
+ dot = document.createElement('div');
+ dot.style.position = 'absolute';
+ dot.style.height = '0';
+ dot.style.width = '0';
+ dot.style.border = '5px solid #4d4d4d';
+ dot.style.borderRadius = '5px';
+
+ this.tooltip = {
+ dataPoint: null,
+ dom: {
+ content: content,
+ line: line,
+ dot: dot
+ }
+ };
+ } else {
+ content = this.tooltip.dom.content;
+ line = this.tooltip.dom.line;
+ dot = this.tooltip.dom.dot;
+ }
+
+ this._hideTooltip();
+
+ this.tooltip.dataPoint = dataPoint;
+ if (typeof this.showTooltip === 'function') {
+ content.innerHTML = this.showTooltip(dataPoint.point);
+ } else {
+ content.innerHTML = '<table>' + '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' + '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' + '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' + '</table>';
+ }
+
+ content.style.left = '0';
+ content.style.top = '0';
+ this.frame.appendChild(content);
+ this.frame.appendChild(line);
+ this.frame.appendChild(dot);
+
+ // calculate sizes
+ var contentWidth = content.offsetWidth;
+ var contentHeight = content.offsetHeight;
+ var lineHeight = line.offsetHeight;
+ var dotWidth = dot.offsetWidth;
+ var dotHeight = dot.offsetHeight;
+
+ var left = dataPoint.screen.x - contentWidth / 2;
+ left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
+
+ line.style.left = dataPoint.screen.x + 'px';
+ line.style.top = dataPoint.screen.y - lineHeight + 'px';
+ content.style.left = left + 'px';
+ content.style.top = dataPoint.screen.y - lineHeight - contentHeight + 'px';
+ dot.style.left = dataPoint.screen.x - dotWidth / 2 + 'px';
+ dot.style.top = dataPoint.screen.y - dotHeight / 2 + 'px';
+ };
+
+ /**
+ * Hide the tooltip when displayed
+ * @private
+ */
+ Graph3d.prototype._hideTooltip = function () {
+ if (this.tooltip) {
+ this.tooltip.dataPoint = null;
+
+ for (var prop in this.tooltip.dom) {
+ if (this.tooltip.dom.hasOwnProperty(prop)) {
+ var elem = this.tooltip.dom[prop];
+ if (elem && elem.parentNode) {
+ elem.parentNode.removeChild(elem);
+ }
+ }
+ }
+ }
+ };
+
+ /**--------------------------------------------------------------------------**/
+
+ /**
+ * Get the horizontal mouse position from a mouse event
+ * @param {Event} event
+ * @return {Number} mouse x
+ */
+ function getMouseX(event) {
+ if ('clientX' in event) return event.clientX;
+ return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
+ }
+
+ /**
+ * Get the vertical mouse position from a mouse event
+ * @param {Event} event
+ * @return {Number} mouse y
+ */
+ function getMouseY(event) {
+ if ('clientY' in event) return event.clientY;
+ return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
+ }
+
+ module.exports = Graph3d;
+
+ // use use defaults
+
+/***/ },
+/* 12 */
+/***/ function(module, exports) {
+
+
+ /**
+ * Expose `Emitter`.
+ */
+
+ module.exports = Emitter;
+
+ /**
+ * Initialize a new `Emitter`.
+ *
+ * @api public
+ */
+
+ function Emitter(obj) {
+ if (obj) return mixin(obj);
+ };
+
+ /**
+ * Mixin the emitter properties.
+ *
+ * @param {Object} obj
+ * @return {Object}
+ * @api private
+ */
+
+ function mixin(obj) {
+ for (var key in Emitter.prototype) {
+ obj[key] = Emitter.prototype[key];
+ }
+ return obj;
+ }
+
+ /**
+ * Listen on the given `event` with `fn`.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+ Emitter.prototype.on =
+ Emitter.prototype.addEventListener = function(event, fn){
+ this._callbacks = this._callbacks || {};
+ (this._callbacks[event] = this._callbacks[event] || [])
+ .push(fn);
+ return this;
+ };
+
+ /**
+ * Adds an `event` listener that will be invoked a single
+ * time then automatically removed.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+ Emitter.prototype.once = function(event, fn){
+ var self = this;
+ this._callbacks = this._callbacks || {};
+
+ function on() {
+ self.off(event, on);
+ fn.apply(this, arguments);
+ }
+
+ on.fn = fn;
+ this.on(event, on);
+ return this;
+ };
+
+ /**
+ * Remove the given callback for `event` or all
+ * registered callbacks.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+ Emitter.prototype.off =
+ Emitter.prototype.removeListener =
+ Emitter.prototype.removeAllListeners =
+ Emitter.prototype.removeEventListener = function(event, fn){
+ this._callbacks = this._callbacks || {};
+
+ // all
+ if (0 == arguments.length) {
+ this._callbacks = {};
+ return this;
+ }
+
+ // specific event
+ var callbacks = this._callbacks[event];
+ if (!callbacks) return this;
+
+ // remove all handlers
+ if (1 == arguments.length) {
+ delete this._callbacks[event];
+ return this;
+ }
+
+ // remove specific handler
+ var cb;
+ for (var i = 0; i < callbacks.length; i++) {
+ cb = callbacks[i];
+ if (cb === fn || cb.fn === fn) {
+ callbacks.splice(i, 1);
+ break;
+ }
+ }
+ return this;
+ };
+
+ /**
+ * Emit `event` with the given args.
+ *
+ * @param {String} event
+ * @param {Mixed} ...
+ * @return {Emitter}
+ */
+
+ Emitter.prototype.emit = function(event){
+ this._callbacks = this._callbacks || {};
+ var args = [].slice.call(arguments, 1)
+ , callbacks = this._callbacks[event];
+
+ if (callbacks) {
+ callbacks = callbacks.slice(0);
+ for (var i = 0, len = callbacks.length; i < len; ++i) {
+ callbacks[i].apply(this, args);
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Return array of callbacks for `event`.
+ *
+ * @param {String} event
+ * @return {Array}
+ * @api public
+ */
+
+ Emitter.prototype.listeners = function(event){
+ this._callbacks = this._callbacks || {};
+ return this._callbacks[event] || [];
+ };
+
+ /**
+ * Check if this emitter has `event` handlers.
+ *
+ * @param {String} event
+ * @return {Boolean}
+ * @api public
+ */
+
+ Emitter.prototype.hasListeners = function(event){
+ return !! this.listeners(event).length;
+ };
+
+
+/***/ },
+/* 13 */
+/***/ function(module, exports) {
+
+ /**
+ * @prototype Point3d
+ * @param {Number} [x]
+ * @param {Number} [y]
+ * @param {Number} [z]
+ */
+ "use strict";
+
+ function Point3d(x, y, z) {
+ this.x = x !== undefined ? x : 0;
+ this.y = y !== undefined ? y : 0;
+ this.z = z !== undefined ? z : 0;
+ };
+
+ /**
+ * Subtract the two provided points, returns a-b
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} a-b
+ */
+ Point3d.subtract = function (a, b) {
+ var sub = new Point3d();
+ sub.x = a.x - b.x;
+ sub.y = a.y - b.y;
+ sub.z = a.z - b.z;
+ return sub;
+ };
+
+ /**
+ * Add the two provided points, returns a+b
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} a+b
+ */
+ Point3d.add = function (a, b) {
+ var sum = new Point3d();
+ sum.x = a.x + b.x;
+ sum.y = a.y + b.y;
+ sum.z = a.z + b.z;
+ return sum;
+ };
+
+ /**
+ * Calculate the average of two 3d points
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} The average, (a+b)/2
+ */
+ Point3d.avg = function (a, b) {
+ return new Point3d((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2);
+ };
+
+ /**
+ * Calculate the cross product of the two provided points, returns axb
+ * Documentation: http://en.wikipedia.org/wiki/Cross_product
+ * @param {Point3d} a
+ * @param {Point3d} b
+ * @return {Point3d} cross product axb
+ */
+ Point3d.crossProduct = function (a, b) {
+ var crossproduct = new Point3d();
+
+ crossproduct.x = a.y * b.z - a.z * b.y;
+ crossproduct.y = a.z * b.x - a.x * b.z;
+ crossproduct.z = a.x * b.y - a.y * b.x;
+
+ return crossproduct;
+ };
+
+ /**
+ * Rtrieve the length of the vector (or the distance from this point to the origin
+ * @return {Number} length
+ */
+ Point3d.prototype.length = function () {
+ return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
+ };
+
+ module.exports = Point3d;
+
+/***/ },
+/* 14 */
+/***/ function(module, exports) {
+
+ /**
+ * @prototype Point2d
+ * @param {Number} [x]
+ * @param {Number} [y]
+ */
+ "use strict";
+
+ function Point2d(x, y) {
+ this.x = x !== undefined ? x : 0;
+ this.y = y !== undefined ? y : 0;
+ }
+
+ module.exports = Point2d;
+
+/***/ },
+/* 15 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var Point3d = __webpack_require__(13);
+
+ /**
+ * @class Camera
+ * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
+ * The camera is always looking in the direction of the origin of the arm.
+ * This way, the camera always rotates around one fixed point, the location
+ * of the camera arm.
+ *
+ * Documentation:
+ * http://en.wikipedia.org/wiki/3D_projection
+ */
+ function Camera() {
+ this.armLocation = new Point3d();
+ this.armRotation = {};
+ this.armRotation.horizontal = 0;
+ this.armRotation.vertical = 0;
+ this.armLength = 1.7;
+
+ this.cameraLocation = new Point3d();
+ this.cameraRotation = new Point3d(0.5 * Math.PI, 0, 0);
+
+ this.calculateCameraOrientation();
+ }
+
+ /**
+ * Set the location (origin) of the arm
+ * @param {Number} x Normalized value of x
+ * @param {Number} y Normalized value of y
+ * @param {Number} z Normalized value of z
+ */
+ Camera.prototype.setArmLocation = function (x, y, z) {
+ this.armLocation.x = x;
+ this.armLocation.y = y;
+ this.armLocation.z = z;
+
+ this.calculateCameraOrientation();
+ };
+
+ /**
+ * Set the rotation of the camera arm
+ * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
+ * Optional, can be left undefined.
+ * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
+ * if vertical=0.5*PI, the graph is shown from the
+ * top. Optional, can be left undefined.
+ */
+ Camera.prototype.setArmRotation = function (horizontal, vertical) {
+ if (horizontal !== undefined) {
+ this.armRotation.horizontal = horizontal;
+ }
+
+ if (vertical !== undefined) {
+ this.armRotation.vertical = vertical;
+ if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
+ if (this.armRotation.vertical > 0.5 * Math.PI) this.armRotation.vertical = 0.5 * Math.PI;
+ }
+
+ if (horizontal !== undefined || vertical !== undefined) {
+ this.calculateCameraOrientation();
+ }
+ };
+
+ /**
+ * Retrieve the current arm rotation
+ * @return {object} An object with parameters horizontal and vertical
+ */
+ Camera.prototype.getArmRotation = function () {
+ var rot = {};
+ rot.horizontal = this.armRotation.horizontal;
+ rot.vertical = this.armRotation.vertical;
+
+ return rot;
+ };
+
+ /**
+ * Set the (normalized) length of the camera arm.
+ * @param {Number} length A length between 0.71 and 5.0
+ */
+ Camera.prototype.setArmLength = function (length) {
+ if (length === undefined) return;
+
+ this.armLength = length;
+
+ // Radius must be larger than the corner of the graph,
+ // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
+ // graph
+ if (this.armLength < 0.71) this.armLength = 0.71;
+ if (this.armLength > 5.0) this.armLength = 5.0;
+
+ this.calculateCameraOrientation();
+ };
+
+ /**
+ * Retrieve the arm length
+ * @return {Number} length
+ */
+ Camera.prototype.getArmLength = function () {
+ return this.armLength;
+ };
+
+ /**
+ * Retrieve the camera location
+ * @return {Point3d} cameraLocation
+ */
+ Camera.prototype.getCameraLocation = function () {
+ return this.cameraLocation;
+ };
+
+ /**
+ * Retrieve the camera rotation
+ * @return {Point3d} cameraRotation
+ */
+ Camera.prototype.getCameraRotation = function () {
+ return this.cameraRotation;
+ };
+
+ /**
+ * Calculate the location and rotation of the camera based on the
+ * position and orientation of the camera arm
+ */
+ Camera.prototype.calculateCameraOrientation = function () {
+ // calculate location of the camera
+ this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+ this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
+ this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
+
+ // calculate rotation of the camera
+ this.cameraRotation.x = Math.PI / 2 - this.armRotation.vertical;
+ this.cameraRotation.y = 0;
+ this.cameraRotation.z = -this.armRotation.horizontal;
+ };
+
+ module.exports = Camera;
+
+/***/ },
+/* 16 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var DataView = __webpack_require__(10);
+
+ /**
+ * @class Filter
+ *
+ * @param {DataSet} data The google data table
+ * @param {Number} column The index of the column to be filtered
+ * @param {Graph} graph The graph
+ */
+ function Filter(data, column, graph) {
+ this.data = data;
+ this.column = column;
+ this.graph = graph; // the parent graph
+
+ this.index = undefined;
+ this.value = undefined;
+
+ // read all distinct values and select the first one
+ this.values = graph.getDistinctValues(data.get(), this.column);
+
+ // sort both numeric and string values correctly
+ this.values.sort(function (a, b) {
+ return a > b ? 1 : a < b ? -1 : 0;
+ });
+
+ if (this.values.length > 0) {
+ this.selectValue(0);
+ }
+
+ // create an array with the filtered datapoints. this will be loaded afterwards
+ this.dataPoints = [];
+
+ this.loaded = false;
+ this.onLoadCallback = undefined;
+
+ if (graph.animationPreload) {
+ this.loaded = false;
+ this.loadInBackground();
+ } else {
+ this.loaded = true;
+ }
+ };
+
+ /**
+ * Return the label
+ * @return {string} label
+ */
+ Filter.prototype.isLoaded = function () {
+ return this.loaded;
+ };
+
+ /**
+ * Return the loaded progress
+ * @return {Number} percentage between 0 and 100
+ */
+ Filter.prototype.getLoadedProgress = function () {
+ var len = this.values.length;
+
+ var i = 0;
+ while (this.dataPoints[i]) {
+ i++;
+ }
+
+ return Math.round(i / len * 100);
+ };
+
+ /**
+ * Return the label
+ * @return {string} label
+ */
+ Filter.prototype.getLabel = function () {
+ return this.graph.filterLabel;
+ };
+
+ /**
+ * Return the columnIndex of the filter
+ * @return {Number} columnIndex
+ */
+ Filter.prototype.getColumn = function () {
+ return this.column;
+ };
+
+ /**
+ * Return the currently selected value. Returns undefined if there is no selection
+ * @return {*} value
+ */
+ Filter.prototype.getSelectedValue = function () {
+ if (this.index === undefined) return undefined;
+
+ return this.values[this.index];
+ };
+
+ /**
+ * Retrieve all values of the filter
+ * @return {Array} values
+ */
+ Filter.prototype.getValues = function () {
+ return this.values;
+ };
+
+ /**
+ * Retrieve one value of the filter
+ * @param {Number} index
+ * @return {*} value
+ */
+ Filter.prototype.getValue = function (index) {
+ if (index >= this.values.length) throw 'Error: index out of range';
+
+ return this.values[index];
+ };
+
+ /**
+ * Retrieve the (filtered) dataPoints for the currently selected filter index
+ * @param {Number} [index] (optional)
+ * @return {Array} dataPoints
+ */
+ Filter.prototype._getDataPoints = function (index) {
+ if (index === undefined) index = this.index;
+
+ if (index === undefined) return [];
+
+ var dataPoints;
+ if (this.dataPoints[index]) {
+ dataPoints = this.dataPoints[index];
+ } else {
+ var f = {};
+ f.column = this.column;
+ f.value = this.values[index];
+
+ var dataView = new DataView(this.data, { filter: function filter(item) {
+ return item[f.column] == f.value;
+ } }).get();
+ dataPoints = this.graph._getDataPoints(dataView);
+
+ this.dataPoints[index] = dataPoints;
+ }
+
+ return dataPoints;
+ };
+
+ /**
+ * Set a callback function when the filter is fully loaded.
+ */
+ Filter.prototype.setOnLoadCallback = function (callback) {
+ this.onLoadCallback = callback;
+ };
+
+ /**
+ * Add a value to the list with available values for this filter
+ * No double entries will be created.
+ * @param {Number} index
+ */
+ Filter.prototype.selectValue = function (index) {
+ if (index >= this.values.length) throw 'Error: index out of range';
+
+ this.index = index;
+ this.value = this.values[index];
+ };
+
+ /**
+ * Load all filtered rows in the background one by one
+ * Start this method without providing an index!
+ */
+ Filter.prototype.loadInBackground = function (index) {
+ if (index === undefined) index = 0;
+
+ var frame = this.graph.frame;
+
+ if (index < this.values.length) {
+ var dataPointsTemp = this._getDataPoints(index);
+ //this.graph.redrawInfo(); // TODO: not neat
+
+ // create a progress box
+ if (frame.progress === undefined) {
+ frame.progress = document.createElement('DIV');
+ frame.progress.style.position = 'absolute';
+ frame.progress.style.color = 'gray';
+ frame.appendChild(frame.progress);
+ }
+ var progress = this.getLoadedProgress();
+ frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
+ // TODO: this is no nice solution...
+ frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider
+ frame.progress.style.left = 10 + 'px';
+
+ var me = this;
+ setTimeout(function () {
+ me.loadInBackground(index + 1);
+ }, 10);
+ this.loaded = false;
+ } else {
+ this.loaded = true;
+
+ // remove the progress box
+ if (frame.progress !== undefined) {
+ frame.removeChild(frame.progress);
+ frame.progress = undefined;
+ }
+
+ if (this.onLoadCallback) this.onLoadCallback();
+ }
+ };
+
+ module.exports = Filter;
+
+/***/ },
+/* 17 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var util = __webpack_require__(1);
+
+ /**
+ * @constructor Slider
+ *
+ * An html slider control with start/stop/prev/next buttons
+ * @param {Element} container The element where the slider will be created
+ * @param {Object} options Available options:
+ * {boolean} visible If true (default) the
+ * slider is visible.
+ */
+ function Slider(container, options) {
+ if (container === undefined) {
+ throw 'Error: No container element defined';
+ }
+ this.container = container;
+ this.visible = options && options.visible != undefined ? options.visible : true;
+
+ if (this.visible) {
+ this.frame = document.createElement('DIV');
+ //this.frame.style.backgroundColor = '#E5E5E5';
+ this.frame.style.width = '100%';
+ this.frame.style.position = 'relative';
+ this.container.appendChild(this.frame);
+
+ this.frame.prev = document.createElement('INPUT');
+ this.frame.prev.type = 'BUTTON';
+ this.frame.prev.value = 'Prev';
+ this.frame.appendChild(this.frame.prev);
+
+ this.frame.play = document.createElement('INPUT');
+ this.frame.play.type = 'BUTTON';
+ this.frame.play.value = 'Play';
+ this.frame.appendChild(this.frame.play);
+
+ this.frame.next = document.createElement('INPUT');
+ this.frame.next.type = 'BUTTON';
+ this.frame.next.value = 'Next';
+ this.frame.appendChild(this.frame.next);
+
+ this.frame.bar = document.createElement('INPUT');
+ this.frame.bar.type = 'BUTTON';
+ this.frame.bar.style.position = 'absolute';
+ this.frame.bar.style.border = '1px solid red';
+ this.frame.bar.style.width = '100px';
+ this.frame.bar.style.height = '6px';
+ this.frame.bar.style.borderRadius = '2px';
+ this.frame.bar.style.MozBorderRadius = '2px';
+ this.frame.bar.style.border = '1px solid #7F7F7F';
+ this.frame.bar.style.backgroundColor = '#E5E5E5';
+ this.frame.appendChild(this.frame.bar);
+
+ this.frame.slide = document.createElement('INPUT');
+ this.frame.slide.type = 'BUTTON';
+ this.frame.slide.style.margin = '0px';
+ this.frame.slide.value = ' ';
+ this.frame.slide.style.position = 'relative';
+ this.frame.slide.style.left = '-100px';
+ this.frame.appendChild(this.frame.slide);
+
+ // create events
+ var me = this;
+ this.frame.slide.onmousedown = function (event) {
+ me._onMouseDown(event);
+ };
+ this.frame.prev.onclick = function (event) {
+ me.prev(event);
+ };
+ this.frame.play.onclick = function (event) {
+ me.togglePlay(event);
+ };
+ this.frame.next.onclick = function (event) {
+ me.next(event);
+ };
+ }
+
+ this.onChangeCallback = undefined;
+
+ this.values = [];
+ this.index = undefined;
+
+ this.playTimeout = undefined;
+ this.playInterval = 1000; // milliseconds
+ this.playLoop = true;
+ }
+
+ /**
+ * Select the previous index
+ */
+ Slider.prototype.prev = function () {
+ var index = this.getIndex();
+ if (index > 0) {
+ index--;
+ this.setIndex(index);
+ }
+ };
+
+ /**
+ * Select the next index
+ */
+ Slider.prototype.next = function () {
+ var index = this.getIndex();
+ if (index < this.values.length - 1) {
+ index++;
+ this.setIndex(index);
+ }
+ };
+
+ /**
+ * Select the next index
+ */
+ Slider.prototype.playNext = function () {
+ var start = new Date();
+
+ var index = this.getIndex();
+ if (index < this.values.length - 1) {
+ index++;
+ this.setIndex(index);
+ } else if (this.playLoop) {
+ // jump to the start
+ index = 0;
+ this.setIndex(index);
+ }
+
+ var end = new Date();
+ var diff = end - start;
+
+ // calculate how much time it to to set the index and to execute the callback
+ // function.
+ var interval = Math.max(this.playInterval - diff, 0);
+ // document.title = diff // TODO: cleanup
+
+ var me = this;
+ this.playTimeout = setTimeout(function () {
+ me.playNext();
+ }, interval);
+ };
+
+ /**
+ * Toggle start or stop playing
+ */
+ Slider.prototype.togglePlay = function () {
+ if (this.playTimeout === undefined) {
+ this.play();
+ } else {
+ this.stop();
+ }
+ };
+
+ /**
+ * Start playing
+ */
+ Slider.prototype.play = function () {
+ // Test whether already playing
+ if (this.playTimeout) return;
+
+ this.playNext();
+
+ if (this.frame) {
+ this.frame.play.value = 'Stop';
+ }
+ };
+
+ /**
+ * Stop playing
+ */
+ Slider.prototype.stop = function () {
+ clearInterval(this.playTimeout);
+ this.playTimeout = undefined;
+
+ if (this.frame) {
+ this.frame.play.value = 'Play';
+ }
+ };
+
+ /**
+ * Set a callback function which will be triggered when the value of the
+ * slider bar has changed.
+ */
+ Slider.prototype.setOnChangeCallback = function (callback) {
+ this.onChangeCallback = callback;
+ };
+
+ /**
+ * Set the interval for playing the list
+ * @param {Number} interval The interval in milliseconds
+ */
+ Slider.prototype.setPlayInterval = function (interval) {
+ this.playInterval = interval;
+ };
+
+ /**
+ * Retrieve the current play interval
+ * @return {Number} interval The interval in milliseconds
+ */
+ Slider.prototype.getPlayInterval = function (interval) {
+ return this.playInterval;
+ };
+
+ /**
+ * Set looping on or off
+ * @pararm {boolean} doLoop If true, the slider will jump to the start when
+ * the end is passed, and will jump to the end
+ * when the start is passed.
+ */
+ Slider.prototype.setPlayLoop = function (doLoop) {
+ this.playLoop = doLoop;
+ };
+
+ /**
+ * Execute the onchange callback function
+ */
+ Slider.prototype.onChange = function () {
+ if (this.onChangeCallback !== undefined) {
+ this.onChangeCallback();
+ }
+ };
+
+ /**
+ * redraw the slider on the correct place
+ */
+ Slider.prototype.redraw = function () {
+ if (this.frame) {
+ // resize the bar
+ this.frame.bar.style.top = this.frame.clientHeight / 2 - this.frame.bar.offsetHeight / 2 + 'px';
+ this.frame.bar.style.width = this.frame.clientWidth - this.frame.prev.clientWidth - this.frame.play.clientWidth - this.frame.next.clientWidth - 30 + 'px';
+
+ // position the slider button
+ var left = this.indexToLeft(this.index);
+ this.frame.slide.style.left = left + 'px';
+ }
+ };
+
+ /**
+ * Set the list with values for the slider
+ * @param {Array} values A javascript array with values (any type)
+ */
+ Slider.prototype.setValues = function (values) {
+ this.values = values;
+
+ if (this.values.length > 0) this.setIndex(0);else this.index = undefined;
+ };
+
+ /**
+ * Select a value by its index
+ * @param {Number} index
+ */
+ Slider.prototype.setIndex = function (index) {
+ if (index < this.values.length) {
+ this.index = index;
+
+ this.redraw();
+ this.onChange();
+ } else {
+ throw 'Error: index out of range';
+ }
+ };
+
+ /**
+ * retrieve the index of the currently selected vaue
+ * @return {Number} index
+ */
+ Slider.prototype.getIndex = function () {
+ return this.index;
+ };
+
+ /**
+ * retrieve the currently selected value
+ * @return {*} value
+ */
+ Slider.prototype.get = function () {
+ return this.values[this.index];
+ };
+
+ Slider.prototype._onMouseDown = function (event) {
+ // only react on left mouse button down
+ var leftButtonDown = event.which ? event.which === 1 : event.button === 1;
+ if (!leftButtonDown) return;
+
+ this.startClientX = event.clientX;
+ this.startSlideX = parseFloat(this.frame.slide.style.left);
+
+ this.frame.style.cursor = 'move';
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the graph, so we can
+ // remove the eventlisteners lateron in the function mouseUp()
+ var me = this;
+ this.onmousemove = function (event) {
+ me._onMouseMove(event);
+ };
+ this.onmouseup = function (event) {
+ me._onMouseUp(event);
+ };
+ util.addEventListener(document, 'mousemove', this.onmousemove);
+ util.addEventListener(document, 'mouseup', this.onmouseup);
+ util.preventDefault(event);
+ };
+
+ Slider.prototype.leftToIndex = function (left) {
+ var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10;
+ var x = left - 3;
+
+ var index = Math.round(x / width * (this.values.length - 1));
+ if (index < 0) index = 0;
+ if (index > this.values.length - 1) index = this.values.length - 1;
+
+ return index;
+ };
+
+ Slider.prototype.indexToLeft = function (index) {
+ var width = parseFloat(this.frame.bar.style.width) - this.frame.slide.clientWidth - 10;
+
+ var x = index / (this.values.length - 1) * width;
+ var left = x + 3;
+
+ return left;
+ };
+
+ Slider.prototype._onMouseMove = function (event) {
+ var diff = event.clientX - this.startClientX;
+ var x = this.startSlideX + diff;
+
+ var index = this.leftToIndex(x);
+
+ this.setIndex(index);
+
+ util.preventDefault();
+ };
+
+ Slider.prototype._onMouseUp = function (event) {
+ this.frame.style.cursor = 'auto';
+
+ // remove event listeners
+ util.removeEventListener(document, 'mousemove', this.onmousemove);
+ util.removeEventListener(document, 'mouseup', this.onmouseup);
+
+ util.preventDefault();
+ };
+
+ module.exports = Slider;
+
+/***/ },
+/* 18 */
+/***/ function(module, exports) {
+
+ /**
+ * @prototype StepNumber
+ * The class StepNumber is an iterator for Numbers. You provide a start and end
+ * value, and a best step size. StepNumber itself rounds to fixed values and
+ * a finds the step that best fits the provided step.
+ *
+ * If prettyStep is true, the step size is chosen as close as possible to the
+ * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
+ *
+ * Example usage:
+ * var step = new StepNumber(0, 10, 2.5, true);
+ * step.start();
+ * while (!step.end()) {
+ * alert(step.getCurrent());
+ * step.next();
+ * }
+ *
+ * Version: 1.0
+ *
+ * @param {Number} start The start value
+ * @param {Number} end The end value
+ * @param {Number} step Optional. Step size. Must be a positive value.
+ * @param {boolean} prettyStep Optional. If true, the step size is rounded
+ * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+ */
+ "use strict";
+
+ function StepNumber(start, end, step, prettyStep) {
+ // set default values
+ this._start = 0;
+ this._end = 0;
+ this._step = 1;
+ this.prettyStep = true;
+ this.precision = 5;
+
+ this._current = 0;
+ this.setRange(start, end, step, prettyStep);
+ };
+
+ /**
+ * Set a new range: start, end and step.
+ *
+ * @param {Number} start The start value
+ * @param {Number} end The end value
+ * @param {Number} step Optional. Step size. Must be a positive value.
+ * @param {boolean} prettyStep Optional. If true, the step size is rounded
+ * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+ */
+ StepNumber.prototype.setRange = function (start, end, step, prettyStep) {
+ this._start = start ? start : 0;
+ this._end = end ? end : 0;
+
+ this.setStep(step, prettyStep);
+ };
+
+ /**
+ * Set a new step size
+ * @param {Number} step New step size. Must be a positive value
+ * @param {boolean} prettyStep Optional. If true, the provided step is rounded
+ * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
+ */
+ StepNumber.prototype.setStep = function (step, prettyStep) {
+ if (step === undefined || step <= 0) return;
+
+ if (prettyStep !== undefined) this.prettyStep = prettyStep;
+
+ if (this.prettyStep === true) this._step = StepNumber.calculatePrettyStep(step);else this._step = step;
+ };
+
+ /**
+ * Calculate a nice step size, closest to the desired step size.
+ * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
+ * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
+ * @param {Number} step Desired step size
+ * @return {Number} Nice step size
+ */
+ StepNumber.calculatePrettyStep = function (step) {
+ var log10 = function log10(x) {
+ return Math.log(x) / Math.LN10;
+ };
+
+ // try three steps (multiple of 1, 2, or 5
+ var step1 = Math.pow(10, Math.round(log10(step))),
+ step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
+ step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
+
+ // choose the best step (closest to minimum step)
+ var prettyStep = step1;
+ if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
+ if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
+
+ // for safety
+ if (prettyStep <= 0) {
+ prettyStep = 1;
+ }
+
+ return prettyStep;
+ };
+
+ /**
+ * returns the current value of the step
+ * @return {Number} current value
+ */
+ StepNumber.prototype.getCurrent = function () {
+ return parseFloat(this._current.toPrecision(this.precision));
+ };
+
+ /**
+ * returns the current step size
+ * @return {Number} current step size
+ */
+ StepNumber.prototype.getStep = function () {
+ return this._step;
+ };
+
+ /**
+ * Set the current value to the largest value smaller than start, which
+ * is a multiple of the step size
+ */
+ StepNumber.prototype.start = function () {
+ this._current = this._start - this._start % this._step;
+ };
+
+ /**
+ * Do a step, add the step size to the current value
+ */
+ StepNumber.prototype.next = function () {
+ this._current += this._step;
+ };
+
+ /**
+ * Returns true whether the end is reached
+ * @return {boolean} True if the current value has passed the end value.
+ */
+ StepNumber.prototype.end = function () {
+ return this._current > this._end;
+ };
+
+ module.exports = StepNumber;
+
+/***/ },
+/* 19 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var Emitter = __webpack_require__(12);
+ var Hammer = __webpack_require__(20);
+ var moment = __webpack_require__(2);
+ var util = __webpack_require__(1);
+ var DataSet = __webpack_require__(8);
+ var DataView = __webpack_require__(10);
+ var Range = __webpack_require__(24);
+ var Core = __webpack_require__(28);
+ var TimeAxis = __webpack_require__(39);
+ var CurrentTime = __webpack_require__(44);
+ var CustomTime = __webpack_require__(42);
+ var ItemSet = __webpack_require__(29);
+
+ var Configurator = __webpack_require__(45);
+ var Validator = __webpack_require__(47)['default'];
+ var printStyle = __webpack_require__(47).printStyle;
+ var allOptions = __webpack_require__(48).allOptions;
+ var configureOptions = __webpack_require__(48).configureOptions;
+
+ /**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | vis.DataView | Array} [items]
+ * @param {vis.DataSet | vis.DataView | Array} [groups]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
+ * @extends Core
+ */
+ function Timeline(container, items, groups, options) {
+ if (!(this instanceof Timeline)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
+ // if the third element is options, the forth is groups (optionally);
+ if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
+ var forthArgument = options;
+ options = groups;
+ groups = forthArgument;
+ }
+
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
+
+ autoResize: true,
+ throttleRedraw: 0, // ms
+
+ orientation: {
+ axis: 'bottom', // axis orientation: 'bottom', 'top', or 'both'
+ item: 'bottom' // not relevant
+ },
+
+ moment: moment,
+
+ width: null,
+ height: null,
+ maxHeight: null,
+ minHeight: null
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
+
+ // Create the DOM, props, and emitter
+ this._create(container);
+
+ // all components listed here will be repainted automatically
+ this.components = [];
+
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
+ },
+ hiddenDates: [],
+ util: {
+ getScale: function getScale() {
+ return me.timeAxis.step.scale;
+ },
+ getStep: function getStep() {
+ return me.timeAxis.step.step;
+ },
+
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime: me._toGlobalTime.bind(me)
+ }
+ };
+
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
+
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.timeAxis2 = null; // used in case of orientation option 'both'
+ this.components.push(this.timeAxis);
+
+ // current time bar
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
+
+ // item set
+ this.itemSet = new ItemSet(this.body);
+ this.components.push(this.itemSet);
+
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ this.on('tap', function (event) {
+ me.emit('click', me.getEventProperties(event));
+ });
+ this.on('doubletap', function (event) {
+ me.emit('doubleClick', me.getEventProperties(event));
+ });
+ this.dom.root.oncontextmenu = function (event) {
+ me.emit('contextmenu', me.getEventProperties(event));
+ };
+
+ // apply options
+ if (options) {
+ this.setOptions(options);
+ }
+
+ // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
+ if (groups) {
+ this.setGroups(groups);
+ }
+
+ // create itemset
+ if (items) {
+ this.setItems(items);
+ } else {
+ this._redraw();
+ }
+ }
+
+ // Extend the functionality from Core
+ Timeline.prototype = new Core();
+
+ /**
+ * Load a configurator
+ * @return {Object}
+ * @private
+ */
+ Timeline.prototype._createConfigurator = function () {
+ return new Configurator(this, this.dom.container, configureOptions);
+ };
+
+ /**
+ * Force a redraw. The size of all items will be recalculated.
+ * Can be useful to manually redraw when option autoResize=false and the window
+ * has been resized, or when the items CSS has been changed.
+ *
+ * Note: this function will be overridden on construction with a trottled version
+ */
+ Timeline.prototype.redraw = function () {
+ this.itemSet && this.itemSet.markDirty({ refreshItems: true });
+ this._redraw();
+ };
+
+ Timeline.prototype.setOptions = function (options) {
+ // validate options
+ var errorFound = Validator.validate(options, allOptions);
+ if (errorFound === true) {
+ console.log('%cErrors have been found in the supplied options object.', printStyle);
+ }
+
+ Core.prototype.setOptions.call(this, options);
+
+ if ('type' in options) {
+ if (options.type !== this.options.type) {
+ this.options.type = options.type;
+
+ // force recreation of all items
+ var itemsData = this.itemsData;
+ if (itemsData) {
+ var selection = this.getSelection();
+ this.setItems(null); // remove all
+ this.setItems(itemsData); // add all
+ this.setSelection(selection); // restore selection
+ }
+ }
+ }
+ };
+
+ /**
+ * Set items
+ * @param {vis.DataSet | Array | null} items
+ */
+ Timeline.prototype.setItems = function (items) {
+ var initialLoad = this.itemsData == null;
+
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ } else if (items instanceof DataSet || items instanceof DataView) {
+ newDataSet = items;
+ } else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ }
+
+ // set items
+ this.itemsData = newDataSet;
+ this.itemSet && this.itemSet.setItems(newDataSet);
+
+ if (initialLoad) {
+ if (this.options.start != undefined || this.options.end != undefined) {
+ if (this.options.start == undefined || this.options.end == undefined) {
+ var range = this.getItemRange();
+ }
+
+ var start = this.options.start != undefined ? this.options.start : range.min;
+ var end = this.options.end != undefined ? this.options.end : range.max;
+
+ this.setWindow(start, end, { animation: false });
+ } else {
+ this.fit({ animation: false });
+ }
+ }
+ };
+
+ /**
+ * Set groups
+ * @param {vis.DataSet | Array} groups
+ */
+ Timeline.prototype.setGroups = function (groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ } else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
+ } else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
+ }
+
+ this.groupsData = newDataSet;
+ this.itemSet.setGroups(newDataSet);
+ };
+
+ /**
+ * Set both items and groups in one go
+ * @param {{items: Array | vis.DataSet, groups: Array | vis.DataSet}} data
+ */
+ Timeline.prototype.setData = function (data) {
+ if (data && data.groups) {
+ this.setGroups(data.groups);
+ }
+
+ if (data && data.items) {
+ this.setItems(data.items);
+ }
+ };
+
+ /**
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {string[] | string} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
+ * @param {Object} [options] Available options:
+ * `focus: boolean`
+ * If true, focus will be set to the selected item(s)
+ * `animation: boolean | {duration: number, easingFunction: string}`
+ * If true (default), the range is animated
+ * smoothly to the new window. An object can be
+ * provided to specify duration and easing function.
+ * Default duration is 500 ms, and default easing
+ * function is 'easeInOutQuad'.
+ * Only applicable when option focus is true.
+ */
+ Timeline.prototype.setSelection = function (ids, options) {
+ this.itemSet && this.itemSet.setSelection(ids);
+
+ if (options && options.focus) {
+ this.focus(ids, options);
+ }
+ };
+
+ /**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+ Timeline.prototype.getSelection = function () {
+ return this.itemSet && this.itemSet.getSelection() || [];
+ };
+
+ /**
+ * Adjust the visible window such that the selected item (or multiple items)
+ * are centered on screen.
+ * @param {String | String[]} id An item id or array with item ids
+ * @param {Object} [options] Available options:
+ * `animation: boolean | {duration: number, easingFunction: string}`
+ * If true (default), the range is animated
+ * smoothly to the new window. An object can be
+ * provided to specify duration and easing function.
+ * Default duration is 500 ms, and default easing
+ * function is 'easeInOutQuad'.
+ */
+ Timeline.prototype.focus = function (id, options) {
+ if (!this.itemsData || id == undefined) return;
+
+ var ids = Array.isArray(id) ? id : [id];
+
+ // get the specified item(s)
+ var itemsData = this.itemsData.getDataSet().get(ids, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+
+ // calculate minimum start and maximum end of specified items
+ var start = null;
+ var end = null;
+ itemsData.forEach(function (itemData) {
+ var s = itemData.start.valueOf();
+ var e = 'end' in itemData ? itemData.end.valueOf() : itemData.start.valueOf();
+
+ if (start === null || s < start) {
+ start = s;
+ }
+
+ if (end === null || e > end) {
+ end = e;
+ }
+ });
+
+ if (start !== null && end !== null) {
+ // calculate the new middle and interval for the window
+ var middle = (start + end) / 2;
+ var interval = Math.max(this.range.end - this.range.start, (end - start) * 1.1);
+
+ var animation = options && options.animation !== undefined ? options.animation : true;
+ this.range.setRange(middle - interval / 2, middle + interval / 2, animation);
+ }
+ };
+
+ /**
+ * Set Timeline window such that it fits all items
+ * @param {Object} [options] Available options:
+ * `animation: boolean | {duration: number, easingFunction: string}`
+ * If true (default), the range is animated
+ * smoothly to the new window. An object can be
+ * provided to specify duration and easing function.
+ * Default duration is 500 ms, and default easing
+ * function is 'easeInOutQuad'.
+ */
+ Timeline.prototype.fit = function (options) {
+ var animation = options && options.animation !== undefined ? options.animation : true;
+ var range = this.getItemRange();
+ this.range.setRange(range.min, range.max, animation);
+ };
+
+ /**
+ * Determine the range of the items, taking into account their actual width
+ * and a margin of 10 pixels on both sides.
+ * @return {{min: Date | null, max: Date | null}}
+ */
+ Timeline.prototype.getItemRange = function () {
+ var _this = this;
+
+ // get a rough approximation for the range based on the items start and end dates
+ var range = this.getDataRange();
+ var min = range.min;
+ var max = range.max;
+ var minItem = null;
+ var maxItem = null;
+
+ if (min != null && max != null) {
+ var interval;
+ var factor;
+ var lhs;
+ var rhs;
+ var delta;
+
+ (function () {
+ var getStart = function getStart(item) {
+ return util.convert(item.data.start, 'Date').valueOf();
+ };
+
+ var getEnd = function getEnd(item) {
+ var end = item.data.end != undefined ? item.data.end : item.data.start;
+ return util.convert(end, 'Date').valueOf();
+ };
+
+ interval = max - min;
+ // ms
+ if (interval <= 0) {
+ interval = 10;
+ }
+ factor = interval / _this.props.center.width;
+
+ // calculate the date of the left side and right side of the items given
+ util.forEach(_this.itemSet.items, (function (item) {
+ item.show();
+
+ var start = getStart(item);
+ var end = getEnd(item);
+
+ var left = new Date(start - (item.getWidthLeft() + 10) * factor);
+ var right = new Date(end + (item.getWidthRight() + 10) * factor);
+
+ if (left < min) {
+ min = left;
+ minItem = item;
+ }
+ if (right > max) {
+ max = right;
+ maxItem = item;
+ }
+ }).bind(_this));
+
+ if (minItem && maxItem) {
+ lhs = minItem.getWidthLeft() + 10;
+ rhs = maxItem.getWidthRight() + 10;
+ delta = _this.props.center.width - lhs - rhs;
+ // px
+
+ if (delta > 0) {
+ min = getStart(minItem) - lhs * interval / delta; // ms
+ max = getEnd(maxItem) + rhs * interval / delta; // ms
+ }
+ }
+ })();
+ }
+
+ return {
+ min: min != null ? new Date(min) : null,
+ max: max != null ? new Date(max) : null
+ };
+ };
+
+ /**
+ * Calculate the data range of the items start and end dates
+ * @returns {{min: Date | null, max: Date | null}}
+ */
+ Timeline.prototype.getDataRange = function () {
+ var min = null;
+ var max = null;
+
+ var dataset = this.itemsData && this.itemsData.getDataSet();
+ if (dataset) {
+ dataset.forEach(function (item) {
+ var start = util.convert(item.start, 'Date').valueOf();
+ var end = util.convert(item.end != undefined ? item.end : item.start, 'Date').valueOf();
+ if (min === null || start < min) {
+ min = start;
+ }
+ if (max === null || end > max) {
+ max = start;
+ }
+ });
+ }
+
+ return {
+ min: min != null ? new Date(min) : null,
+ max: max != null ? new Date(max) : null
+ };
+ };
+
+ /**
+ * Generate Timeline related information from an event
+ * @param {Event} event
+ * @return {Object} An object with related information, like on which area
+ * The event happened, whether clicked on an item, etc.
+ */
+ Timeline.prototype.getEventProperties = function (event) {
+ var clientX = event.center ? event.center.x : event.clientX;
+ var clientY = event.center ? event.center.y : event.clientY;
+ var x = clientX - util.getAbsoluteLeft(this.dom.centerContainer);
+ var y = clientY - util.getAbsoluteTop(this.dom.centerContainer);
+
+ var item = this.itemSet.itemFromTarget(event);
+ var group = this.itemSet.groupFromTarget(event);
+ var customTime = CustomTime.customTimeFromTarget(event);
+
+ var snap = this.itemSet.options.snap || null;
+ var scale = this.body.util.getScale();
+ var step = this.body.util.getStep();
+ var time = this._toTime(x);
+ var snappedTime = snap ? snap(time, scale, step) : time;
+
+ var element = util.getTarget(event);
+ var what = null;
+ if (item != null) {
+ what = 'item';
+ } else if (customTime != null) {
+ what = 'custom-time';
+ } else if (util.hasParent(element, this.timeAxis.dom.foreground)) {
+ what = 'axis';
+ } else if (this.timeAxis2 && util.hasParent(element, this.timeAxis2.dom.foreground)) {
+ what = 'axis';
+ } else if (util.hasParent(element, this.itemSet.dom.labelSet)) {
+ what = 'group-label';
+ } else if (util.hasParent(element, this.currentTime.bar)) {
+ what = 'current-time';
+ } else if (util.hasParent(element, this.dom.center)) {
+ what = 'background';
+ }
+
+ return {
+ event: event,
+ item: item ? item.id : null,
+ group: group ? group.groupId : null,
+ what: what,
+ pageX: event.srcEvent ? event.srcEvent.pageX : event.pageX,
+ pageY: event.srcEvent ? event.srcEvent.pageY : event.pageY,
+ x: x,
+ y: y,
+ time: time,
+ snappedTime: snappedTime
+ };
+ };
+
+ module.exports = Timeline;
+
+/***/ },
+/* 20 */
+/***/ function(module, exports, __webpack_require__) {
+
+ // Only load hammer.js when in a browser environment
+ // (loading hammer.js in a node.js environment gives errors)
+ 'use strict';
+
+ if (typeof window !== 'undefined') {
+ var propagating = __webpack_require__(21);
+ var Hammer = window['Hammer'] || __webpack_require__(22);
+ module.exports = propagating(Hammer, {
+ preventDefault: 'mouse'
+ });
+ } else {
+ module.exports = function () {
+ throw Error('hammer.js is only available in a browser, not in node.js.');
+ };
+ }
+
+/***/ },
+/* 21 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;'use strict';
+
+ (function (factory) {
+ if (true) {
+ // AMD. Register as an anonymous module.
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory();
+ } else {
+ // Browser globals (root is window)
+ window.propagating = factory();
+ }
+ }(function () {
+ var _firstTarget = null; // singleton, will contain the target element where the touch event started
+ var _processing = false; // singleton, true when a touch event is being handled
+
+ /**
+ * Extend an Hammer.js instance with event propagation.
+ *
+ * Features:
+ * - Events emitted by hammer will propagate in order from child to parent
+ * elements.
+ * - Events are extended with a function `event.stopPropagation()` to stop
+ * propagation to parent elements.
+ * - An option `preventDefault` to stop all default browser behavior.
+ *
+ * Usage:
+ * var hammer = propagatingHammer(new Hammer(element));
+ * var hammer = propagatingHammer(new Hammer(element), {preventDefault: true});
+ *
+ * @param {Hammer.Manager} hammer An hammer instance.
+ * @param {Object} [options] Available options:
+ * - `preventDefault: true | 'mouse' | 'touch' | 'pen'`.
+ * Enforce preventing the default browser behavior.
+ * Cannot be set to `false`.
+ * @return {Hammer.Manager} Returns the same hammer instance with extended
+ * functionality
+ */
+ return function propagating(hammer, options) {
+ var _options = options || {
+ preventDefault: false
+ };
+
+ if (hammer.Manager) {
+ // This looks like the Hammer constructor.
+ // Overload the constructors with our own.
+ var Hammer = hammer;
+
+ var PropagatingHammer = function(element, options) {
+ var o = Object.create(_options);
+ if (options) Hammer.extend(o, options);
+ return propagating(new Hammer(element, o), o);
+ };
+ Hammer.extend(PropagatingHammer, Hammer);
+
+ PropagatingHammer.Manager = function (element, options) {
+ var o = Object.create(_options);
+ if (options) Hammer.extend(o, options);
+ return propagating(new Hammer.Manager(element, o), o);
+ };
+
+ return PropagatingHammer;
+ }
+
+ // create a wrapper object which will override the functions
+ // `on`, `off`, `destroy`, and `emit` of the hammer instance
+ var wrapper = Object.create(hammer);
+
+ // attach to DOM element
+ var element = hammer.element;
+ element.hammer = wrapper;
+
+ // register an event to catch the start of a gesture and store the
+ // target in a singleton
+ hammer.on('hammer.input', function (event) {
+ if (_options.preventDefault === true || (_options.preventDefault === event.pointerType)) {
+ event.preventDefault();
+ }
+ if (event.isFirst) {
+ _firstTarget = event.target;
+ }
+ });
+
+ /** @type {Object.<String, Array.<function>>} */
+ wrapper._handlers = {};
+
+ /**
+ * Register a handler for one or multiple events
+ * @param {String} events A space separated string with events
+ * @param {function} handler A callback function, called as handler(event)
+ * @returns {Hammer.Manager} Returns the hammer instance
+ */
+ wrapper.on = function (events, han