645 lines
16 KiB
JavaScript
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);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|