emsApplication/applications/WebConfigure/web/js/lib/timeglider/TG_Timeline.js

645 lines
16 KiB
JavaScript

/*
* Timeglider for Javascript / jQuery
* http://timeglider.com/jquery
*
* Copyright 2011, Mnemograph LLC
* Licensed under Timeglider Dual License
* http://timeglider.com/jquery/?p=license
*
*/
/*
*
* Timeline
* Backbone Model
*
*/
(function(tg){
var TG_Date = tg.TG_Date,
$ = jQuery,
widget_options = {},
tg_units = TG_Date.units,
MED;
tg.TG_EventCollection = Backbone.Collection.extend({
eventHash:{},
comparator: function(ev) {
return ev.get("startdateObj").sec;
},
setTimelineHash: function(timeline_id, hash) {
this.eventHash[timeline_id] = hash;
},
getTimelineHash: function(timeline_id, hash) {
return this.eventHash[timeline_id];
},
model: tg.TG_Event
});
tg.adjustAllTitleWidths = function (collection) {
_.each(collection.models, function(ev) {
var nw = tg.getStringWidth(ev.get("title"));
ev.set({"titleWidth":nw})
})
};
// map model onto larger timeglider namespace
/////////////////////////////////////////////
tg.TG_Event = Backbone.Model.extend({
urlRoot : '/event',
defaults: {
"title": "Untitled",
"selected":false,
"css_class":''
},
initialize: function(ev) {
// Images start out being given a default width and height
// of 0, so that we can "find out for ourselves" what the
// size is.... pretty costly, though...
// can this be done better with PHP?
if (ev.image) {
var img = ev.image;
if (typeof img == "string") {
var display_class = ev.image_class || "lane";
var image_scale = ev.image_scale || 100;
var image_width = ev.image_width || 0;
var image_height = ev.image_height || 0;
ev.image = {id: ev.id, scale:image_scale, src:ev.image, display_class:display_class, width:image_width, height:image_height};
} else {
// id, src etc already set
ev.image.display_class = ev.image.display_class || "lane";
ev.image.width = 0;
ev.image.height = 0;
ev.image.scale = ev.image.scale || 100;
}
// this will follow up with reporting size in separate "thread"
this.getEventImageSize(ev.image, ev);
// MED.imagesToSize++;
} else {
ev.image = '';
}
// further urldecoding?
// by replacing the & with & we actually
// preserve HTML entities
ev.title = ev.title.replace(/&/g, "&");
ev.description = ev.description || "";
ev.titleWidth = tg.getStringWidth(ev.title);
ev.y_position = ev.y_position || 0;
this.set(ev);
},
getEventImageSize:function(img, ev) {
var that = this,
imgTesting = new Image(),
img_src = imgTesting.src = img.src;
imgTesting.onerror= delegatr(imgTesting, function () {
if (tg.app && typeof tg.app.reportMissingImage == "function") {
tg.app.reportMissingImage(img.src, ev);
}
that.set({"image":{src:img.src,status:"missing"}});
});
imgTesting.onload = delegatr(imgTesting, function () {
that.get("image").height = this.height;
that.get("image").width = this.width;
that.get("image").max_height = this.height;
});
function delegatr(contextObject, delegateMethod) {
return function() {
return delegateMethod.apply(contextObject, arguments);
}
};
}, // end getEventImageSize
reIndex: function(do_delete) {
var model = this,
deleting = do_delete || false,
cache = model.get("cache"),
event_id = model.get("id"),
new_start = model.get("startdateObj"),
new_end = model.get("enddateObj"),
ev_timelines = model.get("timelines"),
ev_timeline_cache = cache.timelines,
cache_start = cache.startdateObj || new_start,
span = cache.span,
timeline = {},
hash = {},
ser = 0, new_ser = 0,
arr = [],
tl_union = _.union(ev_timeline_cache, ev_timelines),
TG_Date = tg.TG_Date,
MED = model.get("mediator"),
TIMELINES = MED.timelineCollection,
EVENTS = MED.eventCollection;
// cycle through all event's past/present timelines
// OUTER .each
_.each(tl_union, function(timeline_id){
timeline = TIMELINES.get(timeline_id);
hash = EVENTS.getTimelineHash(timeline_id);
// remove from "all" array (used for bounds)
hash["all"] = _.reject(hash["all"], function(eid){
// truthy is rejected!!
return eid == event_id;
});
// UNITS: "da", "mo", "ye", "de", "ce", "thou", "tenthou",
// "hundredthou", "mill", "tenmill", "hundredmill", "bill"
// INNER .each
_.each(TG_Date.units, function(unit) {
ser = TG_Date.getTimeUnitSerial(cache_start, unit);
// REMOVE CACHED DATE INDICES FROM HASH
// ALL TIMELINES ARE CLEARED
if (hash[unit][ser] !== undefined) {
hash[unit][ser] = _.reject(hash[unit][ser], function(eid){
// truthy is rejected!
return eid == event_id;
});
}
// RE-INDEX IN EVENT'S CURRENT TIMELINES ARRAY!!
if (deleting != true) {
if ($.inArray(timeline_id, ev_timelines) != -1) {
new_ser = TG_Date.getTimeUnitSerial(new_start, unit);
if (hash[unit][new_ser] !== undefined) {
hash[unit][new_ser].push(event_id);
} else {
// create the array
hash[unit][new_ser] = [event_id];
}
}
} // end if not deleting
}); // end inner _.each
if (deleting != true) {
if ($.inArray(timeline_id, ev_timelines) != -1) {
hash["all"].push(event_id);
}
}
// REFRESH BOUNDS: CYCLE THROUGH HASH'S "all" INDEX
// INCLUDE ALL IN UNIONED TIMELINES
var bounds = timeline.get("bounds");
var spill = [];
_.each(hash["all"], function (id) {
var ev = EVENTS.get(id);
spill.push(ev.get("startdateObj").sec);
spill.push(ev.get("enddateObj").sec);
});
// does it have any events
// totally new set of bounds!
timeline.set({bounds:{first:_.min(spill), last:_.max(spill)}});
var timeline_spans = timeline.get("spans");
// WIPE OUT OLD SPAN REF NO MATTER WHAT
if (cache.span) {
delete timeline_spans["s_" + event_id];
}
// RE/LIST SPAN
if (deleting != true) {
if (model.get("span") == true) {
timeline_spans["s_" + event_id] = {id:event_id, start:new_start.sec, end:new_end.sec};
}
}
// make sure timeline "has_events" is accurate
timeline.set({has_events:hash["all"].length});
}); // end outer/first _.each, cycling across timelines cached/new
}
});
// map model onto larger timeglider namespace
/////////////////////////////////////////////
tg.TG_Timeline = Backbone.Model.extend({
urlRoot : '/timeline',
defaults: {
// no other defaults?
"initial_zoom":1,
"timezone":"00:00",
"title": "Untitled",
"with_events":true,
"events": [],
"legend": [],
"tags":{}
},
// processes init model data, adds certain calculated values
_chewTimeline : function (tdata) {
// TODO ==> add additional units
MED = tdata.mediator;
tdata.timeline_id = tdata.id;
widget_options = MED.options;
var dhash = {
"all":[],
"da":[],
"mo":[],
"ye":[],
"de":[],
"ce":[],
"thou":[],
"tenthou":[],
"hundredthou":[],
"mill":[],
"tenmill":[],
"hundredmill":[],
"bill":[]
};
tdata.spans = {};
tdata.hasImageLane = false;
tdata.startSeconds = [];
tdata.endSeconds = [];
tdata.initial_zoom = parseInt(tdata.initial_zoom, 10) || 25;
tdata.inverted = tdata.inverted || false;
// render possible adjective/numeral strings to numeral
tdata.size_importance = (tdata.size_importance == "false" || tdata.size_importance == "0")? 0 : 1;
tdata.is_public = (tdata.is_public == "false" || tdata.is_public == "0")? 0 : 1;
// widget options timezone default is "00:00";
var tzoff = tdata.timezone || "00:00";
tdata.timeOffset = TG_Date.getTimeOffset(tzoff);
// TODO: VALIDATE COLOR, centralize default color(options?)
if (!tdata.color) { tdata.color = "#333333"; }
if (tdata.events.length>0 && !tdata.preload) {
var date, ddisp, ev, id, unit, ser, tWidth;
var l = tdata.events.length;
for(var ei=0; ei< l; ei++) {
ev=tdata.events[ei];
ev.css_class = ev.css_class || "";
// make sure it has an id!
if (ev.id) {
id = ev.id
} else {
// if lacking an id, we'll make one...
ev.id = id = "anon" + this.anonEventId++;
}
ev.importance = parseInt(ev.importance, 10) + widget_options.boost;
ev.low_threshold = ev.low_threshold || 1;
ev.high_threshold = ev.high_threshold || 100;
/*
We do some pre-processing ** INCLUDING HASHING THE EVENT *
BEFORE putting the event into it's Model&Collection because some
(processed) event attributes are needed at the timeline level
*/
if (ev.map) {
if (MED.main_map) {
if (timeglider.mapping.ready){
ev.map.marker_instance = timeglider.mapping.addAddMarkerToMap(ev, MED.main_map);
// debug.log("marker_instance", ev.map.marker_instance);
}
// requires TG_Mapping.js component
} else {
// debug.log("NO MAIN MAP... BUT LOAD MAPS FOR MODAL");
// load instance of maps for modal viewing
// requires: TG_Mapping.js
tg.googleMapsLoad();
}
}
ev.callbacks = ev.callbacks || {};
if (typeof ev.date_display == "object") {
ddisp = "ho";
} else {
// date_limit is allowed old JSON prop name,
// replaced by date_display
ddisp = ev.date_display || ev.date_limit || "ho";
}
ev.date_display = ddisp.toLowerCase().substr(0,2);
if (ev.link) {
if (typeof ev.link == "string" && ev.link.substr(0,4) == "http") {
// make an array
ev.link = [{"url":ev.link, "label":"link"}]
}
} else {
ev.link = "";
}
ev.date_display = ddisp.toLowerCase().substr(0,2);
// if a timezone offset is set on the timeline, adjust
// any events that do not have the timezone set on them
ev.keepCurrent = 0; // not a perpetual now startdate
if (ev.startdate.substr(0,10) == "7777-12-31"
|| ev.startdate.substr(0,10) == "8888-12-31"
|| ev.startdate == "now"
|| ev.startdate == "today") {
// PERPETUAL NOW EVENT
ev.startdate = TG_Date.getToday();
// 7777-12-31 and "now" behave the same
if (ev.startdate == "now" || ev.startdate == "8888-12-31") {
ev.keepCurrent += 1;
ev.css_class += " ongoing";
}
}
ev.zPerp = "0"; // not a perpetual now enddate
if (typeof ev.enddate == "string" && (ev.enddate.substr(0,10) == "7777-12-31"
|| ev.enddate.substr(0,10) == "8888-12-31"
|| ev.enddate == "now"
|| ev.enddate == "today")) {
ev.enddate = TG_Date.getToday();
// 7777-12-31 and "now" behave the same
if (ev.enddate == "now" || ev.enddate == "8888-12-31") {
ev.keepCurrent += 2;
ev.css_class += " ongoing";
}
}
// if keepCurrent == 1 only start
// if keepCurrent == 2 only end
// if keepCurrent == 3 both start and end are "now"
if (tdata.timeOffset.seconds) {
ev.startdate = TG_Date.tzOffsetStr(ev.startdate, tdata.timeOffset.string);
if (ev.enddate) {
ev.enddate = TG_Date.tzOffsetStr(ev.enddate, tdata.timeOffset.string);
}
}
ev.startdateObj = new TG_Date(ev.startdate, ev.date_display);
// !TODO: only if they're valid!
if ((ev.enddate) && (ev.enddate !== ev.startdate)){
ev.enddateObj = new TG_Date(ev.enddate, ev.date_display);
ev.span=true;
// index it rather than push to stack
tdata.spans["s_" + ev.id] = {id:ev.id, start:ev.startdateObj.sec, end:ev.enddateObj.sec};
} else {
ev.enddateObj = ev.startdateObj;
ev.span = false;
}
// haven't parsed the image/image_class business...
if (ev.image) {
if (ev.image.display_class != "inline") {
tdata.hasImageLane = true;
}
}
tdata.startSeconds.push(ev.startdateObj.sec);
tdata.endSeconds.push(ev.enddateObj.sec);
// cache the initial date for updating hash later
// important for edit/delete operations
ev.cache = {timelines:[tdata.timeline_id], span:ev.span, startdateObj:_.clone(ev.startdateObj), enddateObj:_.clone(ev.enddateObj)}
if (!ev.icon || ev.icon === "none") {
ev.icon = "";
} else {
ev.icon = ev.icon;
}
if ((!isNaN(ev.startdateObj.sec))&&(!isNaN(ev.enddateObj.sec))){
dhash["all"].push(id);
var uxl = tg_units.length;
for (var ux = 0; ux < uxl; ux++) {
unit = tg_units[ux];
///// DATE HASHING in action
ser = TG_Date.getTimeUnitSerial(ev.startdateObj, unit);
if (dhash[unit][ser] !== undefined) {
var shash = dhash[unit][ser];
if (_.indexOf(shash, id) === -1) {
dhash[unit][ser].push(id);
}
} else {
// create the array
dhash[unit][ser] = [id];
}
/////////////////////////////
}
ev.mediator = MED;
/////////////////////////////////
if (!MED.eventCollection.get(id)) {
ev.timelines = [tdata.timeline_id];
var new_model = new tg.TG_Event(ev);
// model is defined in the eventCollection
// we just need to add the raw object here and it
// is "vivified", properties set, etc
MED.eventCollection.add(new_model);
} else {
// trusting here that this is a true duplicate!
// just needs to be associated with the timeline
var existing_model = MED.eventCollection.get(id);
existing_model.get("timelines").push(tdata.timeline_id);
}
} // end if !NaN
} // end for: cycling through timeline's events
// cycle through timeline, collecting start, end arrays
// sort start, select first
// sor last select last
// set bounds
var merged = $.merge(tdata.startSeconds,tdata.endSeconds);
var sorted = _.sortBy(merged, function(g){ return parseInt(g); });
/// bounds of timeline
tdata.bounds = {"first": _.first(sorted), "last":_.last(sorted) };
var date_from_sec = TG_Date.getDateFromSec(tdata.bounds.first);
tdata.focus_date = tdata.focus_date || date_from_sec;
tdata.focusDateObj = new TG_Date(tdata.focus_date);
tdata.has_events = 1;
} else {
tdata.tags = tdata.tags || {"test":1};
tdata.focus_date = tdata.focus_date || "today";
tdata.focusDateObj = new TG_Date(tdata.focus_date);
tdata.bounds = {"first": tdata.focusDateObj.sec, "last":tdata.focusDateObj.sec + 86400};
tdata.has_events = 0;
}
/* !TODO: necessary to parse this now, or just leave as is? */
if (tdata.legend.length > 0) {
//var legend = tdata.legend;
//for (var i=0; i<legend.length; i++) {
// var legend_item = legend[i];
// debug.log("leg. title:" + legend_item['title'])
//}
tdata.hasLegend = true;
} else {
tdata.hasLegend = false;
}
if (tdata.lanes && tdata.lanes.length > 0) {
tdata.hasLanes = true;
tdata.useLanes = true;
} else {
tdata.hasLanes = false;
tdata.useLanes = false;
}
/// i.e. expanded or compressed...
/// ought to be attribute at the timeline level
/// TODO: create a $.merge for defaults for a timeline
tdata.display = "expanded";
MED.eventCollection.setTimelineHash(tdata.timeline_id, dhash);
// keeping events in eventCollection
// hashing references to evnet IDs inside the date hash
delete tdata.events;
return tdata;
},
initialize: function(attrs) {
var processed = this._chewTimeline(attrs);
this.set(processed);
this.bind("change", function() {
// debug.log("changola");
});
}
});
})(timeglider);