var bas_map = null;
var map_control = null;
var map_available = false;
var map_icons = [];
var marker_layers = {};
// Initialize the runways and sids with dummy values, so that not all
// runways and SIDs are loaded when no runway or SID data is available yet
var overlays = {runways: [''], sids: ['']};
var map_tracks = {
   do_show: true,
   tracks: {},
   circles: [],
   onselect: selectTrack
};

/**
 * Clear the visible tooltips. This is done after the zoom level of the map
 * changes, because the old tooltips apparently don't show anymore
 */
function clearTooltips()
{
   for (var name in marker_layers)
   {
      var layer = marker_layers[name].visible;
      for (var scale in layer)
      {
         for (var i = 0; i < layer[scale].length; i++)
         {
            if (layer[scale][i].tooltip)
            {
               bas_map.removeOverlay(layer[scale][i].tooltip);
               layer[scale][i].tooltip = null;
            }
         }
      }
   }
}

/**
 * Called when the Google map is created to create the markers
 */
function createIcons()
{
   map_icons = [];
   
   map_icons[0] = new GIcon();
   map_icons[0].image = "images/balnul.png";
   map_icons[0].iconAnchor = new GPoint(7,8);
   for (i = 1; i <= 7; i++)
   {
      map_icons[i] = new GIcon();
      map_icons[i].image = "images/bal" + (i-1) + ".png";
      map_icons[i].iconAnchor = new GPoint(7,8);
   }
   map_icons[-1] = new GIcon();
   map_icons[-1].image = "images/bal_k1.png";
   map_icons[-1].iconAnchor = new GPoint(7,8);
}

/**
 * Creates an overlay on the googlemap to give a clearer display
 * of the runways. This function does not check if Google maps is
 * available, and should probably only be called from createMap().
 */
function createSchiphol()
{
   var runways = {
      'Oostbaan': {
         id: 'oostbaan',
         lat1: 52.300372, lon1: 4.783483,
         lat2: 52.314025, lon2: 4.803025
      },
      'Kaagbaan': {
         id: 'kaagbaan',
         lat1: 52.287930, lon1: 4.734163,
         lat2: 52.304580, lon2: 4.777516
      },
      'Buitenveldertbaan': {
         id: 'buitenveldertbaan',
         lat1: 52.316644, lon1: 4.746347,
         lat2: 52.318374, lon2: 4.796891
      },
      'Zwanenburgbaan': {
         id: 'zwanenburgbaan',
         lat1: 52.331397, lon1: 4.740030,
         lat2: 52.301791, lon2: 4.737311
      },
      'Aalsmeerbaan': {
         id: 'aalsmeerbaan',
         lat1: 52.321325, lon1: 4.780169,
         lat2: 52.290824, lon2: 4.777347
      },
      'Polderbaan': {
         id: 'polderbaan',
         lat1: 52.360258, lon1: 4.711725,
         lat2: 52.328580, lon2: 4.708836
      }
   };

   for (var name in runways)
   {
      var runway = runways[name];

      var div = document.createElement('div');
      div.id = runway.id;
      div.className = 'schiphol';
      div.innerHTML = name;
      bas_map.getContainer().appendChild(div);

      var line;
      if (false) //(!$.browser.safari)
      {
         line = new BDCCPolyline([
            new GLatLng(runway.lat1, runway.lon1),
            new GLatLng(runway.lat2, runway.lon2)
         ], "#000000", 2, 1);
         GEvent.addListener(line, 'mouseout', new Function(
            'setTimeout("document.getElementById(\'" + runway.id + "\').style.display = \'none\'", 450);'
         ));
      }
      else
      {
         line = new GPolyline([
            new GLatLng(runway.lat1, runway.lon1),
            new GLatLng(runway.lat2, runway.lon2)
         ], "#000000", 2);
         GEvent.addListener(line, 'mouseout', new Function(
            "document.getElementById('" + runway.id + "').style.display = 'none';"
         ));
      }
      GEvent.addListener(line, "mouseover", new Function(
         "document.getElementById('" + runway.id + "').style.display = 'block';"
      ));

      bas_map.addOverlay(line);
   }
}

/**
 * Create the Googlemap
 */
function createMap(container)
{
   try
   {
      bas_map = new GMap2(document.getElementById(container),
         {mapTypes: [G_PHYSICAL_MAP]});
      // Schedule the runways to draw 1 second after the map reports that it
      // it has been loaded. Doing it directly after load results in 
      // unspecified error on IE6 when the page is reloaded.
      GEvent.addListener(bas_map, 'load', function() {
         setTimeout(createSchiphol, 1000);
      });
      // The documentation says that mouseover events for GPolyLines only work
      // from v 2.111 onwards, but adding a (empty) mouseover handler on
      // the map itself seems to make it work with v 2.103 as well.
      GEvent.addListener(bas_map, 'mouseover', function(){});
      // Clear tooltips when the zoomlevel changes
      GEvent.addListener(bas_map, 'zoomend', clearTooltips);

      bas_map.setCenter(new GLatLng(52.339052, 4.774677), 10);
      var topLeft = new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(3,3));
      map_control = new GSmallMapControl();
      bas_map.addControl(map_control, topLeft);
      createIcons();

      // Success!
      map_available = true;
   }
   catch (exc)
   {
      alert("Er is een fout opgetreden bij het maken van de kaart. Deze is daarom niet beschikbaar.");
      map_available = false;
   }
}

/**
 * Hide the hidden markers again. This is called on every update of the 
 * marker managers, since apparently it unhides them after a refresh.
 */
function rehideMarkers()
{
   for (var name in marker_layers)
   {
      var layer = marker_layers[name].hidden;
      for (var scale in layer)
      {
         for (var i = 0; i < layer[scale].length; i++)
         {
            layer[scale][i].hide();
         }
      }
   }
}

/**
 * Clean up the Google map. This function is called when the page is 
 * unloaded, and is mainly for the benefit of IE6.
 */
function cleanMap()
{
   map_available = false;
   marker_layers = null;

   if (bas_map)
   {
      bas_map.clearOverlays();
      if (map_control)
      {
         bas_map.removeControl(map_control);
         map_control = null;
      }
      bas_map.removeMapType(G_PHYSICAL_MAP);
      bas_map = null;
   }

   GUnload();
}

/**
 * Returns a (googleMaps) icon depending on scale
 * @param int scale The scale for which to get the icon. 
 */
function getIcon(scale)
{
   if (!scale)
   {
      scale = 0;
   }
   return map_icons[scale];
}

/**
 * Set the mouseover handler for a marker.
 * @param object marker   The marker for which to set the handler
 */
function setMarkerMouseOver(marker)
{
   GEvent.addListener(marker, 'mouseover', function() {
      if (!marker.tooltip)
      {
         marker.tooltip = new Tooltip(marker, marker.html, 3, marker.postcode);
         bas_map.addOverlay(marker.tooltip);
      }
      marker.tooltip.show();
   });
}

/**
 * Set the click handler for a marker.
 * @param object marker   The marker for which to set the handler
 * @param string postcode The postal code for the marker
 * @param string date     If given, the date for which the issue
 *    distribution is shown
 */
function setMarkerClick(marker)
{
   GEvent.addListener(marker, 'click', function() {
      if (marker.date)
      {
         showDateComplaint(marker.postcode, marker.date);
      }
      else
      {
         showComplaint(marker.postcode);
      }
      if (marker.tooltip)
      {
         marker.tooltip.hide();
      }
   });
}

/**
 * Create a marker at the given point and add a tooltip
 *    containing the given html
 *
 * @param points   object
 * @param html     string
 * @param scale    int
 * @param postcode postcode
 * @return marker  GoogleMaps marker, or null if the map is not active
 */
function gMapCreateMarker(point, html, scale, postcode, date)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   var markerOptions = { icon: getIcon(scale) };
   var marker = new GMarker(point, markerOptions);

   marker.tooltip = null;
   marker.postcode = postcode ? postcode : null;
   marker.html = html ? html : null;
   marker.date = date ? date : null;
   // Use a custom tooltip to show complaints
   setMarkerMouseOver(marker);
   if (scale != -1)
   {
      setMarkerClick(marker);
   }
   GEvent.addListener(marker, 'mouseout', function() {
      if (marker.tooltip)
      {
         marker.tooltip.hide();
      }
   });
   GEvent.addListener(marker, 'remove', function() {
      if (marker.tooltip)
      {
         bas_map.removeOverlay(marker.tooltip);
      }
   });

   return marker;
}

/**
 * Create or update a marker layer.
 * @param string name The name of the layer to be updated
 * @param array data  The markers to add
 * @param string issueDate
 */
function gMapCreateMarkerLayer(name, data, issueDate)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   var toadd = [];
   var i, scale;
   for (i in data)
   {
      var elem = data[i];
      
      var pos = new GLatLng(elem.lat, elem.lon);
      var label = elem.label ? elem.label : '';
      scale = elem.scale ? elem.scale : 0;
      var postcode = elem.postalcode ? elem.postalcode : '';

      /*
       * It seems damn near impossible to remove markers from the map in
       * IE6 without leaking DOM nodes. So instead we reuse existing markers,
       * which are hidden when no longer needed. Only when no existing
       * markers of the desired size are available are new markers created.
       * Markers are cached per scale factor since the GMarker class provides
       * no method to change the marker icon.
       */
      var marker;
      if (marker_layers[name].hidden[scale]
          && marker_layers[name].hidden[scale].length > 0)
      {
         marker = marker_layers[name].hidden[scale].pop();
         marker.setLatLng(pos);
         marker.postcode = postcode ? postcode : null;
         marker.html = label ? label : null;
         marker.date = issueDate ? issueDate : null;
         marker.show();
      }
      else
      {
         marker = gMapCreateMarker(pos, label, scale, postcode, issueDate);
         toadd.push(marker);
      }

      if (!marker_layers[name].visible[scale])
      {
         marker_layers[name].visible[scale] = [];
      }
      marker_layers[name].visible[scale].push(marker);
   }

   if (toadd.length > 0)
   {
      marker_layers[name].manager.addMarkers(toadd, 0);
      marker_layers[name].manager.refresh();

      // refresh unhides all current markers :(
      for (scale in marker_layers[name].hidden)
      {
         for (i = 0; i < marker_layers[name].hidden[scale].length; i++)
         {
            marker_layers[name].hidden[scale][i].hide();
         }
      }
   }
}

/**
 * Hide an overlay
 * @param string name The name of the overlay to hide
 */
function hideOverlay(name)
{
   if (!map_available)
   {
      return;
   }

   if (marker_layers[name])
   {
      var layer = marker_layers[name];
      for (var scale in layer.visible)
      {
         for (var i = 0; i < layer.visible[scale].length; i++)
         {
            var marker = layer.visible[scale][i];
            marker.hide();

            marker.postcode = null;
            marker.html = null;
            marker.date = null;
            if (marker.tooltip)
            {
               bas_map.removeOverlay(marker.tooltip);
               marker.tooltip = null;
            }
            marker.setLatLng(new GLatLng(0, 0));
         }

         if (!layer.hidden[scale])
         {
            layer.hidden[scale] = [];
         }
         layer.hidden[scale] = 
            layer.hidden[scale].concat(layer.visible[scale]);
         layer.visible[scale] = [];
      }
   }
}

/**
 * Show the given markers
 *
 * @param markers
 */
function gMapMarkersOverlay(markers, name, date)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   if (markers)
   {
      if (!marker_layers[name])
      {
         var manager = new MarkerManager(bas_map, { trackMarkers: true });
         // When a hide a marker I want it to stay hidden, not show itself
         // again when the marker manager is refreshed. Rehide any hidden
         // markers on a manager update.
         GEvent.addListener(manager, 'changed', rehideMarkers);
         marker_layers[name] = {
            visible: {},
            hidden: {},
            manager: manager
         };
      }

      if (date)
      {
         gMapCreateMarkerLayer(name, markers, date);
      }
      else
      {
         gMapCreateMarkerLayer(name, markers);
      }
   }
}

/**
 * Pans the map to the given address
 * @param array address Coordinates and optional data for the marker
 */
function gMapSearchAddress(address, createMarker, postalCode, keepIssues)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   if (address.lat && address.lon)
   {
      var point = new GLatLng(address.lat, address.lon);
      bas_map.panTo(point, 11);
   }

   if (createMarker)
   {
      var data = [address];
      data[0].scale = -1;
      data[0].label = postalCode;
      if (!keepIssues)
      {
         hideOverlay('issue');
      }
      gMapMarkersOverlay(data, 'issue');	
   }
}

/**
 * Add a known overlay or overlay group to the list.
 * @param id       The ID of the overlay
 * @param name     The name given to the overlay
 * @param children If this overlay corresponds to an overlay group, the
 *    ID, runway and SIDs of the actual overlays in the group.
 */
function setOverlay(id, name, children)
{
   if (!overlays[id])
   {
      overlays[id] = {};
   }

   overlays[id].name = name;
   overlays[id].children = children;
   overlays[id].is_group = false;
   overlays[id].visible = false;
   for (var cid in children)
   {
      var child = children[cid];
      if (!overlays[cid])
      {
         overlays[cid] = {};
      }
      overlays[cid].name = name;
      overlays[cid].runway = child.runway;
      overlays[cid].sid = child.sid;
      overlays[cid].visible = false;

      overlays[id].is_group = true;
   }
}

/**
 * Callback for loading an overlay. After Google maps indicates that an
 * overlay is loaded, this function checks if it was succesful.
 * @param id The ID of the overlay to check
 */
function checkOverlayLoaded(id)
{
   var overlay = overlays[id].overlay;
   if (!overlay.loadedCorrectly())
   {
      alert('Niet in staat om overlay ' + overlays[id].name + ' te tonen');
      delete overlays[id].overlay;
   }
}

/**
 * Request the KML/KMZ data for the overlay with ID id from the server, 
 * and add it to the map
 * @param int id The ID of the overlay to retrieve
 */
function loadOverlay(id)
{
   var url = window.location.href.replace(/\/[^\/]*$/,
      '/overlay.php?id=' + id);
   var overlay = new GGeoXml(url);
   GEvent.addListener(overlay, 'load', function() {
      checkOverlayLoaded(id);
   });
   overlays[id].overlay = overlay;
   bas_map.addOverlay(overlay);
   
}

/**
 * Show the children in an overlay group that match the current runway
 * and SID, and hide all others
 * @param int id The id of the overlay group
 */
function selectOverlayRunwaySid(id)
{
   var parent = overlays[id];
   if (!parent.is_group || !parent.visible)
   {
      return;
   }
   
   for (var cid in parent.children)
   {
      var child = overlays[cid];
      if ((overlays.runways && !arrayContains(overlays.runways, child.runway))
          || (overlays.sids && !arrayContains(overlays.sids, child.sid)))
      {
         if (child.overlay)
         {
            child.overlay.hide();
         }
      }
      else if (child.overlay)
      {
         child.overlay.show();
      }
      else
      {
         loadOverlay(cid);
      }
   }
}

/**
 * Toggle the visibility of an overlay. For groups, toggling the group 
 * on results in all children matching the current runway(s) and SID(s) 
 * being shown.
 * @param int  id      The ID of the overlay to show or hide
 * @param bool checked If true, show the overlay, otherwise hide it
 */
function toggleOverlay(id, checked)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   overlays[id].visible = checked;
   if (overlays[id].is_group)
   {
      if (checked)
      {
         selectOverlayRunwaySid(id);
      }
      else
      {
         for (var cid in overlays[id].children)
         {
            var child = overlays[cid];
            if (child.overlay)
            {
               child.overlay.hide();
            }
         }
      }
   }
   else if (!checked)
   {
      if (overlays[id].overlay)
      {
         overlays[id].overlay.hide();
      }
   }
   else
   {
      if (!overlays[id].overlay)
      {
         loadOverlay(id);
      }
      else
      {
         overlays[id].overlay.show();
      }
   }
}

/**
 * Set the current runway and SID, and update all overlay groups
 * @param array runways The runways to select on
 * @param array sids    The SIDs to select on
 */
function selectAllOverlaysRunwaySid(runways, sids)
{
   overlays.runways = runways ? runways : null;
   overlays.sids = sids ? sids : null;

   for (var id in overlays)
   {
      var parent = overlays[id];
      if (parent.is_group && parent.visible)
      {
         selectOverlayRunwaySid(id);
      }
   }
}

/**
 * Remove a track overlay from the map
 * @param int id The ID of the track to remove
 */
function removeTrack(id)
{
   if (map_tracks.tracks[id] && map_tracks.tracks[id].track)
   {
      var track = map_tracks.tracks[id].track;
      if (track.point)
      {
         bas_map.removeOverlay(track.point);
         delete track.point;
      }
      GEvent.clearInstanceListeners(track);
      bas_map.removeOverlay(track);
      delete map_tracks.tracks[id].track;
   }
}

/**
 * Draw a single track on the map.
 * @param int   id          The ID of the track to draw
 * @param array user_params User defined parameters overriding the 
 *    default style of the track being drawn
 */
function drawTrack(id, user_params)
{
   if (!map_available || !map_tracks.tracks[id]
       || !map_tracks.tracks[id].flight)
   {
      // don't bother
      return;
   }

   if (map_tracks.tracks[id].track)
   {
      removeTrack(id);
   }

   var flight = map_tracks.tracks[id].flight;

   var params = {
      color: flight.direction == 'LANDING' ? '#ff8888' : '#8888ff',
      weight: 3,
      opacity: 0.8,
      points: flight.track,
      levels: flight.levels,
      numLevels: flight.nrzoom,
      zoomFactor: flight.zoomfac
   };
   if (user_params)
   {
      for (var i in user_params)
      {
         params[i] = user_params[i];
      }
   }
   var track = GPolyline.fromEncoded(params);

/*
GEvent.addListener(track, 'mouseover', function() {
   console.log(id);
});
*/
   if (map_tracks.onselect)
   {
      GEvent.addListener(track, 'click', function() {
         map_tracks.onselect(id);
      });
   }

   if (params.point)
   {
      // draw a tiny line with a large width to approximate a dot on the map
      // The line length cannot be 0, however, Google maps refuses to draw it
      var l = 5e-4;
      var h = l*Math.tan(Math.PI/8);
      var point = new GPolygon([
         new GLatLng(params.point.lat-h, params.point.lon+l),
         new GLatLng(params.point.lat+h, params.point.lon+l),
         new GLatLng(params.point.lat+l, params.point.lon+h),
         new GLatLng(params.point.lat+l, params.point.lon-h),
         new GLatLng(params.point.lat+h, params.point.lon-l),
         new GLatLng(params.point.lat-h, params.point.lon-l),
         new GLatLng(params.point.lat-l, params.point.lon-h),
         new GLatLng(params.point.lat-l, params.point.lon+h),
         new GLatLng(params.point.lat-h, params.point.lon+l)
      ], params.color, 7, params.opacity, params.color, params.opacity);
      bas_map.addOverlay(point);
      track.point = point;
   }

   if (!map_tracks.do_show)
   {
      track.hide();
      if (track.point)
      {
         track.point.hide();
      }
   }

   bas_map.addOverlay(track);
   map_tracks.tracks[id].track = track;
}

/**
 * Highlight a track, drawing it in a more staurated color
 * @param int id The ID of the track
 */
function selectTrack(id)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   if (!map_tracks.tracks[id]           // track doesn't exist
       || !map_tracks.tracks[id].flight // flight for track got lost!?
       || map_tracks.selected == id)    // track is already selected
   {
      return;
   }

   // Redraw the previously selected track in default colors
   if (map_tracks.selected !== undefined)
   {
      drawTrack(map_tracks.selected);
   }

   var flight = map_tracks.tracks[id].flight;
   var color = flight.direction == 'LANDING' ? '#ff0000' : '#0000ff';
   var params = {color: color, opacity: 0.8, weight: 4};
   if (flight.lat !== undefined && flight.lon !== undefined)
   {
      params.point = {lat: flight.lat, lon: flight.lon};
   }

   drawTrack(id, params);
   map_tracks.selected = id;
}

/**
 * Draw a circle on the map. These can be used e.g. to indicate the
 * areas around a location in which flights can possibly cause noise
 * disturbance .
 * @param array center Lat/lon coordinates of the center of the circle
 * @param int   radius Radius of the circle in meters
 * @param int   steps  (optional) The number of circle segments to draw.
 */
function drawCircle(center, radius, steps)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   if (!steps)
   {
      steps=36;
   }
   var dang = 360 / steps;

   var cos_p0 = Math.cos(center.lat*Math.PI/180);
   var sin_p0 = Math.sin(center.lat*Math.PI/180);
   var cos_l0 = Math.cos(center.lon*Math.PI/180);
   var sin_l0 = Math.sin(center.lon*Math.PI/180);

   var r = radius / 1000; // Radius of circle in km
   var R = 6371;          // Mean earth radius in km
   var sin_pp = 1 - r*r/(2*R*R);
   var cos_pp = Math.sqrt(1-sin_pp*sin_pp);

   var points = [];
   for (var ipt = 0; ipt <= steps; ipt++)
   {
      var lp = ipt*dang*Math.PI/180;

      var xp = cos_pp * Math.cos(lp);
      var yp = cos_pp * Math.sin(lp);

      var xq = sin_p0 * xp + cos_p0 * sin_pp;
      var sin_phi = sin_p0*sin_pp - cos_p0*xp;
      var cos_phi = Math.sqrt(1-sin_phi*sin_phi);
      var phi = Math.asin(sin_phi);

      var xr = cos_l0 * xq - sin_l0 * yp;
      var yr = sin_l0 * xq + cos_l0 * yp;
      var sin_lambda = yr / cos_phi;
      var cos_lambda = xr / cos_phi;
      var lambda = Math.atan2(sin_lambda, cos_lambda);

      points.push(new GLatLng(phi*180/Math.PI, lambda*180/Math.PI));
   }

   var params = {
      color: '#44ff44',
      weight: 3,
      opacity: 1.0
   };
   var circle = new GPolyline(points, params.color, params.weight, 
      params.opacity);
   bas_map.addOverlay(circle);

   map_tracks.circles.push(circle);
}

/**
 * Draw the tracks of a set of flights, and optionally a set of circles,
 * on the map, removing existing tracks. 
 * @param array flights     The tracks to draw
 * @param array circles     Coordinates and radii of circles specifying areas
 *   of interest
 * @param function onselect Function to call when a flight is selected
 *    by clicking on a track
 */
function drawIssueTracks(flights, circles, onselect)
{
   if (!map_available)
   {
      // don't bother
      return;
   }

   var i;

   // remove old tracks
   for (var id in map_tracks.tracks)
   {
      removeTrack(id);
   }
   for (i = 0; i < map_tracks.circles.length; i++)
   {
      bas_map.removeOverlay(map_tracks.circles[i]);
   }
   map_tracks.tracks = {};
   map_tracks.circles = [];
   map_tracks.onselect = onselect ? onselect : selectTrack;
   delete map_tracks.selected;

   if (flights)
   {
      for (i = 0; i < flights.length; i++)
      {
         var flight = flights[i];
         var params = {};
         map_tracks.tracks[flight.id] = {flight: flight};
         drawTrack(flight.id, params);
      }
   }

   if (circles)
   {
      for (i = 0; i < circles.length; i++)
      {
         var circle = circles[i];
         drawCircle(circle.center, circle.radius);
      }
   }
}

/*!
 * Toggle the visibility of the flight tracks on the map.
 * \param boolean checked If true, the flights become visible. Otherwise,
 *    they are removed and no longer drawn.
 */
function toggleTracksOverlay(checked)
{
   var id, i;

   if (checked)
   {
      for (id in map_tracks.tracks)
      {
         map_tracks.tracks[id].track.show();
         if (map_tracks.tracks[id].track.point)
         {
            map_tracks.tracks[id].track.point.show();
         }
      }
      for (i = 0; i < map_tracks.circles.length; i++)
      {
         map_tracks.circles[i].show();
      }
      map_tracks.do_show = true;
   }
   else
   {
      for (id in map_tracks.tracks)
      {
         map_tracks.tracks[id].track.hide();
         if (map_tracks.tracks[id].track.point)
         {
            map_tracks.tracks[id].track.point.hide();
         }
      }
      for (i = 0; i < map_tracks.circles.length; i++)
      {
         map_tracks.circles[i].hide();
      }
      map_tracks.do_show = false;
   }
}

