/* * 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 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);