Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add Atom feeds for issues either by operator or map area #603

Merged
merged 11 commits into from

2 participants

@mhl
Owner

Add Atom feeds for issues either by operator or map area

These commits add in-page links to Atom feeds for issues in two situations:

  • When you're viewing the Issues tab of a particular Operator's page
  • When you're browsing issues in a particular area, i.e. when a map of issues is being shown

In the latter case the feed link will be updated dynamically when dragging the map or zooming in and out.

This pull request also includes commits with various other minor fixes.

mhl added some commits
@mhl mhl Don't double-escape the title content in Atom feeds 8aaa281
@mhl mhl On dragging the issue map, update the URL with the new permalink
This relies on history.replaceState, which isn't supported on all
browsers.  However, if the history.replaceState function doesn't
exist, the behaviour will be as before.
9cca15d
@mhl mhl Reorder functions in map.js to avoid JSLint complaints 5de6f2c
@mhl mhl Fix many JSLint warnings
There are now only two JSLint errors if you use the following
options:

 {
   browser: true,
   'continue': true,
   'break': true,
   vars: true,
   white: true,
   plusplus: true,
   maxerr: 50,
   indent: 2
 }

The last two are (I think) spurious "strict violation" warnings
for:

  this.drawFeature(segment)

... in each case, I think because JSLint can't detect that
these functions are used as event handlers.
90a723a
@mhl mhl Fix a bug with map drawing if lat, lon and zoom are supplied in the URL 41319d1
@mhl mhl Include a " [FIXED]" suffix to titles of Atom entries where appropriate 62ceefe
@mhl mhl Add a partial to render an in-page link to an Atom feed a295966
@mhl mhl Add a per-operator Atom feed of issues, with an in-page link 588a7b3
@mhl mhl Add Atom feed (+ in-page link) for issues on the browse area page
This dynamically updates the link when scrolling the map
da81acd
@mhl mhl Use the name of the operator instead of "this operator" in the feed l…
…ink text
9011573
@mhl mhl Make it clearer that the link is for issues on and around the map f6ff288
@mysociety mysociety merged commit f6ff288 into mysociety:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 16, 2012
  1. @mhl
  2. @mhl

    On dragging the issue map, update the URL with the new permalink

    mhl authored
    This relies on history.replaceState, which isn't supported on all
    browsers.  However, if the history.replaceState function doesn't
    exist, the behaviour will be as before.
  3. @mhl
  4. @mhl

    Fix many JSLint warnings

    mhl authored
    There are now only two JSLint errors if you use the following
    options:
    
     {
       browser: true,
       'continue': true,
       'break': true,
       vars: true,
       white: true,
       plusplus: true,
       maxerr: 50,
       indent: 2
     }
    
    The last two are (I think) spurious "strict violation" warnings
    for:
    
      this.drawFeature(segment)
    
    ... in each case, I think because JSLint can't detect that
    these functions are used as event handlers.
  5. @mhl
  6. @mhl
  7. @mhl
  8. @mhl
  9. @mhl

    Add Atom feed (+ in-page link) for issues on the browse area page

    mhl authored
    This dynamically updates the link when scrolling the map
  10. @mhl
  11. @mhl
This page is out of date. Refresh to see the latest.
View
24 app/controllers/operators_controller.rb
@@ -4,7 +4,7 @@ class OperatorsController < ApplicationController
before_filter :long_cache
before_filter :find_operator, :except => [:index]
before_filter :setup_shared_title, :except => [:index]
-
+ before_filter :setup_issues_feed, :only => [:show, :issues, :routes]
def index
@operator_list_threshold = 20
@@ -45,7 +45,12 @@ def index
end
end
end
-
+
+ def setup_issues_feed
+ @issues_feed_params = params.clone
+ @issues_feed_params[:format] = 'atom'
+ end
+
def show
@title = @operator.name
@current_tab = :issues
@@ -74,9 +79,18 @@ def show
# reported to another organisation (e.g., a PTE) then that route won't show here.
def issues
- show
- @title = t('route_operators.issues.title', :operator => @operator.name)
- render :show
+ respond_to do |format|
+ format.html do
+ show
+ @title = t('route_operators.issues.title', :operator => @operator.name)
+ render :show
+ end
+ format.atom do
+ @title = t('route_operators.issues.feed_title', :operator => @operator.name)
+ @issues = Problem.find_recent_issues false, :single_operator => @operator
+ render :template => 'shared/issues.atom.builder', :layout => false
+ end
+ end
end
def routes
View
192 app/controllers/problems_controller.rb
@@ -7,7 +7,8 @@ class ProblemsController < ApplicationController
:find_train_route,
:find_ferry_route,
:find_other_route,
- :browse]
+ :browse,
+ :atom_link]
before_filter :find_visible_problem, :only => [:show, :update, :add_comment]
before_filter :require_problem_reporter, :only => [:convert]
skip_before_filter :require_beta_password, :only => [:frontpage]
@@ -376,6 +377,15 @@ def find_ferry_route
def choose_location
end
+ def atom_link
+ @issues_feed_title = atom_feed_title @lon, @lat
+ @issues_feed_params = params.clone
+ @issues_feed_params['action'] = 'browse'
+ @issues_feed_params['format'] = 'atom'
+ render :partial => 'shared/atom_link',
+ :locals => { :feed_link_text => t('problems.browse.feed_link_text') }
+ end
+
def browse
if params[:geolocate] == '1'
@geolocate_on_load = true
@@ -430,9 +440,16 @@ def find_nearest_stop(lon, lat, transport_mode_name)
end
end
+ def atom_feed_title(lon, lat)
+ t('problems.browse.feed_title',
+ :longitude => lon,
+ :latitude => lat)
+ end
+
def find_area(options)
@map_height = options[:map_height]
@map_width = options[:map_width]
+ to_render = nil
if is_valid_lon_lat?(params[:lon], params[:lat])
lat = params[:lat].to_f
lon = params[:lon].to_f
@@ -442,100 +459,107 @@ def find_area(options)
@geolocate_on_load = false
# don't show geolocate button
@geolocation_failed = true
- render options[:find_template]
- return
- end
- nearest_stop = find_nearest_stop(params[:lon], params[:lat], nil)
- if nearest_stop
- map_params_from_location([nearest_stop],
- find_other_locations=true,
- @map_height,
- @map_width,
- options[:map_options])
- @locations = [nearest_stop]
- render options[:browse_template]
- return
- else # no nearest stop suggests empty database
- location_search.fail
- @error_message = t('problems.find_stop.please_enter_an_area')
- end
- elsif params[:name]
- if params[:name].blank?
- @error_message = t('problems.find_stop.please_enter_an_area')
- render options[:find_template]
- return
- end
- location_search = LocationSearch.new_search!(session_id, :name => params[:name],
- :location_type => 'Stop/station')
- stop_info = Gazetteer.place_from_name(params[:name], params[:stop_name], options[:map_options][:mode])
- # got back localities
- if stop_info[:localities]
- if stop_info[:localities].size > 1
- @localities = stop_info[:localities]
- @matched_stops_or_stations = stop_info[:matched_stops_or_stations]
- @name = params[:name]
- render :choose_locality
- return
- else
- return render_browse_template(stop_info[:localities], options[:map_options], options[:browse_template])
- end
- # got back district
- elsif stop_info[:district]
- return render_browse_template([stop_info[:district]], options[:map_options], options[:browse_template])
- # got back admin area
- elsif stop_info[:admin_area]
- return render_browse_template([stop_info[:admin_area]], options[:map_options], options[:browse_template])
- # got back stops/stations
- elsif stop_info[:locations]
- if options[:map_options][:mode] == :browse
- return render_browse_template(stop_info[:locations], options[:map_options], options[:browse_template])
- else
- map_params_from_location(stop_info[:locations],
+ to_render = options[:find_template]
+ else
+ nearest_stop = find_nearest_stop(params[:lon], params[:lat], nil)
+ if nearest_stop
+ map_params_from_location([nearest_stop],
find_other_locations=true,
@map_height,
@map_width,
options[:map_options])
- @locations = stop_info[:locations]
- render options[:browse_template]
- return
- end
- # got back postcode info
- elsif stop_info[:postcode_info]
- postcode_info = stop_info[:postcode_info]
- if postcode_info[:error]
+ @locations = [nearest_stop]
+ to_render = options[:browse_template]
+ else # no nearest stop suggests empty database
location_search.fail
- if postcode_info[:error] == :area_not_known
- @error_message = t('problems.find_stop.postcode_area_not_known')
- elsif postcode_info[:error] == :service_unavailable
- @error_message = t('problems.find_stop.postcode_service_unavailable')
+ @error_message = t('problems.find_stop.please_enter_an_area')
+ end
+ end
+ elsif params[:name]
+ if params[:name].blank?
+ @error_message = t('problems.find_stop.please_enter_an_area')
+ to_render = options[:find_template]
+ else
+ location_search = LocationSearch.new_search!(session_id, :name => params[:name],
+ :location_type => 'Stop/station')
+ stop_info = Gazetteer.place_from_name(params[:name], params[:stop_name], options[:map_options][:mode])
+ # got back localities
+ if stop_info[:localities]
+ if stop_info[:localities].size > 1
+ @localities = stop_info[:localities]
+ @matched_stops_or_stations = stop_info[:matched_stops_or_stations]
+ @name = params[:name]
+ to_render = :choose_locality
else
- @error_message = t('problems.find_stop.postcode_not_found')
+ return render_browse_template(stop_info[:localities], options[:map_options], options[:browse_template])
+ end
+ # got back district
+ elsif stop_info[:district]
+ return render_browse_template([stop_info[:district]], options[:map_options], options[:browse_template])
+ # got back admin area
+ elsif stop_info[:admin_area]
+ return render_browse_template([stop_info[:admin_area]], options[:map_options], options[:browse_template])
+ # got back stops/stations
+ elsif stop_info[:locations]
+ if options[:map_options][:mode] == :browse
+ return render_browse_template(stop_info[:locations], options[:map_options], options[:browse_template])
+ else
+ map_params_from_location(stop_info[:locations],
+ find_other_locations=true,
+ @map_height,
+ @map_width,
+ options[:map_options])
+ @locations = stop_info[:locations]
+ to_render = options[:browse_template]
+ end
+ # got back postcode info
+ elsif stop_info[:postcode_info]
+ postcode_info = stop_info[:postcode_info]
+ if postcode_info[:error]
+ location_search.fail
+ if postcode_info[:error] == :area_not_known
+ @error_message = t('problems.find_stop.postcode_area_not_known')
+ elsif postcode_info[:error] == :service_unavailable
+ @error_message = t('problems.find_stop.postcode_service_unavailable')
+ else
+ @error_message = t('problems.find_stop.postcode_not_found')
+ end
+ to_render = options[:find_template]
+ else
+ @lat = postcode_info[:lat] unless @lat
+ @lon = postcode_info[:lon] unless @lon
+ @zoom = postcode_info[:zoom] unless @zoom
+ map_data = Map.other_locations(@lat, @lon, @zoom, @map_height, @map_width, @highlight)
+ @other_locations = map_data[:locations]
+ @issues_on_map = map_data[:issues]
+ @nearest_issues = map_data[:nearest_issues]
+ @distance = map_data[:distance]
+ @locations = []
+ @find_other_locations = true
+ to_render = options[:browse_template]
end
- render options[:find_template]
- return
else
- @lat = postcode_info[:lat] unless @lat
- @lon = postcode_info[:lon] unless @lon
- @zoom = postcode_info[:zoom] unless @zoom
- map_data = Map.other_locations(@lat, @lon, @zoom, @map_height, @map_width, @highlight)
- @other_locations = map_data[:locations]
- @issues_on_map = map_data[:issues]
- @nearest_issues = map_data[:nearest_issues]
- @distance = map_data[:distance]
- @locations = []
- @find_other_locations = true
- render options[:browse_template]
- return
+ # didn't find anything
+ location_search.fail
+ @error_message = t('problems.find_stop.area_not_found')
+ to_render = options[:find_template]
end
- else
- # didn't find anything
- location_search.fail
- @error_message = t('problems.find_stop.area_not_found')
- render options[:find_template]
- return
end
end
-
+ respond_to do |format|
+ format.html do
+ @issues_feed_params = params.clone
+ @issues_feed_params[:format] = 'atom'
+ render to_render if to_render
+ end
+ format.atom do
+ @title = atom_feed_title @lon, @lat
+ @issues = []
+ @issues.concat(@issues_on_map) if @issues_on_map
+ @issues.concat(@nearest_issues) if @nearest_issues
+ render :template => 'shared/issues.atom.builder', :layout => false
+ end
+ end
end
def is_valid_lon_lat?(lon, lat)
View
5 app/models/campaign.rb
@@ -263,4 +263,9 @@ def self.needing_questionnaire(weeks_ago, user=nil)
self.visible.find(:all, :conditions => query + params,
:include => :problem)
end
+
+ def feed_title_suffix
+ :fixed == status ? " [FIXED]" : ""
+ end
+
end
View
6 app/models/problem.rb
@@ -494,4 +494,8 @@ def self.needing_questionnaire(weeks_ago, user=nil)
self.visible.sent.find(:all, :conditions => query + params)
end
-end
+ def feed_title_suffix
+ :fixed == status ? " [FIXED]" : ""
+ end
+
+end
View
1  app/views/operators/show.erb
@@ -57,6 +57,7 @@
<%= will_paginate @stations %>
<%- elsif @current_tab == :issues %>
<%- if @issue_count != 0 %>
+ <%= render :partial => 'shared/atom_link', :locals => { :feed_link_text => t('route_operators.show.feed_link_text', :operator => @operator.name) } %>
<%= will_paginate @issues, :params => { :action => :issues } %>
<ul class="issues-list widecol">
<%= render :partial => "shared/issue", :collection => @issues, :locals => {:context => :issue_list } %>
View
1  app/views/problems/browse_area.erb
@@ -7,6 +7,7 @@
<div id="main-content" class="browse-map-container container">
<div class="leftcol mediumnarrowcol">
+ <%= render :partial => 'shared/atom_link', :locals => { :feed_link_text => t('problems.browse.feed_link_text') } %>
<div id="issues-in-area">
<%= render :partial => 'shared/issues_in_area'%>
</div>
View
5 app/views/shared/_atom_link.erb
@@ -0,0 +1,5 @@
+<%- if @issues_feed_params %>
+ <div class="atom-link" id="in-page-atom-link">
+ <%= link_to image_tag("feed.png", :class => 'inline-icon') + " " + feed_link_text, @issues_feed_params %>
+ </div>
+<%- end %>
View
4 app/views/shared/issues.atom.builder
@@ -4,7 +4,7 @@ atom_feed do |feed|
@issues.each do |issue|
if issue.is_a?(Campaign)
feed.entry(issue) do |entry|
- entry.title(h(issue.title))
+ entry.title issue.title + issue.feed_title_suffix
entry.content(strip_tags(issue.description))
entry.author do |author|
author.name(issue.initiator.name)
@@ -12,7 +12,7 @@ atom_feed do |feed|
end
elsif issue.is_a?(Problem)
feed.entry(issue) do |entry|
- entry.title(h(issue.subject))
+ entry.title issue.subject + issue.feed_title_suffix
entry.content(strip_tags(issue.description))
entry.author do |author|
author.name(issue.reporter.name)
View
2  config/locales/views/operators/en.yml
@@ -16,6 +16,7 @@ en:
routes_header: "Routes"
stations_header: "Stations"
issues_header: "Issues"
+ feed_link_text: "Get updates on issues for %{operator}"
is_an_operator: "{%operator} is a transport operator."
is_an_operator_with_transport_mode: "%{operator} is a transport operator providing %{transport_mode} services."
operates_routes:
@@ -32,6 +33,7 @@ en:
other: "%{count} issues have been reported to %{operator}."
issues:
title: "%{operator} issues"
+ feed_title: "Issues for %{operator} on FixMyTransport"
routes:
title: "%{operator} routes"
stations:
View
2  config/locales/views/problems/en.yml
@@ -8,6 +8,8 @@ en:
title: "Issues in '%{area}'"
no_other_issues: No issues have been reported yet.
title_no_name: "Browsing issues"
+ feed_title: "Issues around longitude %{longitude} and latitude %{latitude} on FixMyTransport"
+ feed_link_text: "Get updates on issues on and around this map"
choose_area:
choose_area: "Choose area"
multiple_localities: "We found more than one place matching \"%{name}\". Please select one, or try a different search if yours is not here:"
View
7 config/routes.rb
@@ -72,6 +72,13 @@
map.issues '/issues', :action => 'issues_index', :controller => 'problems'
map.browse_issues '/issues/browse', :action => 'browse', :controller => 'problems'
+ map.browse_issues_atom_link '/issues/browse/atom_link/:zoom/:lat/:lon', :action => 'atom_link',
+ :controller => 'problems',
+ :conditions => { :method => :get },
+ :requirements => { :zoom => /\d\d?/,
+ :lon => /[-+]?[0-9]*\.?[0-9]+/,
+ :lat => /[-+]?[0-9]*\.?[0-9]+/ }
+
# stops
map.add_comment_stop "/stops/:scope/:id/add_comment", :controller => "locations",
:action => 'add_comment_to_stop',
View
623 public/javascripts/map.js
@@ -1,298 +1,385 @@
-// Mapping functions
-
-var map;
-var markers;
-var otherMarkers;
-var stopsById = new Array();
-var proj = new OpenLayers.Projection("EPSG:4326");
-var selectControl;
-var openPopup;
-var openHover;
-var segmentStyle =
-{
- strokeColor: "#CC0000",
- strokeOpacity: 0.2,
- strokeWidth: 4
-};
-
-var segmentSelectedStyle =
-{
- strokeColor: "#000000",
- strokeOpacity: 0.5,
- strokeWidth: 4
-};
-
-// Handle a click on a marker - go to its URL
-function markerClick(evt) {
- if (evt.feature.attributes.url) {
- window.location = evt.feature.attributes.url;
+/* -*- mode: espresso; espresso-indent-level: 2; indent-tabs-mode: nil -*- */
+/* vim: set softtabstop=2 shiftwidth=2 tabstop=2 expandtab: */
+
+/* Directives for JSLint ---------------------------------------------- */
+
+/*global OpenLayers, $ */
+/*global lon, lat, zoom, areaStops, otherAreaStops, findOtherLocations */
+/*global minZoomForOtherMarkers, highlight, linkType */
+/*global mapWidth, mapHeight, minZoom, maxZoom */
+
+/*jslint browser: true, vars: true, white: true, plusplus: true */
+/*jslint continue: true, maxerr: 50, indent: 2 */
+
+/* End of directives for JSLint --------------------------------------- */
+
+var area_init, route_init;
+
+(function () {
+
+ "use strict";
+
+ // Mapping functions
+
+ var map;
+ var markers;
+ var otherMarkers;
+ var stopsById = [];
+ var proj = new OpenLayers.Projection("EPSG:4326");
+ var selectControl;
+ var openPopup;
+ var openHover;
+ var segmentStyle = {
+ strokeColor: "#CC0000",
+ strokeOpacity: 0.2,
+ strokeWidth: 4
+ };
+
+ var segmentSelectedStyle = {
+ strokeColor: "#000000",
+ strokeOpacity: 0.5,
+ strokeWidth: 4
+ };
+
+ var bounds;
+ var highlightedMarkers;
+ var centerCoords;
+
+ // Handle a click on a marker - go to its URL
+ function markerClick(evt) {
+ if (evt.feature.attributes.url) {
+ window.location = evt.feature.attributes.url;
+ }
+ OpenLayers.Event.stop(evt);
}
- OpenLayers.Event.stop(evt);
-}
-function area_init() {
- // Vector layers must be added onload for IE
- if ($.browser.msie) {
- $(window).load(createAreaMap);
- } else {
- createAreaMap();
+ function jsPath(filename) {
+ var i, r = new RegExp("(^|(.*?\\/))("+filename+"\\.js)(\\?|$)"),
+ s = document.getElementsByTagName('script'),
+ src, m, result = "", len;
+ for(i=0, len=s.length; i<len; i++) {
+ src = s[i].getAttribute('src');
+ if(src) {
+ m = src.match(r);
+ if(m) {
+ result = m[1];
+ break;
+ }
+ }
+ }
+ return result;
}
-}
-
-function createAreaMap(){
- createMap('map');
- bounds = new OpenLayers.Bounds();
-
- // layers for markers that are the current focus (markers), other markers in the area
- // (otherMarkers) and other markers that are significant (highlightedMarkers)
- // - e.g. locations with problem reports, rather than without when browsing
- // an area.
- markers = new OpenLayers.Layer.Vector( "Markers" );
- otherMarkers = new OpenLayers.Layer.Vector( "Other Markers" );
- highlightedMarkers = new OpenLayers.Layer.Vector( "Highlighted Markers" );
- map.addLayer(otherMarkers);
- map.addLayer(highlightedMarkers);
- map.addLayer(markers);
-
- // All markers should handle to a click event
- markers.events.register( 'featureselected', markers, markerClick );
- otherMarkers.events.register( 'featureselected', otherMarkers, markerClick );
- highlightedMarkers.events.register( 'featureselected', highlightedMarkers, markerClick );
- var select = new OpenLayers.Control.SelectFeature( [markers, otherMarkers, highlightedMarkers] );
- map.addControl( select );
- select.activate();
-
- // Load the main markers, and the background markers (in either the highlighted or other layer)
- addMarkerList(areaStops, markers, false, null);
- addMarkerList(otherAreaStops, otherMarkers, true, highlightedMarkers);
-
- centerCoords = new OpenLayers.LonLat(lon, lat);
- centerCoords.transform(proj, map.getProjectionObject());
- map.setCenter(centerCoords, zoom);
-
- if (findOtherLocations == true) {
- map.events.register('moveend', map, updateLocations);
+
+ function createMap(map_element) {
+ // handle both cached and uncached js files in calculating image path
+ var javascriptPath = jsPath('OpenLayers');
+ if (javascriptPath === ''){
+ javascriptPath = jsPath('libraries');
+ }
+ if (javascriptPath === ''){
+ javascriptPath = jsPath('admin_libraries');
+ }
+ OpenLayers.ImgPath = javascriptPath + 'img/';
+ var options = {
+ 'projection': new OpenLayers.Projection("EPSG:900913"),
+ 'units': "m",
+ 'numZoomLevels': 18,
+ 'maxResolution': 156543.0339,
+ 'theme': null,
+ 'maxExtent': new OpenLayers.Bounds(-20037508.34, -20037508.34,
+ 20037508.34, 20037508.34),
+ 'controls': [
+ new OpenLayers.Control.Navigation(),
+ new OpenLayers.Control.PanZoom(),
+ new OpenLayers.Control.Attribution()
+ ]
+ };
+ $('.static-map-element').hide();
+ map = new OpenLayers.Map(map_element, options);
+ var layer = new OpenLayers.Layer.Google("Google Streets",{'sphericalMercator': true,
+ 'maxExtent': new OpenLayers.Bounds(-20037508.34, -20037508.34,
+ 20037508.34, 20037508.34)});
+ map.addLayer(layer);
}
- // if we're constrained to less than the expected map dimensions, try
- // to make sure the markers are all shown
- if (($('#map').width() < mapWidth || $('#map').height() < mapHeight) && (areaStops.length > 0)) {
- map.zoomToExtent(bounds, false);
+ function pointCoords(lon, lat) {
+ return new OpenLayers.Geometry.Point(lon, lat).transform(proj, map.getProjectionObject());
}
-
- // Enforce some zoom constraints
- map.events.register("zoomend", map, function() {
- // World zoom resets map
- if (map.getZoom() == 0) {
- map.setCenter(centerCoords, zoom);
- return;
+
+ function addMarker(current, bounds, layer, other, highlightedLayer){
+ var stopCoords = pointCoords(current.lon, current.lat);
+
+ if (stopsById[current.id] === undefined) {
+ bounds.extend(stopCoords);
+ var marker = new OpenLayers.Feature.Vector(stopCoords, {
+ url: current.url,
+ name: current.description,
+ id: current.id,
+ highlight: current.highlight
+ },
+ {externalGraphic: current.icon + ".png",
+ graphicTitle: current.description,
+ graphicWidth: current.width,
+ graphicHeight: current.height,
+ graphicOpacity: 1,
+ graphicXOffset: -( current.width/2),
+ graphicYOffset: -current.height,
+ cursor: 'pointer'
+ });
+
+ stopsById[current.id] = marker;
+ if (current.highlight === true && other === true) {
+ highlightedLayer.addFeatures( marker );
+ }else{
+ layer.addFeatures( marker );
+ }
+ }
+
+ }
+
+ function addMarkerList(list, layer, others, highlightedLayer) {
+ var i, j, item;
+ for (i=0; i < list.length; i++){
+ item = list[i];
+ // an element in the list may be an individual marker or an array
+ // of markers representing a route
+ if (item instanceof Array){
+ for (j=0; j < item.length; j++){
+ addMarker(item[j], bounds, layer, others, highlightedLayer);
+ }
+ }else{
+ addMarker(item, bounds, layer, others, highlightedLayer);
}
- if (map.getZoom() < minZoom) map.zoomTo(minZoom);
- if (map.getZoom() > maxZoom) map.zoomTo(maxZoom);
- });
-
-
-}
-
-function updateLocations(event) {
- var currentZoom = map.getZoom();
- if (currentZoom >= minZoomForOtherMarkers){
- if ($('#map-zoom-notice').length > 0) {
- $('#map-zoom-notice').fadeOut(500);
}
- // Show other, non-highlighted markers
- otherMarkers.setVisibility(true)
- }else{
- if ($('#map-zoom-notice').length > 0) {
- $('#map-zoom-notice').fadeIn(500);
+ }
+
+ function loadNewMarkers(markerData) {
+ var newMarkers, newContent;
+ newMarkers = markerData.locations;
+ // load new background markers
+ addMarkerList(newMarkers, otherMarkers, true, highlightedMarkers);
+ // update any associated list of issues
+ newContent = markerData.issue_content;
+ if ($('#issues-in-area').length > 0){
+ $('#issues-in-area').html(newContent);
}
- // Hide other, non-highlighted markers
- otherMarkers.setVisibility(false)
}
- // Request and load markers by ajax
- if (currentZoom >= minZoomForOtherMarkers || highlight == 'has_content'){
- center = map.getCenter();
- center = center.transform(map.getProjectionObject(), proj);
- url = "/locations/" + map.getZoom() + "/" + Math.round(center.lat*1000)/1000 + "/" + Math.round(center.lon*1000)/1000 + "/" + linkType;
- params = "?height=" + $('#map').height() + "&width=" + $('#map').width();
- params = params + "&highlight=" + highlight;
- $.ajax({
- url: url + params,
- dataType: 'json',
- success: loadNewMarkers,
- failure: markerFail})
+
+ function markerFail(){
+ // do nothing
}
-}
+ function replaceAtomLink(data, textStatus, jqXHR) {
+ $('#in-page-atom-link').replaceWith(data);
+ }
-function loadNewMarkers(markerData) {
- newMarkers = markerData['locations'];
- // load new background markers
- addMarkerList(newMarkers, otherMarkers, true, highlightedMarkers);
- // update any associated list of issues
- newContent = markerData['issue_content'];
- if ($('#issues-in-area').length > 0){
- $('#issues-in-area').html(newContent);
+ function replaceAtomLinkFailure(jqXHR, textStatus, errorThrown) {
+ // ignore errors here; there's nothing much that can be done
}
-}
-
-function markerFail(){
-// do nothing
-}
-
-function addMarkerList(list, layer, others, highlightedLayer) {
- for (var i=0; i < list.length; i++){
- var item = list[i];
- // an element in the list may be an individual marker or an array
- // of markers representing a route
- if (item instanceof Array){
- for (var j=0; j < item.length; j++){
- addMarker(item[j], bounds, layer, others, highlightedLayer);
+
+ function getQueryStringParametersMap() {
+ // Based on: http://stackoverflow.com/a/3855394/223092
+ var result = {}, i, value, parts;
+ var keyValuePairs = window.location.search.substr(1).split('&');
+ if (!keyValuePairs) {
+ return result;
+ }
+ for (i = 0; i < keyValuePairs.length; ++i) {
+ parts = keyValuePairs[i].split('=');
+ // Skip over any malformed parts with multiple = signs:
+ if (parts.length !== 2) {
+ continue;
}
- }else{
- addMarker(item, bounds, layer, others, highlightedLayer);
+ value = decodeURIComponent(parts[1].replace(/\+/g, " "));
+ result[parts[0]] = value;
}
+ return result;
}
-}
-
-
-function addMarker(current, bounds, layer, other, highlightedLayer){
- stopCoords = pointCoords(current.lon, current.lat);
-
- if (stopsById[current.id] == undefined) {
- bounds.extend(stopCoords);
- var marker = new OpenLayers.Feature.Vector(stopCoords, {
- url: current.url,
- name: current.description,
- id: current.id,
- highlight: current.highlight
- },
- {externalGraphic: current.icon + ".png",
- graphicTitle: current.description,
- graphicWidth: current.width,
- graphicHeight: current.height,
- graphicOpacity: 1,
- graphicXOffset: -( current.width/2),
- graphicYOffset: -current.height,
- cursor: 'pointer'
- });
-
- stopsById[current.id] = marker;
- if (current.highlight == true && other == true) {
- highlightedLayer.addFeatures( marker );
+
+ function updateLocations(eevent) {
+ var currentZoom = map.getZoom(), newLat, newLon, newPath, url;
+ var parameters, key, center, mapViewParameters;
+ var positionKeys = {'lon': true,
+ 'lat': true,
+ 'zoom': true};
+ if (currentZoom >= minZoomForOtherMarkers){
+ if ($('#map-zoom-notice').length > 0) {
+ $('#map-zoom-notice').fadeOut(500);
+ }
+ // Show other, non-highlighted markers
+ otherMarkers.setVisibility(true);
}else{
- layer.addFeatures( marker );
+ if ($('#map-zoom-notice').length > 0) {
+ $('#map-zoom-notice').fadeIn(500);
+ }
+ // Hide other, non-highlighted markers
+ otherMarkers.setVisibility(false);
}
- }
-
-}
-
-function pointCoords(lon, lat) {
- return new OpenLayers.Geometry.Point(lon, lat).transform(proj, map.getProjectionObject());
-}
-
-function route_init(map_element, routeSegments) {
-
- createMap(map_element);
- bounds = new OpenLayers.Bounds();
-
- var vectorLayer = new OpenLayers.Layer.Vector("Vector Layer",{projection: proj});
- map.addLayer(vectorLayer);
-
- addSelectedHandler(vectorLayer);
- for (var i=0; i < routeSegments.length; i++){
- var coords = routeSegments[i];
- var fromCoords = pointCoords(coords[0].lon, coords[0].lat);
- var toCoords = pointCoords(coords[1].lon, coords[1].lat);
- var points = [];
- points.push(fromCoords);
- points.push(toCoords)
- bounds.extend(fromCoords);
- bounds.extend(toCoords);
- lineString = new OpenLayers.Geometry.LineString(points);
- lineFeature = new OpenLayers.Feature.Vector(lineString, {projection: proj}, segmentStyle);
- lineFeature.segment_id = coords[2];
- vectorLayer.addFeatures([lineFeature]);
- }
- map.zoomToExtent(bounds, false);
-
-}
-
-function addSelectedHandler(vectorLayer) {
- vectorLayer.events.on({
- 'featureselected': segmentSelected,
- 'featureunselected': segmentUnselected
- });
- selectControl = new OpenLayers.Control.SelectFeature(vectorLayer, {multiple: false,
- toggleKey: "ctrlKey",
- multipleKey: "shiftKey"});
- map.addControl(selectControl);
- selectControl.activate();
-}
-
-function segmentSelected(event) {
- segment = event.feature;
- segment.style = segmentSelectedStyle;
- this.drawFeature(segment);
- var row = $("#route_segment_" + segment.segment_id);
- row.toggleClass("selected");
- row.find(".check-route-segment").attr('checked', 'true');
-}
-
-function segmentUnselected(event) {
- segment = event.feature;
- segment.style = segmentStyle;
- this.drawFeature(segment);
- $("#route_segment_" + segment.segment_id).toggleClass("selected");
-
-}
-
-function jsPath(filename) {
- var r = new RegExp("(^|(.*?\\/))("+filename+"\.js)(\\?|$)"),
- s = document.getElementsByTagName('script'),
- src, m, l = "";
- for(var i=0, len=s.length; i<len; i++) {
- src = s[i].getAttribute('src');
- if(src) {
- var m = src.match(r);
- if(m) {
- l = m[1];
- break;
+ // Request and load markers by ajax
+ if (currentZoom >= minZoomForOtherMarkers || highlight === 'has_content'){
+ center = map.getCenter();
+ center = center.transform(map.getProjectionObject(), proj);
+ newLat = Math.round(center.lat*1000)/1000;
+ newLon = Math.round(center.lon*1000)/1000;
+ mapViewParameters = map.getZoom() + "/" + newLat + "/" + newLon;
+ url = "/locations/" + mapViewParameters + "/" + linkType;
+ parameters = "?height=" + $('#map').height() + "&width=" + $('#map').width();
+ parameters = parameters + "&highlight=" + highlight;
+ $.ajax({
+ url: url + parameters,
+ dataType: 'json',
+ success: loadNewMarkers,
+ failure: markerFail});
+ $.ajax({
+ url: "/issues/browse/atom_link/" + mapViewParameters,
+ dataType: 'html',
+ success: replaceAtomLink,
+ failure: replaceAtomLinkFailure});
+ // If we're able to replace the URL with history.replaceState,
+ // update it to give a permalink to the new map position:
+ if (history.replaceState) {
+ newPath = window.location.pathname + '?';
+ parameters = getQueryStringParametersMap();
+ for (key in parameters) {
+ if (parameters.hasOwnProperty(key)) {
+ if (!positionKeys[key]) {
+ newPath += key + '=' + encodeURIComponent(parameters[key]) + '&';
}
+ }
}
+ newPath += 'lon='+newLon+'&lat='+newLat+'&zoom='+currentZoom;
+ history.replaceState(null, "New Map Position", newPath);
+ }
}
- return l;
-}
-
-function createMap(map_element) {
- // handle both cached and uncached js files in calculating image path
- var javascriptPath = jsPath('OpenLayers');
- if (javascriptPath == ''){
- javascriptPath = jsPath('libraries')
+
}
- if (javascriptPath == ''){
- javascriptPath = jsPath('admin_libraries')
+
+ function createAreaMap(){
+ var centerCoords, select;
+ createMap('map');
+ bounds = new OpenLayers.Bounds();
+
+ // layers for markers that are the current focus (markers), other markers in the area
+ // (otherMarkers) and other markers that are significant (highlightedMarkers)
+ // - e.g. locations with problem reports, rather than without when browsing
+ // an area.
+ markers = new OpenLayers.Layer.Vector( "Markers" );
+ otherMarkers = new OpenLayers.Layer.Vector( "Other Markers" );
+ highlightedMarkers = new OpenLayers.Layer.Vector( "Highlighted Markers" );
+ map.addLayer(otherMarkers);
+ map.addLayer(highlightedMarkers);
+ map.addLayer(markers);
+
+ // All markers should handle to a click event
+ markers.events.register( 'featureselected', markers, markerClick );
+ otherMarkers.events.register( 'featureselected', otherMarkers, markerClick );
+ highlightedMarkers.events.register( 'featureselected', highlightedMarkers, markerClick );
+ select = new OpenLayers.Control.SelectFeature( [markers, otherMarkers, highlightedMarkers] );
+ map.addControl( select );
+ select.activate();
+
+ // Load the main markers, and the background markers (in either the highlighted or other layer)
+ addMarkerList(areaStops, markers, false, null);
+ addMarkerList(otherAreaStops, otherMarkers, true, highlightedMarkers);
+
+ centerCoords = new OpenLayers.LonLat(lon, lat);
+ centerCoords.transform(proj, map.getProjectionObject());
+ map.setCenter(centerCoords, zoom);
+
+ if (findOtherLocations === true) {
+ map.events.register('moveend', map, updateLocations);
+ }
+
+ // if we're constrained to less than the expected map dimensions, try
+ // to make sure the markers are all shown
+ if (($('#map').width() < mapWidth || $('#map').height() < mapHeight) && (areaStops.length > 0)) {
+ map.zoomToExtent(bounds, false);
+ }
+
+ // Enforce some zoom constraints
+ map.events.register("zoomend", map, function() {
+ // World zoom resets map
+ if (map.getZoom() === 0) {
+ map.setCenter(centerCoords, zoom);
+ return;
+ }
+ if (map.getZoom() < minZoom) {
+ map.zoomTo(minZoom);
+ }
+ if (map.getZoom() > maxZoom) {
+ map.zoomTo(maxZoom);
+ }
+ });
+
}
- OpenLayers.ImgPath = javascriptPath + 'img/';
- var options = {
- 'projection': new OpenLayers.Projection("EPSG:900913"),
- 'units': "m",
- 'numZoomLevels': 18,
- 'maxResolution': 156543.0339,
- 'theme': null,
- 'maxExtent': new OpenLayers.Bounds(-20037508.34, -20037508.34,
- 20037508.34, 20037508.34)
- };
- $('.static-map-element').hide();
- map = new OpenLayers.Map(map_element, options);
- var layer = new OpenLayers.Layer.Google("Google Streets",{'sphericalMercator': true,
- 'maxExtent': new OpenLayers.Bounds(-20037508.34, -20037508.34,
- 20037508.34, 20037508.34)});
- map.addLayer(layer);
-}
+ area_init = function() {
+ // Vector layers must be added onload for IE
+ if ($.browser.msie) {
+ $(window).load(createAreaMap);
+ } else {
+ createAreaMap();
+ }
+ };
+
+ function segmentSelected(event) {
+ var segment = event.feature;
+ segment.style = segmentSelectedStyle;
+ // FIXME: a strict violation
+ this.drawFeature(segment);
+ var row = $("#route_segment_" + segment.segment_id);
+ row.toggleClass("selected");
+ row.find(".check-route-segment").attr('checked', 'true');
+ }
+ function segmentUnselected(event) {
+ var segment = event.feature;
+ segment.style = segmentStyle;
+ // FIXME: a strict violation
+ this.drawFeature(segment);
+ $("#route_segment_" + segment.segment_id).toggleClass("selected");
+ }
+ function addSelectedHandler(vectorLayer) {
+ vectorLayer.events.on({
+ 'featureselected': segmentSelected,
+ 'featureunselected': segmentUnselected
+ });
+ selectControl = new OpenLayers.Control.SelectFeature(vectorLayer, {multiple: false,
+ toggleKey: "ctrlKey",
+ multipleKey: "shiftKey"});
+ map.addControl(selectControl);
+ selectControl.activate();
+ }
+ route_init = function(map_element, routeSegments) {
+
+ var vectorLayer, i, coords, fromCoords, toCoords, points;
+ var lineString, lineFeature;
+
+ createMap(map_element);
+ bounds = new OpenLayers.Bounds();
+
+ vectorLayer = new OpenLayers.Layer.Vector("Vector Layer",{projection: proj});
+ map.addLayer(vectorLayer);
+
+ addSelectedHandler(vectorLayer);
+ for (i=0; i < routeSegments.length; i++){
+ coords = routeSegments[i];
+ fromCoords = pointCoords(coords[0].lon, coords[0].lat);
+ toCoords = pointCoords(coords[1].lon, coords[1].lat);
+ points = [];
+ points.push(fromCoords);
+ points.push(toCoords);
+ bounds.extend(fromCoords);
+ bounds.extend(toCoords);
+ lineString = new OpenLayers.Geometry.LineString(points);
+ lineFeature = new OpenLayers.Feature.Vector(lineString, {projection: proj}, segmentStyle);
+ lineFeature.segment_id = coords[2];
+ vectorLayer.addFeatures([lineFeature]);
+ }
+ map.zoomToExtent(bounds, false);
+ }
+}());
View
10 public/stylesheets/fixmytransport.css
@@ -2295,6 +2295,16 @@ body.facebook-canvas .button {
overflow: hidden;
}
+/*===[ Atom / RSS Links ]===*/
+.inline-icon {
+ vertical-align: middle
+}
+
+.atom-link {
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
/*===[ Operator ]===*/
#operator-feedback-link {
display: block;
View
35 spec/controllers/operators_controller_spec.rb
@@ -1,5 +1,6 @@
require 'spec_helper'
require 'digest'
+require 'nokogiri'
describe OperatorsController do
@@ -167,5 +168,35 @@ def make_request(params={})
end
end
-
-end
+
+ # FIXME: integrate_views is here to force views to be rendered,
+ # solely so that we can check that valid XML is generated when the
+ # Atom feed is requested. Really this should be done in functional
+ # test, but currently the project is lacking those.
+
+ integrate_views
+
+ describe 'GET #issues [atom]' do
+
+ def make_request(params={})
+ get :issues, params
+ end
+
+ it 'should ask for all issues' do
+ Operator.should_receive(:find).with('11').and_return(@mock_operator)
+ Problem.should_receive(:find_recent_issues).with(false, {:single_operator => @mock_operator})
+ make_request(:id => "11", :format => "atom")
+ assigns[:issues].length.should == 0
+ end
+
+ it 'should return valid XML' do
+ Operator.should_receive(:find).with('11').and_return(@mock_operator)
+ make_request(:id => "11", :format => "atom")
+ Nokogiri::XML(response.body) { |config|
+ config.options = Nokogiri::XML::ParseOptions::STRICT
+ }
+ end
+
+ end
+
+end
Something went wrong with that request. Please try again.