Janus Projekt (2013)
Using Google Maps, the Janus Projekt website helps the Ancestry.com Deutschland content acquisition team illustrate their increasing catalogue and pursue new partners. The team independently manages website content and partners via an admin interface.
The Importance of Maps
Ancestry.com's business model relies on digitized public records. Unlike the U.S., the public registries for births, marriages and deaths are not nationalized in Germany. The Janus Projekt is an Ancestry effort to build a national registry by cooperating with all archives including small local and communal archives.
The breadth of the map is encouraging for the team to continue and archivists to join. For example, small communal archives may want to be listed on the same footing as a large national institution like the Bundesarchiv.
Features
- The content team manages the map by simply curating a database of partners.
- The info window popup is populated by partner information.
- The map coordinates are determined via Google Maps geolocation from an address.
Stack
- Ruby on Rails - powers the backend of the website
- Redactor - for the WYSIWYG editor
- Devise - for user authentication.
- Simple HTTP Authentication - because there is no need to do registration or forgot passwords, we don't need a full loaded gem like Devise.
- Paperclip - to handle image uploads
- Underscore.js - for useful functional helpers.
Sample JavaScript
Google Maps is great. But there is a lot of JavaScript required to customize a map. I wrapped my methods in a GoogleMap
object. Even if I do not use a framework, I am fastidious about organizing and documenting my code for future use and reference.
Please scroll to view the entire JavaScript object.
var GoogleMap = {
markers: [],
tooltips: [],
// defaults
mapEl: 'js-map',
options: {
lat: 48.1368244,
lng: 11.568748899999946,
markers: {
standard: '<%= asset_path('markers/cherry.png') %>',
parish: '<%= asset_path('markers/orange.png') %>',
},
originMarker: false,
infoWindow: true,
google: {},
styled: true,
partnerTypes: '<%= ENV["PARTNER_TYPES"] %>',
parishLogo: '<%= asset_path('parish.png') %>'
},
googleOptions: function(){
return {
startZoom: '<%= ENV['START_ZOOM'] %>'*1,
maxZoom: '<%= ENV['MAX_ZOOM'] %>'*1,
zoomControl: true,
mapTypeControl: false,
mapTypeId: google.maps.MapTypeId.ROADMAP,
styles: [
{
featureType: 'administrative.province',
elementType: 'geometry.stroke',
stylers: [
{color: '#ff8080'},
{weight: 1.5}
]
},
{
featureType: 'road.highway',
elementType: 'geometry',
stylers: [
{visibility: 'off'}
]
},
{
featureType: 'road.highway',
elementType: 'labels',
stylers: [
{visibility: 'off'}
]
}
]
}
},
infoWindowMarkup:
"<div class='map-infowindow'> \
<strong>{{name}}</strong><br> \
{{text}} \
<img src='{{logo}}'> \
</div>",
init: function(el, options) {
this.setMapEl(el);
options = options || {};
if (options.zoom) {
this.options.google.zoom = options.zoom;
options = _.omit(options, 'zoom');
}
if (options.marker) {
this.customMarker = true;
}
_.extend(this.options, options);
if (window.console) {
console.log('GoogleMap options on init:');
console.log(this.options);
}
this.compileTemplate();
this.createMap();
return this;
},
/**
* Set DOM Element for our map
*
* @method setMapEl
* @param el {String}
*/
setMapEl: function(el){
el = (el[0] == '#') ? el.substr(1) : el;
this.mapEl = document.getElementById(el);
},
/**
* Create Map
*
* @method createMap
*/
createMap: function () {
var mapCenter = new google.maps.LatLng(this.options.lat, this.options.lng),
mapOptions = _.extend(this.options.google, this.googleOptions(), {center: mapCenter, mapTypeControlOptions: { mapTypeIds: [google.maps.MapTypeId.ROADMAP, 'map_style'] }});
// calculate logo zoom level, half way between start and max
this.options.midZoom = Math.ceil((this.options.google.maxZoom - this.options.google.startZoom)/2) + this.options.google.startZoom;
// create map
this.map = new google.maps.Map(this.mapEl, mapOptions);
if (this.options.styled) {
var styledMap = new google.maps.StyledMapType(this.options.styles, {name: "Partner"});
this.map.mapTypes.set('map_style', styledMap);
this.map.setMapTypeId('map_style');
}
if (this.options.originMarker) {
this.placeMarker(mapCenter);
}
if (this.options.infoWindow) {
this.infoWindow = new google.maps.InfoWindow();
}
},
/**
* Adds Markers to our map, creates the markers and binds them to a logo.
*
* @method addMarkers
* @param marks {Array}
*/
addMarkers: function(markers) {
if (markers.length > 0){
for (var i=0; i<markers.length; i++){
this.createMarker(markers[i], i);
}
this.bindLogos();
}
},
/*
* Binds logos to their marker's infoWindow
*
* @method bindLogos
* @requires ul#js-archives with li#js-archive-{{id}} children,
* @requires markers @param in addMarkers to have same index order as in markup.
*/
bindLogos: function() {
var archives = $('.js-archives').find('li');
_.each(archives, function(archive, index){
var $archive = $(archive),
id = _.last($archive.attr('id').split('-')),
_self = GoogleMap;
$archive.find('.js-archive-logo').css({cursor: 'pointer'}).on('click', function(e){
_self.onLogoClick(e, id);
});
});
},
/**
* Event Handler for when user clicks on Logo
*
* @method onLogoClick
* @param e, browser event {Event}
* @param id, so we can reference its marker and info window {Integer}
*/
onLogoClick: function(e, id){
e.preventDefault();
this.openInfoWindow(this.markers[id], this.tooltips[id]);
this.map.setCenter(this.markers[id].position);
if (window.console) {
console.log('clicked on id: ' + id + ', setZoom: ' + this.options.google.maxZoom);
}
this.map.setZoom(this.options.midZoom);
},
/**
* Creates {google.maps.Marker}, stored in marker
*
* @method createMarker
* @param partner {Object}
* @param index {Integer}
*/
createMarker: function(partner, index){
var opts = {index: index};
if (this.options.partnerTypes) {
opts = _.extend(opts, {partnerType: partner.type})
}
if (partner.logo == '' && partner.type == 2) {
partner.logo = this.options.parishLogo;
}
var loc = new google.maps.LatLng(partner.lat, partner.lng),
marker = this.placeMarker(loc, opts),
tip = this.infoWindowMarkup(partner);
//-- store for future reference
this.tooltips.push(tip);
//-- finally add infoWindow
this.addInfoWindow(marker);
},
/**
* Places Marker on Map
*
* @method placeMarker
* @param latLng {google.maps.latLng},
* @param options.partnerType {Integer} determines marker icon
* @return {google.maps.Marker}
*/
placeMarker: function(latLng, options) {
options = options || {};
var pinType = (options.partnerType == 2) ? 'parish' : 'standard';
if (options.reset) {
this.resetMarkers();
}
var marker = new google.maps.Marker({
map: this.map,
position: latLng,
icon: this.markerPin(pinType),
});
if (options.index !== null) {
marker.mid = options.index; // for matching with tool tips
this.markers[options.index] = marker;
} else {
this.markers.push(marker);
}
return marker;
},
/**
* Gets Customer Marker Type based on string.
*
* @method markerPin
* @param markerType {String}
* @return {google.maps.MarkerImage}
*/
markerPin: function(markerType){
markerType = markerType || 'standard';
if (this.customMarker) {
return new google.maps.MarkerImage(this.options.marker);
} else {
return new google.maps.MarkerImage(this.options.markers[markerType],
new google.maps.Size(30.0, 48.0),
new google.maps.Point(0, 0),
new google.maps.Point(15.0, 24.0)
);
}
},
/**
* Removes marker by id or all markers from map
*
* @method resetMarkers
* @param markerId, optional {Integer}
*/
resetMarkers: function(markerId){
markerId = markerId || null;
if (markerId) {
this.markers[markerId].setMap(null)
} else {
for (var i = 0; i < this.markers.length; i++) {
this.markers[i].setMap(null);
}
}
},
/**
* Add info window to marker
*
* @method addInfoWindow
* @param marker {google.maps.Marker}
*/
addInfoWindow: function(marker){
var _self = GoogleMap,
mid = marker.mid;
google.maps.event.addListener(marker, 'click', function() {
_self.openInfoWindow(_self.markers[mid], _self.tooltips[mid]);
});
},
/**
* Opens marker's infoWindow
*
* @method openInfoWindow
* @param marker {google.maps.Marker}
* @param tooltip {String}
*/
openInfoWindow: function(marker, tooltip) {
if (this.infoWindow) {
this.infoWindow.close();
}
this.infoWindow.setContent(tooltip);
this.infoWindow.open(this.map, marker);
},
/**
* Compile mustache-style underscore template
* Sets this.infoWindowMarkup
*
* @method compileTemplate
*/
compileTemplate: function(){
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
}
this.infoWindowMarkup = _.template(this.infoWindowMarkup);
},
/**
* Reposition map, e.g. after Geocoding
*
* @method moveTo
* @param location {google.maps.GeocoderRequest.location}
* @param zoom {Integer}
*/
moveTo: function(location, zoom){
zoom = zoom || this.options.google.zoom;
this.map.setZoom(zoom);
this.map.setCenter(location);
this.placeMarker(location);
}
};