From e827a0460795bc076c115f941d074dca67decaa1 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Wed, 25 Feb 2009 01:14:19 +0000 Subject: [PATCH] further work on 0.6 history (not quite complete yet) --- app/controllers/amf_controller.rb | 191 +++++++++++++++--------------- app/models/old_way.rb | 4 +- config/potlatch/autocomplete.txt | 25 +++- config/potlatch/presets.txt | 173 +++++++++++++++++++++++---- lib/potlatch.rb | 4 +- public/potlatch/potlatch.swf | Bin 170218 -> 173474 bytes 6 files changed, 271 insertions(+), 126 deletions(-) diff --git a/app/controllers/amf_controller.rb b/app/controllers/amf_controller.rb index 4103ceda0..69222fc53 100644 --- a/app/controllers/amf_controller.rb +++ b/app/controllers/amf_controller.rb @@ -27,7 +27,10 @@ # return(-1,"message") <-- just puts up a dialogue # return(-2,"message") <-- also asks the user to e-mail me # -# To write to the Rails log, use RAILS_DEFAULT_LOGGER.info("message"). +# To write to the Rails log, use logger.info("message"). + +# Remaining issues: +# * version conflict when POIs and ways are reverted class AmfController < ApplicationController require 'stringio' @@ -75,7 +78,7 @@ class AmfController < ApplicationController when 'whichways_deleted'; results[index]=AMF.putdata(index,whichways_deleted(*args)) when 'getway'; results[index]=AMF.putdata(index,getway(args[0].to_i)) when 'getrelation'; results[index]=AMF.putdata(index,getrelation(args[0].to_i)) - when 'getway_old'; results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1].to_i)) + when 'getway_old'; results[index]=AMF.putdata(index,getway_old(args[0].to_i,args[1])) when 'getway_history'; results[index]=AMF.putdata(index,getway_history(args[0].to_i)) when 'getnode_history'; results[index]=AMF.putdata(index,getnode_history(args[0].to_i)) when 'findgpx'; results[index]=AMF.putdata(index,findgpx(*args)) @@ -180,8 +183,9 @@ class AmfController < ApplicationController # used in any way, rel is any relation which refers to either a way # or node that we're returning. def whichways(xmin, ymin, xmax, ymax) #:doc: - xmin -= 0.01; ymin -= 0.01 - xmax += 0.01; ymax += 0.01 + enlarge = [(xmax-xmin)/8,0.01].min + xmin -= enlarge; ymin -= enlarge + xmax += enlarge; ymax += enlarge # check boundary is sane and area within defined # see /config/application.yml @@ -202,7 +206,7 @@ class AmfController < ApplicationController # find the node ids in an area that aren't part of ways nodes_not_used_in_area = nodes_in_area.select { |node| node.ways.empty? } - points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] } + points = nodes_not_used_in_area.collect { |n| [n.id, n.lon, n.lat, n.tags, n.version] }.uniq # find the relations used by those nodes and ways relations = Relation.find_for_nodes(nodes_in_area.collect { |n| n.id }, :conditions => {:visible => true}) + @@ -210,7 +214,7 @@ class AmfController < ApplicationController relations = relations.collect { |relation| [relation.id,relation.version] }.uniq end - [0,ways, points, relations] + [0, ways, points, relations] rescue Exception => err [-2,"Sorry - I can't get the map for that area."] @@ -220,8 +224,9 @@ class AmfController < ApplicationController # with a deleted node only - not POIs or relations). def whichways_deleted(xmin, ymin, xmax, ymax) #:doc: - xmin -= 0.01; ymin -= 0.01 - xmax += 0.01; ymax += 0.01 + enlarge = [(xmax-xmin)/8,0.01].min + xmin -= enlarge; ymin -= enlarge + xmax += enlarge; ymax += enlarge # check boundary is sane and area within defined # see /config/application.yml @@ -284,23 +289,24 @@ class AmfController < ApplicationController # 3. hash of tags, # 4. version, # 5. is this the current, visible version? (boolean) - # - # *** FIXME: - # Should work by timestamp, not version (so that we can recover versions when - # a node has been changed, but not the enclosing way) - # Use strptime (http://www.ruby-doc.org/core/classes/DateTime.html) to - # to turn string back into timestamp. - def getway_old(id, version) #:doc: - if version < 0 + def getway_old(id, timestamp) #:doc: + if timestamp == '' + # undelete old_way = OldWay.find(:first, :conditions => ['visible = ? AND id = ?', true, id], :order => 'version DESC') points = old_way.get_nodes_undelete unless old_way.nil? else - old_way = OldWay.find(:first, :conditions => ['id = ? AND version = ?', id, version]) - points = old_way.get_nodes_revert unless old_way.nil? + # revert + timestamp = DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S") + old_way = OldWay.find(:first, :conditions => ['id = ? AND timestamp <= ?', id, timestamp], :order => 'timestamp DESC') + points = old_way.get_nodes_revert(timestamp) unless old_way.nil? + if !old_way.visible + return [-1, "Sorry, the way was deleted at that time - please revert to a previous version."] + end end if old_way.nil? + # *** FIXME: shouldn't this be returning an error? return [-1, id, [], {}, -1,0] else curway=Way.find(id) @@ -309,67 +315,68 @@ class AmfController < ApplicationController end end - # Find history of a way. Returns 'way', id, and - # an array of previous versions. + # Find history of a way. + # Returns 'way', id, and an array of previous versions: + # - formerly [old_way.version, old_way.timestamp.strftime("%d %b %Y, %H:%M"), old_way.visible ? 1 : 0, user, uid] + # - now [timestamp,user,uid] # - # *** FIXME: - # Should look for changes in constituent nodes as well, - # and return timestamps. - # Heuristic: Find all nodes that have ever been part of the way; + # Heuristic: Find all nodes that have ever been part of the way; # get a list of their revision dates; add revision dates of the way; # sort and collapse list (to within 2 seconds); trim all dates before the - # start dateƊof the way. + # start date of the way. def getway_history(wayid) #:doc: - # Find list of revision dates for way and all constituent nodes - revdates=[] - Way.find(wayid).old_ways.collect do |a| - revdates.push(a.timestamp) - a.nds.each do |n| - Node.find(n).old_nodes.collect do |o| - revdates.push(o.timestamp) + begin + # Find list of revision dates for way and all constituent nodes + revdates=[] + revusers={} + Way.find(wayid).old_ways.collect do |a| + revdates.push(a.timestamp) + unless revusers.has_key?(a.timestamp.to_i) then revusers[a.timestamp.to_i]=change_user(a) end + a.nds.each do |n| + Node.find(n).old_nodes.collect do |o| + revdates.push(o.timestamp) + unless revusers.has_key?(o.timestamp.to_i) then revusers[o.timestamp.to_i]=change_user(o) end + end end end + waycreated=revdates[0] + revdates.uniq! + revdates.sort! + revdates.reverse! + + # Remove any dates (from nodes) before first revision date of way + revdates.delete_if { |d| d0 then - n = OldNode.find(id, :conditions=>['version=?',version]) - else + def getpoi(id,timestamp) #:doc: + if timestamp == '' then n = Node.find(id) + else + n = OldNode.find(id, :conditions=>['timestamp=?',DateTime.strptime(timestamp, "%d %b %Y, %H:%M:%S")]) end if n @@ -729,27 +737,29 @@ RAILS_DEFAULT_LOGGER.info("** range: #{revdates[-1]-revdates[0]}") # Need a transaction so that if one item fails to delete, the whole delete fails. Way.transaction do + # delete the way + old_way = Way.find(way_id) + delete_way = Way.new + delete_way.version = way_version + delete_way.changeset_id = changeset_id + old_way.delete_with_history!(delete_way, user) + # FIXME: would be good not to make two history entries when removing # two nodes from the same relation - old_way = Way.find(way_id) #old_way.unshared_node_ids.each do |n| # deleteitemrelations(n, 'node') #end #deleteitemrelations(way_id, 'way') - #way.delete_with_relations_and_nodes_and_history(changeset_id.to_i) old_way.unshared_node_ids.each do |node_id| # delete the node node = Node.find(node_id) delete_node = Node.new - delete_node.version = node_id_version[node_id] + delete_node.changeset_id = changeset_id + delete_node.version = node_id_version[node_id.to_s] node.delete_with_history!(delete_node, user) end - # delete the way - delete_way = Way.new - delete_way.version = way_version - old_way.delete_with_history!(delete_way, user) end # transaction [0, way_id] rescue OSM::APIChangesetAlreadyClosedError => ex @@ -770,11 +780,6 @@ RAILS_DEFAULT_LOGGER.info("** range: #{revdates[-1]-revdates[0]}") # ==================================================================== # Support functions - # delete a way and its nodes that aren't part of other ways - # this functionality used to be in the model, however it is specific to amf - # controller - #def delete_unshared_nodes(changeset_id, way_id) - # Remove a node or way from all relations # FIXME needs version, changeset, and user # Fixme make sure this doesn't depend on anything and delete this, as potlatch diff --git a/app/models/old_way.rb b/app/models/old_way.rb index da9cf0697..df2ab3fa4 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -134,10 +134,10 @@ class OldWay < ActiveRecord::Base points end - def get_nodes_revert + def get_nodes_revert(timestamp) points=[] self.nds.each do |n| - oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,self.timestamp], :order=>"timestamp DESC") + oldnode=OldNode.find(:first, :conditions=>['id=? AND timestamp<=?',n,timestamp], :order=>"timestamp DESC") curnode=Node.find(n) id=n; v=curnode.visible ? 1 : 0 if oldnode.lat!=curnode.lat or oldnode.lon!=curnode.lon or oldnode.tags!=curnode.tags then diff --git a/config/potlatch/autocomplete.txt b/config/potlatch/autocomplete.txt index ad6cb0154..a3c80c7da 100755 --- a/config/potlatch/autocomplete.txt +++ b/config/potlatch/autocomplete.txt @@ -82,12 +82,31 @@ is_in/way - note/point - note/POI - note/way - -source/point - -source/POI - -source/way - +source/point survey,Yahoo,NPE,local_knowledge,GPS,cadastre +source/POI survey,Yahoo,NPE,local_knowledge,GPS,cadastre +source/way survey,Yahoo,NPE,local_knowledge,GPS,cadastre postal_code/point - postal_code/POI - postal_code/way - description/point - description/POI - description/way - +addr:housenumber/point - +addr:street/point - +addr:full/point - +addr:postcode/point - +addr:city/point - +addr:country/point - +addr:housenumber/POI - +addr:street/POI - +addr:full/POI - +addr:postcode/POI - +addr:city/POI - +addr:country/POI - +addr:housenumber/way - +addr:street/way - +addr:full/way - +addr:postcode/way - +addr:city/way - +addr:country/way - +addr:interpolation/way even,odd,all,alphabetic diff --git a/config/potlatch/presets.txt b/config/potlatch/presets.txt index c46f9d12a..6be23b993 100644 --- a/config/potlatch/presets.txt +++ b/config/potlatch/presets.txt @@ -12,17 +12,16 @@ way/footway public footpath: highway=footway,foot=yes,tracktype= permissive path: highway=footway,foot=permissive,tracktype= bridleway: highway=bridleway,foot=yes,tracktype= -paved track: highway=track,foot=,tracktype=grade1 -gravel track: highway=track,foot=,tracktype=grade2 -rough track: highway=track,foot=,tracktype=grade3 -dirt track: highway=track,foot=,tracktype=grade4 -grass track: highway=track,foot=,tracktype=grade5 +paved track: highway=track,foot=,surface=paved +gravel track: highway=track,foot=,surface=gravel +dirt track: highway=track,foot=,surface=dirt +grass track: highway=track,foot=,surface=grass way/cycleway -cycle lane: highway=cycleway,cycleway=lane,ncn_ref= -cycle track: highway=cycleway,cycleway=track,ncn_ref= -cycle lane (NCN): highway=cycleway,cycleway=lane,name=(type name here),ncn_ref=(type route number) -cycle track (NCN): highway=cycleway,cycleway=track,name=(type name here),ncn_ref=(type route number) +cycle track: highway=cycleway,ncn_ref=,rcn_ref=,lcn_ref= +cycle track (national route): highway=cycleway,ncn_ref=(type route number) +cycle track (regional route): highway=cycleway,rcn_ref=(type route number) +cycle track (local route): highway=cycleway,lcn_ref=(type route number) way/waterway canal: waterway=canal,name=(type name here) @@ -42,9 +41,57 @@ disused railway tracks: railway=disused course of old railway: railway=abandoned railway platform: railway=platform +way/tourism +archaeological: place=,tourism=,historic=archaeological_site,name=(type name here) +attraction: place=,tourism=attraction,historic=,amenity=,name=(type name here) +campsite: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +caravan site: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +castle: place=,tourism=,historic=castle,name=(type name here) +hotel: place=,tourism=hotel,historic=,amenity=,name=(type name here),operator=(type chain here) +museum: place=,tourism=museum,historic=,amenity=,name=(type name here) +ruins: place=,tourism=,historic=ruins,name=(type name here) + +way/recreation +golf course: landuse=,leisure=golf_course +pitch: landuse=,leisure=pitch, sport=(type sport here) +playground: landuse=,leisure=playground +recreation ground: landuse=recreation_ground,leisure= +sports centre: landuse=,leisure=sports_centre +stadium: landuse=,leisure=stadium + +way/utility +college: place=,tourism=,amenity=college,name=(type name here) +school: place=,tourism=,amenity=school,name=(type name here) +hospital: place=,tourism=,amenity=hospital,name=(type name here) +library: place=,tourism=,amenity=library,name=(type name here) +university: place=,tourism=,amenity=university,name=(type name here) + way/natural -lake: natural=water,landuse= -forest: landuse=forest,natural= +coastline: natural=coastline,landuse=,leisure= +fell: natural=fell,landuse=,leisure= +heath: natural=heath,landuse=,leisure= +lake: natural=water,landuse=,leisure= +forest: landuse=forest,natural=,leisure= +marsh: natural=marsh,landuse=,leisure= +nature reserve: leisure=nature_reserve,landuse=,natural= +scree: natural=scree,landuse=,leisure= +woodland: natural=wood,landuse=,leisure= + +way/landuse +allotments: landuse=allotments,leisure= +building site: landuse=construction,leisure= +commercial: landuse=commercial,leisure= +common: landuse=,leisure=common +farm: landuse=farm,leisure= +farmyard: landuse=farmyard,leisure= +industry: landuse=industrial,leisure= +landfill site: landuse=landfill,leisure= +park: leisure=park,landuse= +quarry: landuse=quarry,leisure= +reservoir: landuse=reservoir,leisure= +residential: landuse=residential,leisure= +retail: landuse=retail,leisure= +village green: landuse=village_green,leisure= point/road mini roundabout: place=,highway=mini_roundabout @@ -57,20 +104,25 @@ stile: place=,highway=stile cattle grid: place=,highway=cattle_grid point/cycleway -gate: place=,highway=gate +bike park: place=,highway=,amenity=bicycle_parking,capacity=(type number of spaces) +gate: place=,highway=gate,amenity=,capacity= point/waterway -lock gate: place=,waterway=lock_gate -weir: place=,waterway=weir -aqueduct: place=,waterway=aqueduct -winding hole: place=,waterway=turning_point -mooring: place=,waterway=mooring +lock: place=,waterway=,lock=yes,name=(type name here) +single lockgate: place=,waterway=lock_gate,lock= +weir: place=,waterway=weir,lock= +aqueduct: place=,waterway=aqueduct,lock= +winding hole: place=,waterway=turning_point,lock= +mooring: place=,waterway=mooring,lock= point/railway station: place=,railway=station,name=(type name here) viaduct: place=,railway=viaduct level crossing: place=,railway=crossing +point/landmark +pylon: man_made=,power=tower + point/natural peak: place=,natural=peak @@ -78,8 +130,13 @@ POI/road car park: place=,amenity=parking petrol station: place=,amenity=fuel +POI/footway +bench: amenity=bench + POI/cycleway -bike park: place=,amenity=bicycle_parking +bike park: place=,shop=,amenity=bicycle_parking,capacity=(type number of spaces) +bike rental: place=,amenity=bicycle_rental,capacity=(type number of bikes) +bike shop: place=,shop=bicycle POI/place city: place=city,name=(type name here),is_in=(type region or county) @@ -89,14 +146,78 @@ village: place=village,name=(type name here),is_in=(type region or county) hamlet: place=hamlet,name=(type name here),is_in=(type region or county) POI/tourism -attraction: place=,tourism=attraction,amenity=,religion=,denomination= -church: place=,tourism=,amenity=place_of_worship,name=(type name here),religion=christian,denomination=(type denomination here) -hotel: place=,tourism=hotel,amenity=,religion=,denomination= -other religious: place=,tourism=,amenity=place_of_worship,name=(type name here),religion=(type religion),denomination= -post box: place=,amenity=post_box,tourism=,name=,religion=,denomination= -post office: place=,amenity=post_office,tourism=,name=,religion=,denomination= -pub: place=,tourism=,amenity=pub,name=(type name here),religion=,denomination= -school: place=,tourism=,amenity=school,name=(type name here),religion=,denomination= +archaeological: place=,tourism=,historic=archaeological_site,name=(type name here) +artwork: place=,tourism=artwork,historic=,amenity= +attraction: place=,tourism=attraction,historic=,amenity=,name=(type name here) +cafe: place=,tourism=,historic=,amenity=cafe,name=(type name here) +campsite: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +caravan site: place=,tourism=camp_site,historic=,amenity=,name=(type name here) +castle: place=,tourism=,historic=castle,name=(type name here) +cinema: place=,tourism=,historic=,amenity=cinema,name=(type name here),operator=(type chain here) +fast food: place=,tourism=,historic=,amenity=fast_food,name=(type name here) +guesthouse: place=,tourism=guest_house,historic=,amenity=,name=(type name here) +hostel: place=,tourism=hostel,historic=,amenity=,name=(type name here),operator=(type chain here) +hotel: place=,tourism=hotel,historic=,amenity=,name=(type name here),operator=(type chain here) +monument: place=,tourism=,historic=monument,name=(type name here) +museum: place=,tourism=museum,historic=,amenity=,name=(type name here) +picnic site: place=,tourism=picnic_site,historic= +pub: place=,tourism=,historic=,amenity=pub,name=(type name here) +restaurant: place=,tourism=,historic=,amenity=restaurant,name=(type name here) +ruins: place=,tourism=,historic=ruins,name=(type name here) +viewpoint: place=,tourism=viewpoint,historic= + +POI/landmark +church: man_made=,amenity=place_of_worship,name=(type name here),religion=christian,denomination=(type denomination here),power= +other religious: man_made=,amenity=place_of_worship,name=(type name here),religion=(type religion),denomination=,power= +lighthouse: man_made=lighthouse,power=,amenity=,name=,religion=,denomination= +pylon: man_made=,power=tower,amenity=,name=,religion=,denomination= +windmill: man_made=windmill,power=,amenity=,name=,religion=,denomination= + +POI/recreation +golf course: leisure=golf_course +pitch: leisure=pitch, sport=(type sport here) +playground: leisure=playground +recreation ground: landuse=recreation_ground,leisure= +sports centre: leisure=sports_centre +stadium: leisure=stadium + +POI/shop +bank: amenity=bank,shop=,operator=(type bank name) +bike shop: amenity=,shop=bicycle,name=(type name here),operator=(type chain here) +bookshop: amenity=,shop=books,name=(type name here),operator=(type chain here) +butchers: amenity=,shop=butcher,name=(type name here),operator=(type chain here) +chemists: amenity=,shop=chemist,name=(type name here),operator=(type chain here) +convenience store: amenity=,shop=convenience,operator=(type chain here) +department store: amenity=,shop=department_store,operator=(type chain here) +DIY: amenity=,shop=doityourself,operator=(type chain here) +garden centre: amenity=,shop=garden_centre,name=(type name here),operator=(type chain here) +laundry: amenity=,shop=laundry,name=(type name here),operator=(type chain here) +off-licence: amenity=,shop=alcohol,name=(type name here),operator=(type chain here) +outdoor: amenity=,shop=outdoor,name=(type name here),operator=(type chain here) +pharmacy: amenity=pharmacy,shop=,name=(type name here),operator=(type chain here) +post office: amenity=post_office,shop=,name=(type name here) +supermarket: amenity=,shop=supermarket,operator=(type chain here) + +POI/utility +college: place=,tourism=,amenity=college,name=(type name here) +post box: place=,amenity=post_box,tourism=,name=,ref=(type code here) +recycling: place=,amenity=recycling,tourism=,name=,ref=(type code here) +school: place=,tourism=,amenity=school,name=(type name here) +surgery: place=,tourism=,amenity=doctors,name=(type name here) +hospital: place=,tourism=,amenity=hospital,name=(type name here) +library: place=,tourism=,amenity=library,name=(type name here) +phone box: place=,tourism=,amenity=telephone,name=(type name here) +toilets: place=,tourism=,amenity=toilets,name=(type name here) +university: place=,tourism=,amenity=university,name=(type name here) POI/natural peak: place=,natural=peak + +point/address +address: addr:housenumber=(type house number),addr:street=(type street name),addr:postcode=(type postcode),addr:city=(type town name) + +POI/address +address: addr:housenumber=(type house number),addr:street=(type street name),addr:postcode=(type postcode),addr:city=(type town name) + +way/address +address: addr:housenumber=(type house number),addr:street=(type street name),addr:interpolation=(type pattern of house numbers),addr:postcode=(type postcode),addr:city=(type town name) diff --git a/lib/potlatch.rb b/lib/potlatch.rb index ebafbce00..cfb602817 100644 --- a/lib/potlatch.rb +++ b/lib/potlatch.rb @@ -147,7 +147,7 @@ module Potlatch presetcategory=$2 presetmenus[presettype].push(presetcategory) presetnames[presettype][presetcategory]=["(no preset)"] - elsif (t=~/^(.+):\s?(.+)$/) then + elsif (t=~/^([\w\s]+):\s?(.+)$/) then pre=$1; kv=$2 presetnames[presettype][presetcategory].push(pre) presets[pre]={} @@ -191,7 +191,7 @@ module Potlatch File.open("#{RAILS_ROOT}/config/potlatch/autocomplete.txt") do |file| file.each_line {|line| t=line.chomp - if (t=~/^(\w+)\/(\w+)\s+(.+)$/) then + if (t=~/^([\w:]+)\/(\w+)\s+(.+)$/) then tag=$1; type=$2; values=$3 if values=='-' then autotags[type][tag]=[] else autotags[type][tag]=values.split(',').sort.reverse end diff --git a/public/potlatch/potlatch.swf b/public/potlatch/potlatch.swf index 5a9b7572a5864593c1c41cc0398e8d5c3da963b3..2d237cd2236db223f83ee7b205ecfab07e42f4de 100755 GIT binary patch literal 173474 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