Exhibit is a great tool. Kudos to whoever is working on it!
I made the following extensions and bug fixes to Exhibit in developing the Pedi-maps feature at WeRelate.org. I'm posting them here in case they might be helpful to anyone else.
As an aside, I used ex:if, ex:formats, and ex:href-subcontent attributes in the HTML file. Although these attributes are documented I wasn't able to find other example websites that showed how they could be used. You're welcome click on the above Pedi-maps link and view the source to see how I used them.
Contents |
Remove dependencies
On JQuery
I already use jquery 1.4 and I didn't want two additional versions of jquery 1.2.1 loaded, so I deleted jquery 1.2.1 from the exhibit and timeline directories and rebuilt.
On MIT website
I didn't want to be dependent upon the Simile website, so I removed a couple hard-coded references to MIT and replaced them with hard-coded references to my directory structure.
--- extensions/time/time-extension.js (revision 9034)
+++ extensions/time/time-extension.js (working copy)
@@ -17,17 +17,17 @@
"timeline-view.css"
];
var url = SimileAjax.findScript(document, "/time-extension.js");
if (url == null) {
SimileAjax.Debug.exception(new Error("Failed to derive URL prefix for Simile Exhibit Time Extension code files"));
return;
}
Exhibit.TimeExtension.urlPrefix = url.substr(0, url.indexOf("time-extension.js"));
var paramTypes = { bundle: Boolean };
SimileAjax.parseURLParameters(url, Exhibit.TimeExtension.params, paramTypes);
- var scriptURLs = [ "http://static.simile.mit.edu/timeline/api-2.0/timeline-api.js" ];
+ var scriptURLs = [ "/w/timeline/src/webapp/api/timeline-api.js" ];
var cssURLs = [];
if (Exhibit.TimeExtension.params.bundle) {
--- api/exhibit-api.js (revision 9034)
+++ api/exhibit-api.js (working copy)
@@ -228,9 +228,7 @@
if (typeof SimileAjax == "undefined") {
window.SimileAjax_onLoad = loadMe;
- var url = useLocalResources ?
- "http://127.0.0.1:8888/ajax/api/simile-ajax-api.js" :
- "http://static.simile.mit.edu/ajax/api-2.0/simile-ajax-api.js";
+ var url = "/w/exhibit/src/ajax/api/simile-ajax-api.js";
var createScriptElement = function() {
var script = document.createElement("script");
On Painter
Painter is great for prototyping, but it's more efficient to load static images once you know what they're going to look like. I added some code to allow me to pass in my own functions for generating color markers, size markers, and icons.
--- extensions/map/scripts/map-view.js (revision 9034)
+++ extensions/map/scripts/map-view.js (working copy)
@@ -146,6 +147,34 @@
Exhibit.SettingsUtilities.createAccessorsFromDOM(configElmt, Exhibit.MapView._accessorSpecs, view._accessors);
Exhibit.SettingsUtilities.collectSettingsFromDOM(configElmt, Exhibit.MapView._settingSpecs, view._settings);
+ var s = Exhibit.getAttribute(configElmt, "colorMarkerGenerator");
+ if (s != null && s.length > 0) {
+ var f = eval(s);
+ if (typeof f == "function") {
+ view._settings.colorMarkerGenerator = f;
+ }
+ }
+ var s = Exhibit.getAttribute(configElmt, "sizeMarkerGenerator");
+ if (s != null && s.length > 0) {
+ var f = eval(s);
+ if (typeof f == "function") {
+ view._settings.sizeMarkerGenerator = f;
+ }
+ }
+ var s = Exhibit.getAttribute(configElmt, "makeIcon");
+ if (s != null && s.length > 0) {
+ var f = eval(s);
+ if (typeof f == "function") {
+ view._settings.makeIcon = f;
+ }
+ }
Exhibit.MapView._configure(view, configuration);
view._internalValidate();
@@ -163,6 +192,18 @@
accessors.getLatlng(proxy, database, visitor);
});
};
+ if ("addOverlaysCallback" in configuration) {
+ view._settings.addOverlaysCallback = configuration.addOverlaysCallback;
+ }
+ if ("colorMarkerGenerator" in configuration) {
+ view._settings.colorMarkerGenerator = configuration.colorMarkerGenerator;
+ }
+ if ("sizeMarkerGenerator" in configuration) {
+ view._settings.sizeMarkerGenerator = configuration.sizeMarkerGenerator;
+ }
+ if ("makeIcon" in configuration) {
+ view._settings.makeIcon = configuration.makeIcon;
+ }
};
Exhibit.MapView.lookupLatLng = function(set, addressExpressionString, outputProperty, outputTextArea, database, accuracy) {
@@ -292,7 +335,11 @@
var legendWidgetSettings = {};
legendWidgetSettings.colorGradient = (this._colorCoder != null && "_gradientPoints" in this._colorCoder);
- legendWidgetSettings.colorMarkerGenerator = function(color) {
+ if (settings.colorMarkerGenerator != null) {
+ legendWidgetSettings.colorMarkerGenerator = settings.colorMarkerGenerator;
+ }
+ else {
+ legendWidgetSettings.colorMarkerGenerator = function(color) {
var shape=settings.shape;
return SimileAjax.Graphics.createTranslucentImage(
Exhibit.MapView._markerUrlPrefix+
@@ -300,8 +347,13 @@
"&width=20&height=20&pinHeight=5&background="+color.substr(1),
"middle"
);
+ }
}
- legendWidgetSettings.sizeMarkerGenerator = function(iconSize) {
+ if (settings.sizeMarkerGenerator != null) {
+ legendWidgetSettings.sizeMarkerGenerator = settings.sizeMarkerGenerator;
+ }
+ else {
+ legendWidgetSettings.sizeMarkerGenerator = function(iconSize) {
var shape=settings.shape;
return SimileAjax.Graphics.createTranslucentImage(
Exhibit.MapView._markerUrlPrefix+
@@ -311,6 +363,7 @@
"&pinHeight=0",
"middle"
);
+ }
}
legendWidgetSettings.iconMarkerGenerator = function(iconURL) {
elmt = document.createElement('img');
@@ -474,14 +529,27 @@
icon = self._iconCoder.translateSet(locationData.iconKeys, iconCodingFlags);
}
- var icon = Exhibit.MapView._makeIcon(
+ var icon;
+ if (self._settings.makeIcon != null) {
+ icon = self._settings.makeIcon(
shape,
color,
iconSize,
itemCount == 1 ? "" : itemCount.toString(),
icon,
self._settings
- );
+ );
+ }
+ else {
+ icon = Exhibit.MapView._makeIcon(
+ shape,
+ color,
+ iconSize,
+ itemCount == 1 ? "" : itemCount.toString(),
+ icon,
+ self._settings
+ );
+ }
var point = new GLatLng(locationData.latlng.lat, locationData.latlng.lng);
var marker = new GMarker(point, icon);
Fix a few minor bugs / oversights
Show toolbox should be an option everywhere
--- extensions/map/scripts/map-view.js (revision 9034)
+++ extensions/map/scripts/map-view.js (working copy)
@@ -64,7 +64,8 @@
"markerScale": { type: "text", defaultValue: null },
"showHeader": { type: "boolean", defaultValue: true },
"showSummary": { type: "boolean", defaultValue: true },
- "showFooter": { type: "boolean", defaultValue: true }
+ "showFooter": { type: "boolean", defaultValue: true },
+ "showToolbox": { type: "boolean", defaultValue: true }
};
Exhibit.MapView._accessorSpecs = [
@@ -236,8 +277,10 @@
}
this._itemIDToMarker = {};
- this._toolboxWidget.dispose();
- this._toolboxWidget = null;
+ if (this._toolboxWidget) {
+ this._toolboxWidget.dispose();
+ this._toolboxWidget = null;
+ }
this._dom.dispose();
this._dom = null;
@@ -332,7 +385,9 @@
legendWidgetSettings
);
- this._toolboxWidget = Exhibit.ToolboxWidget.createFromDOM(this._div, this._div, this._uiContext);
+ if (this._settings.showToolbox) {
+ this._toolboxWidget = Exhibit.ToolboxWidget.createFromDOM(this._div, this._div, this._uiContext);
+ }
var mapDiv = this._dom.plotContainer;
mapDiv.style.height = settings.mapHeight + "px";
--- extensions/time/scripts/timeline-view.js (revision 9034)
+++ extensions/time/scripts/timeline-view.js (working copy)
@@ -43,7 +43,8 @@
"selectCoordinator": { type: "text", defaultValue: null },
"showHeader": { type: "boolean", defaultValue: true },
"showSummary": { type: "boolean", defaultValue: true },
- "showFooter": { type: "boolean", defaultValue: true }
+ "showFooter": { type: "boolean", defaultValue: true },
+ "showToolbox": { type: "boolean", defaultValue: true }
};
Exhibit.TimelineView._accessorSpecs = [
@@ -127,8 +128,10 @@
this._selectListener = null;
}
- this._toolboxWidget.dispose();
- this._toolboxWidget = null;
+ if (this._toolboxWidget) {
+ this._toolboxWidget.dispose();
+ this._toolboxWidget = null;
+ }
this._dom.dispose();
this._dom = null;
@@ -176,7 +179,10 @@
},
legendWidgetSettings
);
- this._toolboxWidget = Exhibit.ToolboxWidget.createFromDOM(this._div, this._div, this._uiContext);
+
+ if (this._settings.showToolbox) {
+ this._toolboxWidget = Exhibit.ToolboxWidget.createFromDOM(this._div, this._div, this._uiContext);
+ }
this._eventSource = new Timeline.DefaultEventSource();
this._reconstruct();
Bug in maps code
If you have a multi-valued proxy field, you'll want this fix or you'll end up with different locationData's having the same key sets.
--- extensions/map/scripts/map-view.js (revision 9034)
+++ extensions/map/scripts/map-view.js (working copy)
@@ -432,9 +487,9 @@
latlng: latlng,
items: [ itemID ]
};
- if (hasColorKey) { locationData.colorKeys = colorKeys;}
- if (hasSizeKey) { locationData.sizeKeys = sizeKeys; }
- if (hasIconKey) { locationData.iconKeys = iconKeys; }
+ if (hasColorKey) { locationData.colorKeys = new Exhibit.Set(); locationData.colorKeys.addSet(colorKeys);}
+ if (hasSizeKey) { locationData.sizeKeys = new Exhibit.Set(); locationData.sizeKeys.addSet(sizeKeys); }
+ if (hasIconKey) { locationData.iconKeys = new Exhibit.Set(); locationData.iconKeys.addSet(iconKeys); }
locationToData[latlngKey] = locationData;
}
}
Auto-zoom maps
This fix was suggested previously in the mailing list. I tweaked it a bit to extend the bounds a little, but I'm too lazy to do it correctly based upon the zoom level.
--- extensions/map/scripts/map-view.js (revision 9034)
+++ extensions/map/scripts/map-view.js (working copy)
@@ -592,12 +668,19 @@
}
}
- if (bounds && typeof settings.zoom == "undefined") {
- var zoom = Math.max(0, self._map.getBoundsZoomLevel(bounds) - 1);
+ if (bounds) { // && typeof settings.zoom == "undefined") {
+ // add bounds extension by margin instead of -1 on getBoundsZoomLevel
+ var margin = 0.3; // HACK - too lazy to calculate it based upon the zoom level
+ var p = new GLatLng(bounds.getSouthWest().lat(), bounds.getSouthWest().lng() - margin);
+ bounds.extend(p);
+ p = new GLatLng(bounds.getNorthEast().lat() + margin, bounds.getNorthEast().lng() + margin);
+ bounds.extend(p);
+ var zoom = Math.max(0, self._map.getBoundsZoomLevel(bounds));
+ if (settings.maxAutoZoom == undefined) settings.maxAutoZoom = Infinity;
zoom = Math.min(zoom, maxAutoZoom, settings.maxAutoZoom);
self._map.setZoom(zoom);
}
- if (bounds && typeof settings.center == "undefined") {
+ if (bounds) { // && typeof settings.center == "undefined") {
self._map.setCenter(bounds.getCenter());
}
}
IE weirdness with Thumbnail view
For me, when I tried to display the thumbnail view in IE, the thumbnail lenses all overwrote each other. I finally made the following change and it worked. Not sure what was meant by having a separate style for IE here, since the regular style settings worked for me but the IE-specific style settings did not.
--- api/styles/views/thumbnail-view.css (revision 9034)
+++ api/styles/views/thumbnail-view.css (working copy)
@@ -11,6 +11,4 @@
div.exhibit-thumbnailView-itemContainer-IE {
float: left;
- width: 0;
- overflow: visible;
}
Default color coder
I added some additional colors to the 8 colors that the default color coder rotates through. I know I could have created by own color coder, but why go to the trouble when I just wanted to rotate a few more colors than the default 8?
en-US locale
This was causing trouble for me since exhibit was looking for an en-US locale but there is none.
--- api/exhibit-api.js (revision 9034)
+++ api/exhibit-api.js (working copy)
@@ -121,7 +121,7 @@
var defaultClientLocales = ("language" in navigator ? navigator.language : navigator.browserLanguage).split(";");
for (var l = 0; l < defaultClientLocales.length; l++) {
var locale = defaultClientLocales[l];
- if (locale != "en") {
+ if (locale != "en" && locale != "en-US") {
var segments = locale.split("-");
if (segments.length > 1 && segments[0] != "en") {
Exhibit.locales.push(segments[0]);
Bug with opening bubbles
This was causing an exception on my machine. I'm not positive that changing elmt to anchorElmt is the correct fix, but it seems to work.
--- api/scripts/util/views.js (revision 9034)
+++ api/scripts/util/views.js (working copy)
@@ -9,8 +9,8 @@
Exhibit.ViewUtilities.openBubbleForItems = function(anchorElmt, arrayOfItemIDs, uiContext) {
var coords = SimileAjax.DOM.getPageCoordinates(anchorElmt);
var bubble = SimileAjax.Graphics.createBubbleForPoint(
- coords.left + Math.round(elmt.offsetWidth / 2),
- coords.top + Math.round(elmt.offsetHeight / 2),
+ coords.left + Math.round(anchorElmt.offsetWidth / 2),
+ coords.top + Math.round(anchorElmt.offsetHeight / 2),
uiContext.getSetting("bubbleWidth"), // px
uiContext.getSetting("bubbleHeight") // px
);
Add additional overlays to maps
I wanted to add paths between the places being mapped for each person. The addOverlays callback allows me to add additional overlays to the map after Exhibit has finished adding its overlays.
--- extensions/map/scripts/map-view.js (revision 9034)
+++ extensions/map/scripts/map-view.js (working copy)
@@ -146,6 +147,34 @@
Exhibit.SettingsUtilities.createAccessorsFromDOM(configElmt, Exhibit.MapView._accessorSpecs, view._accessors);
Exhibit.SettingsUtilities.collectSettingsFromDOM(configElmt, Exhibit.MapView._settingSpecs, view._settings);
+ var s = Exhibit.getAttribute(configElmt, "addOverlaysCallback");
+ if (s != null && s.length > 0) {
+ var f = eval(s);
+ if (typeof f == "function") {
+ view._settings.addOverlaysCallback = f;
+ }
+ }
@@ -505,6 +573,14 @@
for (var latlngKey in locationToData) {
addMarkerAtLocation(locationToData[latlngKey]);
}
+
+ // call callback in case the caller wants to add any additional overlays to the map
+ if (self._settings.addOverlaysCallback != null) {
+ currentSet.visit(function(itemID) {
+ self._settings.addOverlaysCallback(self, itemID);
+ });
+ }
+
if (hasColorKey) {
var legendWidget = this._dom.legendWidget;
var colorCoder = this._colorCoder;
Add a "Template" view
This view allows you to append items to different slots (divs) of a passed-in template based upon the value of some field. I used this to create the pedigree view, and I think it's generally useful.
/*==================================================
* Exhibit.TemplateView
*==================================================
*/
Exhibit.TemplateView = function(containerElmt, uiContext) {
this._div = containerElmt;
this._uiContext = uiContext;
this._settings = { tempate: null, lenses: null };
var view = this;
this._listener = {
onItemsChanged: function() {
view._reconstruct();
}
};
uiContext.getCollection().addListener(this._listener);
};
Exhibit.TemplateView._settingSpecs = {
"showToolbox": { type: "boolean", defaultValue: true },
"showSummary": { type: "boolean", defaultValue: true },
"slotIDPrefix": { type: "text", defaultValue: '' },
"slotKey": { type: "text", defaultValue: null },
"slotClass": { type: "text", defaultValue: null }
};
Exhibit.TemplateView.create = function(configuration, containerElmt, uiContext) {
var view = new Exhibit.TemplateView(
containerElmt,
Exhibit.UIContext.create(configuration, uiContext)
);
Exhibit.TemplateView._configure(view, configuration);
view._initializeUI();
return view;
};
Exhibit.TemplateView.createFromDOM = function(configElmt, containerElmt, uiContext) {
var configuration = Exhibit.getConfigurationFromDOM(configElmt);
uiContext = Exhibit.UIContext.createFromDOM(configElmt, uiContext);
var view = new Exhibit.TemplateView(
containerElmt != null ? containerElmt : configElmt,
uiContext
);
Exhibit.SettingsUtilities.collectSettingsFromDOM(configElmt, Exhibit.TemplateView._settingSpecs, view._settings);
var s = Exhibit.getAttribute(configElmt, "template");
if (s != null && s.length > 0) {
var o = eval(s);
if (typeof o == "object") {
view._settings.template = o;
}
}
s = Exhibit.getAttribute(configElmt, "lenses");
if (s != null && s.length > 0) {
o = eval(s);
if (typeof o == "object") {
view._settings.lenses = {};
for (var lensType in o) {
var template = o[lensType];
var templateResult = SimileAjax.DOM.createDOMFromTemplate(template);
view._settings.lenses[lensType] = Exhibit.Lens.compileTemplate(templateResult.elmt, false, uiContext);
}
}
}
Exhibit.TemplateView._configure(view, configuration);
view._initializeUI();
return view;
};
Exhibit.TemplateView._configure = function(view, configuration) {
Exhibit.SettingsUtilities.collectSettings(configuration, Exhibit.TemplateView._settingSpecs, view._settings);
if ("template" in configuration) {
view._settings.template = configuration.template;
}
if ("lenses" in configuration) {
view._settings.lenses = configuration.lenses;
}
};
Exhibit.TemplateView.prototype.dispose = function() {
this._uiContext.getCollection().removeListener(this._listener);
if (this._toolboxWidget) {
this._toolboxWidget.dispose();
this._toolboxWidget = null;
}
this._collectionSummaryWidget.dispose();
this._collectionSummaryWidget = null;
this._uiContext.dispose();
this._uiContext = null;
this._div.innerHTML = "";
this._dom = null;
this._div = null;
};
Exhibit.TemplateView.prototype._initializeUI = function() {
var self = this;
this._div.innerHTML = "";
this._dom = Exhibit.TemplateView.createDom(this._div, this._settings.template);
this._collectionSummaryWidget = Exhibit.CollectionSummaryWidget.create(
{},
this._dom.collectionSummaryDiv,
this._uiContext
);
if (this._settings.showToolbox) {
this._toolboxWidget = Exhibit.ToolboxWidget.createFromDOM(this._div, this._div, this._uiContext);
this._toolboxWidget.getGeneratedHTML = function() {
return self._dom.bodyDiv.innerHTML;
};
}
if (!this._settings.showSummary) {
this._dom.collectionSummaryDiv.style.display = "none";
}
this._reconstruct();
};
Exhibit.TemplateView.prototype._reconstruct = function() {
var self = this;
var collection = this._uiContext.getCollection();
var database = this._uiContext.getDatabase();
var bodyDiv = this._dom.bodyDiv;
/*
* Clear the slots
*/
$('.'+this._settings.slotClass).html('');
/*
* Get the current collection and check if it's empty
*/
if (collection.countRestrictedItems() > 0) {
var currentSet = collection.getRestrictedItems();
/*
* Create item rows
*/
currentSet.visit(function(itemID) {
var slotKey = database.getObject(itemID, self._settings.slotKey);
var node = document.getElementById(self._settings.slotIDPrefix+slotKey);
var lensTemplate = self._settings.lenses[slotKey];
if (!lensTemplate) {
lensTemplate = self._settings.lenses['*'];
}
if (node != null && lensTemplate != null) {
Exhibit.Lens.constructFromLensTemplate(itemID, lensTemplate, node, self._uiContext);
}
});
}
};
Exhibit.TemplateView.createDom = function(div, template) {
var l10n = Exhibit.TemplateView.l10n;
var templateResult = SimileAjax.DOM.createDOMFromTemplate(template)
var headerTemplate = {
elmt: div,
className: "exhibit-collectionView-header",
children: [
{ tag: "div",
field: "collectionSummaryDiv"
},
{ elmt: templateResult.elmt,
field: "bodyDiv"
}
]
};
return SimileAjax.DOM.createDOMFromTemplate(headerTemplate);
};

