Julie Ng

← Work

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.

Janus Partner by State Page
Partners Page

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

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);
  }
};
    
  

More Screenshots

Admin interface where users can manage a partner, including uploading a logo and placing the marker via geolocation.
Admin interface where users can manage a partner, including uploading a logo and placing the marker via geolocation.
Example content page, which is WYSIWYG editable.
Example content page, which is WYSIWYG editable.

← Case Studies