├── map.pdf ├── icons ├── pub.png └── parking_cycle.png ├── lib ├── pdf_renderer │ ├── canvas_item.rb │ ├── fill_item.rb │ ├── drawing_item.rb │ ├── collision_object.rb │ ├── casing_item.rb │ ├── stroke_item.rb │ ├── point_item.rb │ ├── display_list.rb │ ├── text_item.rb │ └── map_spec.rb ├── pdf_renderer.rb └── rquad │ ├── quadvector.rb │ └── quadtree.rb ├── README.txt ├── pdf_test.rb └── opencyclemap.css /map.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemed/MapPDF/HEAD/map.pdf -------------------------------------------------------------------------------- /icons/pub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemed/MapPDF/HEAD/icons/pub.png -------------------------------------------------------------------------------- /icons/parking_cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/systemed/MapPDF/HEAD/icons/parking_cycle.png -------------------------------------------------------------------------------- /lib/pdf_renderer/canvas_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class CanvasItem < DrawingItem 3 | 4 | def draw(pdf, spec) 5 | pdf.fill_color(sprintf("%06X",@style.get(@tags,'fill_color'))) 6 | pdf.transparent(@style.get(@tags,'fill_opacity',1).to_f) do 7 | pdf.rectangle [spec.boxoriginx, spec.boxoriginy],spec.boxwidth,-spec.boxheight 8 | pdf.fill 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/pdf_renderer.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer #:nodoc: 2 | VERSION = "0.1" 3 | end 4 | 5 | require "pdf_renderer/display_list" 6 | require "pdf_renderer/map_spec" 7 | require "pdf_renderer/drawing_item" 8 | require "pdf_renderer/casing_item" 9 | require "pdf_renderer/stroke_item" 10 | require "pdf_renderer/text_item" 11 | require "pdf_renderer/fill_item" 12 | require "pdf_renderer/point_item" 13 | require "pdf_renderer/canvas_item" 14 | require "pdf_renderer/collision_object" 15 | require "rquad/quadtree" 16 | require "rquad/quadvector" 17 | -------------------------------------------------------------------------------- /lib/pdf_renderer/fill_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class FillItem < DrawingItem 3 | 4 | def draw(pdf, spec) 5 | dictionary=StyleParser::Dictionary.instance 6 | return if dictionary.is_member_of(@entity,'multipolygon','inner') 7 | multipolygons=dictionary.parent_relations_of_type(@entity,'multipolygon','outer') 8 | 9 | pdf.fill_color(sprintf("%06X",@style.get(@tags,'fill_color'))) 10 | pdf.transparent(@style.get(@tags,'fill_opacity',1).to_f) do 11 | StrokeItem.draw_line(pdf, spec, @entity) 12 | draw_inners(pdf, spec) 13 | pdf.add_content("f*") # like pdf.fill, but for even-odd winding 14 | end 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/pdf_renderer/drawing_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class DrawingItem 3 | 4 | def initialize(style, entity, tags=nil) 5 | @entity=entity 6 | @style=style 7 | @tags=tags ? tags : (entity ? entity.tags : {} ) 8 | end 9 | 10 | def get_sublayer 11 | @style.sublayer 12 | end 13 | 14 | def draw_inners(pdf,spec) 15 | dictionary=StyleParser::Dictionary.instance 16 | multipolygons=dictionary.parent_relations_of_type(@entity,'multipolygon','outer') 17 | multipolygons.each do |multi| 18 | dictionary.relation_loaded_members(@entity.db,multi,'inner').each do |obj| 19 | if obj.type=='way' then StrokeItem.draw_line(pdf, spec, obj) end 20 | end 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/pdf_renderer/collision_object.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class CollisionObject 3 | 4 | attr_accessor :left, :right, :top, :bottom, :item, :sub_id 5 | 6 | def initialize(x,y,xradius,yradius,item=nil,sub_id=nil) 7 | @left =x-xradius 8 | @right =x+xradius 9 | @top =y+yradius 10 | @bottom=y-yradius 11 | @item =item 12 | @sub_id =sub_id 13 | end 14 | 15 | def collides_with(cx,cy,cxradius,cyradius) 16 | cleft =cx-cxradius 17 | cright =cx+cxradius 18 | ctop =cy+cyradius 19 | cbottom=cy-cyradius 20 | ((cleft >@left && cleft <@right) || 21 | (cright>@left && cright<@right) || 22 | (cleft <@left && cright>@right)) && 23 | ((cbottom>@bottom && cbottom<@top) || 24 | (ctop >@bottom && ctop <@top) || 25 | (cbottom<@bottom && ctop >@top)) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/pdf_renderer/casing_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class CasingItem < DrawingItem 3 | 4 | def draw(pdf, spec) 5 | pdf.line_width=@style.get(@tags,'width').to_f + @style.get(@tags,'casing_width').to_f 6 | pdf.stroke_color(sprintf("%06X",@style.get(@tags,'casing_color'))) 7 | defaultcap = @style.get(@tags,'linecap') 8 | defaultjoin= @style.get(@tags,'linejoin') 9 | pdf.cap_style=case @style.get(@tags,'casing_linecap') 10 | when 'none' then :butt 11 | when 'square' then :projecting_square 12 | when 'round' then :round 13 | else (defaultcap ? defaultcap : :butt) 14 | end 15 | pdf.join_style=case @style.get(@tags,'casing_linejoin') 16 | when 'miter' then :miter 17 | when 'bevel' then :bevel 18 | when 'round' then :round 19 | else (defaultjoin ? defaultjoin : :round) 20 | end 21 | if @style.defined('casing_dashes') then 22 | dashes=@style.get(@tags,'casing_dashes').split(',').collect! {|n| n.to_f} 23 | if dashes.length==1 then pdf.dash(dashes[0]) else pdf.dash(dashes[0], :space=>dashes[1]) end 24 | end 25 | 26 | opacity=@style.get(@tags,'casing_opacity', @style.get(@tags,'opacity',1).to_f ).to_f 27 | pdf.transparent(opacity) do 28 | StrokeItem.draw_line(pdf, spec, @entity) 29 | pdf.stroke 30 | end 31 | if @style.defined('casing_dashes') then pdf.undash end 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | MapPDF - Ruby map PDF renderer 2 | ============================== 3 | 4 | This is an experimental PDF renderer from MapCSS stylesheets. 5 | 6 | == Dependencies == 7 | 8 | * Jochen Topf's OSMlib (http://osmlib.rubyforge.org/) 9 | * Prawn (http://prawn.majesticseacreature.com/) 10 | * Ruby MapCSS parser (https://github.com/systemed/mapcss_ruby) 11 | * RQuad (https://github.com/iterationlabs/rquad) 12 | 13 | NOTE: current Prawn will not work perfectly. You need a slight patch to lib/prawn/images.rb - 14 | see comments in lib/pdf_renderer/point_item.rb. You'll still get a map without this patch, 15 | but the icons will be offset. 16 | 17 | == How to use == 18 | 19 | See pdf_test.rb. In short: 20 | 21 | 1. (OSMlib) Read your OSM data into a database 22 | 2. (mapcss_ruby) Create a 'parent objects' dictionary 23 | 3. (mapcss_ruby) Read the MapCSS file into a RuleSet 24 | 4. (mappdf_ruby) Create a MapSpec with the bounding box and map area parameters 25 | 5. (Prawn) Create a PDF 26 | 6. (mappdf_ruby) Tell the MapSpec to draw onto it 27 | 28 | == To do == 29 | 30 | * Better text offset 31 | 32 | == Not currently supported == 33 | 34 | * Dash decoration (arrows etc.) 35 | * Underline 36 | 37 | == Licence and author == 38 | 39 | WTFPL. You can do whatever the fuck you want with this code. Code by Richard Fairhurst, autumn 2011. 40 | 41 | OpenStreetMap data by OpenStreetMap contributors (CC-BY-SA). 42 | -------------------------------------------------------------------------------- /pdf_test.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '.', '../mapcss_ruby/lib')) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '.', 'lib')) 3 | 4 | require "OSM" 5 | require "OSM/StreamParser" 6 | require "OSM/Database" 7 | require "style_parser" 8 | require "prawn" 9 | require "pdf_renderer" 10 | 11 | # ----- Read stylesheet 12 | 13 | ruleset=StyleParser::RuleSet.new(12,20) 14 | ruleset.parse_from_file('opencyclemap.css') 15 | 16 | # ----- Read OSM data 17 | # (typing 'export OSMLIB_XML_PARSER=Expat' beforehand may speed things up) 18 | 19 | puts "Reading file" 20 | db = OSM::Database.new 21 | parser = OSM::StreamParser.new(:filename => 'charlbury.osm', :db => db) 22 | parser.parse 23 | 24 | puts "Creating dictionary" 25 | dictionary = StyleParser::Dictionary.instance 26 | dictionary.populate(db) 27 | 28 | # ----- Create a MapSpec 29 | 30 | spec=PDFRenderer::MapSpec.new 31 | spec.set_pagesize(PDFRenderer::MapSpec::A4, :margin=>10) 32 | spec.minlon=-1.50; spec.minlat=51.86 33 | spec.maxlon=-1.47; spec.maxlat=51.89 34 | spec.minscale=12; spec.maxscale=18; spec.scale=15 35 | spec.minlayer=-5; spec.maxlayer=5 36 | spec.init_projection 37 | 38 | # ----- Output the map 39 | 40 | puts "Drawing map" 41 | start=Time.now 42 | Prawn::Document.generate('map.pdf', :page_size=>'A4') do |pdf| 43 | spec.draw(pdf,ruleset,db) 44 | end 45 | puts "map.pdf generated in #{Time.now-start} seconds" 46 | -------------------------------------------------------------------------------- /lib/pdf_renderer/stroke_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class StrokeItem < DrawingItem 3 | 4 | def draw(pdf, spec) 5 | dictionary=StyleParser::Dictionary.instance 6 | return if dictionary.is_member_of(@entity,'multipolygon','inner') 7 | multipolygons=dictionary.parent_relations_of_type(@entity,'multipolygon','outer') 8 | 9 | pdf.line_width=@style.get(@tags,'width').to_f 10 | pdf.stroke_color(sprintf("%06X",@style.get(@tags,'color'))) 11 | pdf.cap_style=case @style.get(@tags,'linecap') 12 | when 'none' then :butt 13 | when 'square' then :projecting_square 14 | when 'round' then :round 15 | else :butt 16 | end 17 | pdf.join_style=case @style.get(@tags,'linejoin') 18 | when 'miter' then :miter 19 | when 'bevel' then :bevel 20 | when 'round' then :round 21 | else :round 22 | end 23 | if @style.defined('dashes') then 24 | dashes=@style.get(@tags,'dashes').split(',').collect! {|n| n.to_f} 25 | if dashes.length==1 then pdf.dash(dashes[0]) else pdf.dash(dashes[0], :space=>dashes[1]) end 26 | # ** https://github.com/sandal/prawn/issues/276 27 | # we probably need to implement our own routine as a fallback so that we can do arrow decoration 28 | # (also add to casing-dashes when we've done it) 29 | end 30 | 31 | pdf.transparent(@style.get(@tags,'opacity',1).to_f) do 32 | StrokeItem.draw_line(pdf, spec, @entity) 33 | draw_inners(pdf, spec) 34 | pdf.stroke 35 | end 36 | if @style.defined('dashes') then pdf.undash end 37 | end 38 | 39 | def self.draw_line(pdf, spec, way) # static method 40 | node = way.node_objects[0]; 41 | pdf.move_to(spec.x(node.lon), spec.y(node.lat)) 42 | for i in 1..(way.nodes.length-1) 43 | node=way.node_objects[i] 44 | pdf.line_to(spec.x(node.lon), spec.y(node.lat)) 45 | end 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/pdf_renderer/point_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class PointItem < DrawingItem 3 | 4 | # Note: the code currently requires the following to be added to lib/prawn/images.rb after x,y = map_to_absolute(options[:at]) : 5 | # 6 | # case options[:position] 7 | # when :center 8 | # x-=w/2 9 | # when :right 10 | # x-=w 11 | # end 12 | # case options[:vposition] 13 | # when :center 14 | # y+=h/2 15 | # when :bottom 16 | # y+=h 17 | # end 18 | 19 | attr_accessor :x, :y, :rendered_width, :rendered_height 20 | 21 | def initialize(style, shapestyle, entity, tags=nil) 22 | @entity=entity 23 | @style=style 24 | @tags=tags ? tags : entity.tags 25 | @shapestyle=shapestyle 26 | end 27 | 28 | def draw(pdf, spec) 29 | shape=@style.get(@tags,'icon_image') 30 | @x=spec.x(@entity.lon) 31 | @y=spec.y(@entity.lat) 32 | if shape=='square' || shape=='circle' then 33 | width =@style.get(@tags,'icon_width',8).to_f 34 | height=width 35 | 36 | filled=false; stroked=false 37 | if @shapestyle.defined('color') then 38 | pdf.fill_color sprintf("%06X",@shapestyle.get(@tags,'color')) 39 | filled=true 40 | end 41 | if @shapestyle.defined('casing_color') then 42 | pdf.stroke_color sprintf("%06X",@shapestyle.get(@tags,'color')) 43 | stroked=true 44 | end 45 | 46 | if shape=='square' then pdf.rectangle [@x-width/2,@y-width/2], width, width 47 | else pdf.circle [@x-width/2,@y-width/2], width end 48 | if filled and stroked then pdf.fill_and_stroke 49 | elsif filled then pdf.fill 50 | else pdf.stroke end 51 | 52 | else 53 | options = { :position=>:center, :vposition=>:center, :at=>[@x,@y] } 54 | if @style.defined('icon_width' ) then options[:width ]=@style.get(@tags,'icon_width' ).to_f end 55 | if @style.defined('icon_height') then options[:height]=@style.get(@tags,'icon_height').to_f end 56 | info=pdf.image shape,options 57 | # ** FIXME: add image rotation 58 | # ** FIXME: add opacity 59 | width =info.scaled_width 60 | height=info.scaled_height 61 | end 62 | 63 | @rendered_width=width 64 | @rendered_height=height 65 | spec.add_to_collide_map(@x,@y,@rendered_width/2,@rendered_height/2,self) 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/pdf_renderer/display_list.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class DisplayList 3 | 4 | attr_accessor :rules, :spec 5 | 6 | def initialize(rules,spec) 7 | @rules=rules 8 | @spec=spec 9 | @list=[] 10 | end 11 | 12 | def compile_way(way) 13 | # ** calculate midpoint and length 14 | pathlength=0 15 | 16 | # Get tags 17 | states = {} 18 | if (way.is_closed?) then states[':area']='yes' end 19 | tags = TagsBinding.new(way,states) 20 | 21 | # Get stylelist 22 | stylelist=@rules.get_styles(way, tags, @spec.scale) 23 | layer = parse_layer(stylelist, tags) 24 | 25 | # ** Do multipolygon stuff 26 | 27 | # Add entry for each subpart 28 | stylelist.subparts.each do |subpart| 29 | if stylelist.shapestyles[subpart] then 30 | s=stylelist.shapestyles[subpart] 31 | filled=(s.defined('fill_color') || s.defined('fill_image')) # ** multipolygon stuff 32 | 33 | if s.defined('width') then add_item(layer, StrokeItem.new(s, way, tags)) end 34 | if filled then add_item(layer, FillItem.new(s, way, tags)) end 35 | if s.defined('casing_width') then add_item(layer, CasingItem.new(s, way, tags)) end 36 | end 37 | if stylelist.textstyles[subpart] then 38 | add_item(layer, TextItem.new(stylelist.textstyles[subpart], way, nil, tags, pathlength)) 39 | end 40 | end 41 | end 42 | 43 | def compile_poi(node) 44 | dictionary=StyleParser::Dictionary.instance 45 | 46 | # Get tags 47 | states = {} 48 | if !dictionary.has_parent_ways(node) then states[':poi']='yes' 49 | elsif dictionary.num_parent_ways(node)>1 then states[':junction']='yes' end 50 | tags = TagsBinding.new(node,states) 51 | # ** do hasInterestingTags 52 | 53 | # Find style 54 | stylelist=@rules.get_styles(node, tags, @spec.scale) 55 | layer = parse_layer(stylelist,tags) 56 | 57 | # Add entry for each subpart 58 | stylelist.subparts.each do |subpart| 59 | pointitem = nil 60 | if stylelist.pointstyles[subpart] then 61 | pointitem=PointItem.new(stylelist.pointstyles[subpart], 62 | stylelist.shapestyles[subpart], node, tags) 63 | add_item(layer, pointitem) 64 | end 65 | if stylelist.textstyles[subpart] then 66 | add_item(layer, TextItem.new(stylelist.textstyles[subpart], node, pointitem, tags)) 67 | end 68 | end 69 | end 70 | 71 | def compile_canvas 72 | stylelist=@rules.get_styles(nil, {}, spec.scale) 73 | stylelist.subparts.each do |subpart| 74 | if stylelist.shapestyles[subpart] then 75 | add_item(@spec.minlayer, CanvasItem.new(stylelist.shapestyles[subpart], nil)) 76 | end 77 | end 78 | end 79 | 80 | def add_item(layer,item) 81 | sublayer=item.get_sublayer 82 | l=layer-@spec.minlayer 83 | 84 | if !@list[l] then @list[l]=[] end 85 | if !@list[l][sublayer] then @list[l][sublayer]=[] end 86 | @list[l][sublayer]< v = QuadVector.new(1, 2) 36 | # => # 37 | # irb(main):003:0> v + QuadVector.new(10, 11) 38 | # => # 39 | # irb(main):004:0> (v + QuadVector.new(10, 11)).length 40 | # => 17.0293863659264 41 | # irb(main):005:0> v * -2 42 | # => # 43 | class QuadVector 44 | # Initialize a QuadVector with either another QuadVector, an Array, or 2-3 numbers. 45 | def initialize(x = nil, y = nil, z = nil) 46 | if x && x.class == QuadVector 47 | @x = x.x 48 | @y = x.y 49 | @z = x.z 50 | elsif x && x.class == Array 51 | @x = x[0] 52 | @y = x[1] 53 | @z = x[2] 54 | else 55 | @x = x.to_f if x 56 | @y = y.to_f if y 57 | @z = z.to_f if z 58 | end 59 | end 60 | 61 | # The X component of this vector. 62 | def x 63 | @x 64 | end 65 | 66 | # The Y component of this vector. 67 | def y 68 | @y 69 | end 70 | 71 | # The Z component of this vector. 72 | def z 73 | @z 74 | end 75 | 76 | # Set the X component of this vector. 77 | def x=(new_x) 78 | @x = new_x.to_f if new_x 79 | end 80 | 81 | # Set the Y component of this vector. 82 | def y=(new_y) 83 | @y = new_y.to_f if new_y 84 | end 85 | 86 | # Set the Z component of this vector. 87 | def z=(new_z) 88 | @z = new_z.to_f if new_z 89 | end 90 | 91 | # The length of this vector in 2D or 3D space. 92 | def length 93 | if z 94 | Math.sqrt(x * x + y * y + z * z) 95 | elsif x && y 96 | Math.sqrt(x * x + y * y) 97 | else 98 | nil 99 | end 100 | end 101 | 102 | # The Euclidean distnce between this and another Vector `other`. 103 | def dist_to(other) 104 | (other - self).length 105 | end 106 | 107 | # This vector minus another Vector `other`. 108 | def -(other) 109 | self + other * -1 110 | end 111 | 112 | # Divide this vector by a `scalar`. 113 | def /(scalar) 114 | if z 115 | QuadVector.new(x / scalar, y / scalar, z / scalar) 116 | elsif x && y 117 | QuadVector.new(x / scalar, y / scalar) 118 | else 119 | QuadVector.new 120 | end 121 | end 122 | 123 | # Multiply this vector by a `scalar`. 124 | def *(scalar) 125 | if z 126 | QuadVector.new(x * scalar, y * scalar, z * scalar) 127 | elsif x && y 128 | QuadVector.new(x * scalar, y * scalar) 129 | else 130 | QuadVector.new 131 | end 132 | end 133 | 134 | # Add another Vector `other` to this vector. 135 | def +(other) 136 | if z && other.z 137 | QuadVector.new(x + other.x, y + other.y, z + other.z) 138 | elsif x && y && other.x && other.y 139 | QuadVector.new(x + other.x, y + other.y) 140 | else 141 | QuadVector.new 142 | end 143 | end 144 | 145 | # Test if this vector is equal to another Vector `other`. 146 | def ==(other) 147 | result = (other.x == x && other.y == y && other.z == z) 148 | # puts "(#{other.x} == #{x} && #{other.y} == #{y} && #{other.z} == #{z}) = #{result.inspect}" 149 | result 150 | end 151 | 152 | # Display this vector as a String, either in or notation. 153 | def to_s 154 | if z 155 | "<#{x ? x : 'nil'}, #{y ? y : 'nil'}, #{z}>" 156 | else 157 | "<#{x ? x : 'nil'}, #{y ? y : 'nil'}>" 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /opencyclemap.css: -------------------------------------------------------------------------------- 1 | canvas { fill-color: #FFFFE8; } 2 | 3 | /* Simple OCM-like test stylesheet for PDF renderer. Based on Potlatch 2 stylesheet by Andy Allan. */ 4 | 5 | way[highway=motorway],way[highway=motorway_link] { z-index: 9; color: #bfbfcf; width: 7; casing-color: #506077; casing-width: 2; } 6 | way[highway=trunk],way[highway=trunk_link] { z-index: 9; color: #c8d8c8; width: 7; casing-color: #477147; casing-width: 2; } 7 | way[highway=primary],way[highway=primary_link] { z-index: 8; color: #d8c8c8; width: 7; casing-color: #8d4346; casing-width: 2; } 8 | way[highway=secondary],way[highway=secondary_link] { z-index: 7; color: #eeeec9; width: 7; casing-color: #a37b48; casing-width: 2; } 9 | way[highway=tertiary],way[highway=unclassified] { z-index: 6; color: #eeeec9; width: 5; casing-color: #999999; casing-width: 2; } 10 | way[highway=residential] { z-index: 5; color: white; width: 5; casing-color: #999; casing-width: 2; } 11 | way[highway=service] { z-index: 4; color: white; width: 3; casing-color: #999; casing-width: 2; } 12 | 13 | way[highway=steps] { color: #be6c6c; width: 2; dashes: 4, 2; } 14 | way[highway=footway] { color: #be6c6c; width: 2; dashes: 6, 3; } 15 | way[highway=cycleway] { color: blue; width: 1.6; dashes: 5, 4; } 16 | way[highway=bridleway] { z-index:9; color: #996644; width: 2; dashes: 4, 2, 2, 2; } 17 | way[highway=track] { color: #996644; width: 2; dashes: 4, 2; } 18 | way[highway=path] { color: lightgreen; width: 2; dashes: 2, 2; } 19 | 20 | way[highway] { text: name; text-color: black; font-size: 10; text-position: line; text-halo-color: white; text-halo-radius: 2; } 21 | 22 | way[waterway=river], way[waterway=canal] { color: blue; width: 2; text:name; text-color:blue; font-size:9; text-position: offset; text-offset: 7;} 23 | 24 | way[barrier] {color: #000000; width: 1} 25 | 26 | /* Fills can be solid colour or bitmap images */ 27 | 28 | way[building] :area { fill-color: #EEEEFF; } 29 | way[natural] :area { color: #ADD6A5; width: 1; fill-color: #ADD6A5; fill-opacity: 0.2; } 30 | way[amenity],way[shop] :area { color: #ADCEB5; width: 1; fill-color: #ADCEB5; fill-opacity: 0.2; } 31 | way[leisure],way[sport] :area { color: #8CD6B5; width: 1; fill-color: #8CD6B5; fill-opacity: 0.2; } 32 | way[tourism] :area { color: #F7CECE; width: 1; fill-color: #F7CECE; fill-opacity: 0.2; } 33 | way[historic],way[ruins] :area { color: #F7F7DE; width: 1; fill-color: #F7F7DE; fill-opacity: 0.2; } 34 | way[military] :area { color: #D6D6D6; width: 1; fill-color: #D6D6D6; fill-opacity: 0.2; } 35 | way[building] :area { color: #8d8d8d; width: 1; fill-color: #e0e0e0; fill-opacity: 0.2; } 36 | way[natural=water], 37 | way[waterway] :area { color: blue; width: 2; fill-color: blue; fill-opacity: 0.2; } 38 | way[landuse=forest],way[natural=wood] :area { color: green; width: 2; fill-color: green; fill-opacity: 0.2; } 39 | way[leisure=pitch],way[leisure=park] { color: #44ff44; width: 1; fill-color: #44ff44; fill-opacity: 0.2; } 40 | way[amenity=parking] :area { color: gray; width: 1; fill-color: gray; fill-opacity: 0.2; } 41 | way[public_transport=pay_scale_area] :area { color: gray; width: 1; fill-color: gray; fill-opacity: 0.1; } 42 | 43 | /* Addressing. Nodes with addresses *and* match POIs should have a poi icon, so we put addressing first */ 44 | 45 | node[addr:housenumber], 46 | node[addr:housename] { icon-image: circle; icon-width: 4; color: #B0E0E6; casing-color:blue; casing-width: 1; } 47 | way[addr:interpolation] { color: #B0E0E6; width: 3; dashes: 3,3;} 48 | 49 | /* POIs, too, can have bitmap icons - they can even be transparent */ 50 | 51 | node[amenity=pub] { icon-image: icons/pub.png; text-offset: 15; text: name; font-size: 9; text-halo-color: white; text-halo-radius: 2; } 52 | node[amenity=bicycle_parking] { icon-image: icons/parking_cycle.png; text-offset: 15; text: capacity; text-color: blue } 53 | 54 | /* Bridge */ 55 | way[bridge=yes]::bridge, way[bridge=viaduct]::bridge, way[bridge=suspension]::bridge { z-index: 3; color: white; width: eval('_width+1'); casing-color: black; casing-width: 2; casing-linecap: round; } 56 | 57 | /* Tunnel */ 58 | way[tunnel=yes] { z-index: 3; color: white; width: eval('_width+2'); } 59 | 60 | /* Descendant selectors provide an easy way to style relations: this example means "any way 61 | which is part of a relation whose type=route". */ 62 | 63 | relation[type=route][route=bicycle][network=rcn] way::cycle { z-index: 1; linecap: round; width: 12; color: cyan; opacity: 0.3; } 64 | relation[type=route][route=bicycle][network=lcn] way::cycle { z-index: 1; linecap: round; width: 12; color: blue; opacity: 0.3; } 65 | relation[type=route][route=bicycle][network=mtb] way::cycle { z-index: 1; linecap: round; width: 12; color: #48a448; opacity: 0.3; } 66 | relation[type=route][route=bicycle][network=ncn] way::cycle { z-index: 1; linecap: round; width: 12; color: red; opacity: 0.3; } 67 | 68 | 69 | /* Railways */ 70 | 71 | way[railway=rail] { z-index: 6; color: black; width: 5; } 72 | way[railway=rail]::dashes { z-index: 7; color: white; width: 3; dashes: 12,12; } 73 | 74 | way[railway=platform] { color:black; width: 2; } 75 | 76 | way[railway=subway] { z-index: 6; color: #444444; width: 5; } 77 | way[railway=subway]::dashes { z-index: 7; color: white; width: 3; dashes: 8,8; } 78 | 79 | way[railway=disused],way[railway=abandoned] { z-index: 6; color: #444400; width: 3; dashes: 17, 2, 5, 0; } 80 | way[railway=disused]::dashes,way[railway=abandoned]::dashes { z-index: 7; color: #999999; width: 2; dashes: 12,12; } 81 | -------------------------------------------------------------------------------- /lib/pdf_renderer/text_item.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class TextItem < DrawingItem 3 | 4 | def initialize(style, entity, associateditem=nil, tags=nil, pathlength=nil) 5 | @entity=entity 6 | @style=style 7 | @associateditem=associateditem # the PointItem for this entity, for label placement etc. 8 | @tags=tags ? tags : entity.tags 9 | @pathlength=pathlength 10 | end 11 | 12 | def draw(pdf, spec) 13 | return unless @style.defined('text') 14 | text=@tags[@style.get(@tags,'text')] 15 | return unless text 16 | if @style.get(@tags,'font_caps') then text.upcase! end 17 | 18 | typeface = @style.get(@tags,'font_family', 'Helvetica') 19 | typesize = @style.get(@tags,'font_size', 10).to_f 20 | typestyle = :normal 21 | if @style.get(@tags,'font_bold') && @style.get(@tags,'font_italic') then 22 | typestyle = :bold_italic 23 | elsif @style.get(@tags,'font_bold') then 24 | typestyle = :bold 25 | elsif @style.get(@tags,'font_italic') then 26 | typestyle = :italic 27 | end 28 | 29 | pdf.font typeface, :style => typestyle 30 | pdf.font_size typesize 31 | font=pdf.font 32 | charheight=font.ascender 33 | textwidth = pdf.width_of(text) 34 | colour = sprintf("%06X",@style.get(@tags,'text_color',0).to_f) 35 | 36 | if @entity.instance_of?(OSM::Way) then 37 | pathlength = spec.properties(@entity)['length'] 38 | centroid_x = spec.properties(@entity)['centroid_x'] 39 | centroid_y = spec.properties(@entity)['centroid_y'] 40 | end 41 | 42 | # Position text at node 43 | 44 | if @entity.instance_of?(OSM::Node) 45 | x=@associateditem.x-textwidth/2 46 | y=@associateditem.y-@associateditem.rendered_width/2-charheight 47 | place_label(pdf, spec, text, x, y, colour) 48 | 49 | # Position text at centre of way 50 | 51 | elsif @entity.instance_of?(OSM::Way) && @style.get(@tags,'text_center') && centroid_x 52 | x=centroid_x-textwidth/2 53 | y=centroid_y-charheight/2 54 | place_label(pdf, spec, text, x, y, colour) 55 | 56 | # Position text along way 57 | 58 | elsif @entity.instance_of?(OSM::Way) 59 | return if pathlengthleft or upside down 65 | reverse = (p1[0] < p2[0] && p1[2] < Math::PI/2 && p1[2] > -Math::PI/2) 66 | angleoffset = reverse ? 0 : Math::PI # so we can do a 180º if we're running backwards 67 | offsetsign = reverse ? 1 : -1 # -1 if we're starting at t2 68 | tstart = reverse ? t1 : t2 # which end to start at 69 | 70 | positions=calculate_text_path_positions(pdf,spec,text,tstart,offsetsign,pathlength,angleoffset,charheight,textoffset) 71 | return if positions_collide(spec,positions) 72 | 73 | if @style.defined('text_halo_color') then 74 | pdf.text_rendering_mode(:stroke) do 75 | pdf.stroke_color(sprintf("%06X",@style.get(@tags,'text_halo_color',0).to_f)) 76 | pdf.line_width=@style.get(@tags,'text_halo_width',1).to_f 77 | text_on_path(pdf,text,positions) 78 | end 79 | end 80 | 81 | pdf.fill_color(colour) 82 | text_on_path(pdf,text,positions) 83 | positions_reserve(spec,positions) 84 | end 85 | end 86 | 87 | private 88 | 89 | def place_label(pdf,spec,text,x,y,colour) 90 | positions=[] 91 | cx=x 92 | charheight=pdf.font.ascender 93 | for i in 0..(text.length-1) 94 | charwidth = pdf.width_of(text.slice(i,1)) 95 | positions << [cx+charwidth/2, y+charheight/2, charwidth/2, charheight/2, 0] 96 | cx+=charwidth 97 | end 98 | return if positions_collide(spec,positions) 99 | 100 | if @style.defined('text_halo_color') then 101 | pdf.text_rendering_mode(:stroke) do 102 | pdf.stroke_color(sprintf("%06X",@style.get(@tags,'text_halo_color',0).to_f)) 103 | pdf.line_width=@style.get(@tags,'text_halo_width',1).to_f 104 | pdf.draw_text text, :at=>[x,y] 105 | end 106 | end 107 | pdf.fill_color(colour) 108 | pdf.draw_text text, :at=>[x,y] 109 | positions_reserve(spec,positions) 110 | end 111 | 112 | def calculate_text_path_positions(pdf,spec,text,tstart,offsetsign,pathlength,angleoffset,charheight,textoffset) 113 | positions = [] 114 | charpos = 0 115 | for i in 0..(text.length-1) 116 | charwidth =pdf.width_of(text.slice(i,1)) 117 | x, y, pa =spec.point_at(@entity, tstart+offsetsign*(charpos+charwidth/2)/pathlength, nil, textoffset) 118 | radians =pa+angleoffset 119 | degrees =radians*(180/Math::PI) 120 | positions << [x,y,charwidth,charheight,degrees] 121 | charpos+=charwidth 122 | end 123 | positions 124 | end 125 | 126 | def positions_collide(spec,positions) 127 | positions.each do |pos| 128 | return true unless spec.space_at(pos[0],pos[1],pos[2]/2,pos[3]/2) 129 | end 130 | false 131 | end 132 | 133 | def positions_reserve(spec,positions) 134 | positions.each do |pos| 135 | spec.add_to_collide_map(pos[0],pos[1],pos[2]/2,pos[3]/2,self) 136 | end 137 | end 138 | 139 | def text_on_path(pdf,text,positions) 140 | # write each character one-by-one 141 | charpos = 0 142 | for i in 0..(text.length-1) 143 | x,y,charwidth,charheight,degrees = positions[i] 144 | pdf.save_graphics_state 145 | pdf.rotate degrees, :origin=>[x,y] do 146 | pdf.draw_text text.slice(i,1), :at =>[x-charwidth/2,y-charheight/2] 147 | end 148 | pdf.restore_graphics_state 149 | charpos+=charwidth 150 | end 151 | end 152 | 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/pdf_renderer/map_spec.rb: -------------------------------------------------------------------------------- 1 | module PDFRenderer 2 | class MapSpec 3 | 4 | attr_accessor :minlon, :minlat, :maxlon, :maxlat 5 | attr_accessor :minscale, :maxscale, :scale 6 | attr_accessor :minlayer, :maxlayer 7 | attr_accessor :boxwidth, :boxheight, :boxoriginx, :boxoriginy, :boxscale 8 | 9 | A3 = { 'width' => 842, 'height' => 1190 } 10 | A4 = { 'width' => 595, 'height' => 842 } 11 | 12 | def initialize 13 | @minlayer=-5 14 | @maxlayer= 5 15 | @minscale=12 16 | @maxscale=20 17 | @scale =14 18 | @properties={} 19 | @offsetways={} 20 | end 21 | 22 | def init_projection 23 | @baselon =@minlon 24 | @baselatp =MapSpec.lat2latp(@minlat) 25 | maxlatp =MapSpec.lat2latp(@maxlat) 26 | @boxscale =[(@maxlon-@minlon)/@boxwidth, (maxlatp-@baselatp)/@boxheight].max 27 | @boxwidth =(@maxlon-@minlon)/@boxscale 28 | @boxheight=(maxlatp-@baselatp)/@boxscale 29 | 30 | @quadtree=QuadTree.new(QuadVector.new(@boxoriginx, @boxoriginy+@boxheight), 31 | QuadVector.new(@boxoriginx+@boxwidth, @boxoriginy)) 32 | end 33 | 34 | def set_pagesize(size,options={}) 35 | margin=options[:margin] ? options[:margin] : 0 36 | @boxheight =size['height']-margin*2 37 | @boxwidth =size['width' ]-margin*2 38 | @boxoriginx=margin 39 | @boxoriginy=margin 40 | if (options[:landscape]) then @boxheight,@boxwidth=@boxwidth,@boxheight end 41 | end 42 | 43 | def draw(pdf,ruleset,db) 44 | # create DisplayList with all ways and nodes inside 45 | list = DisplayList.new(ruleset,self) 46 | list.compile_canvas 47 | db.ways.values.each do |way| 48 | if inside(way) then list.compile_way(way) end 49 | end 50 | dictionary = StyleParser::Dictionary.instance 51 | db.nodes.values.each do |node| 52 | if inside(node) and !dictionary.has_parent_ways(node) then list.compile_poi(node) end 53 | end 54 | 55 | # draw within clipping box 56 | pdf.add_content("q #{@boxoriginx} #{@boxoriginy} #{@boxwidth} #{@boxheight} re W* n") 57 | pdf.canvas do list.draw(pdf) end 58 | pdf.add_content("Q") 59 | end 60 | 61 | def x(lon) 62 | x=lon.to_f-@baselon 63 | x/=@boxscale 64 | x+@boxoriginx 65 | end 66 | 67 | def y(lat) 68 | y_from_latp(MapSpec.lat2latp(lat.to_f)) 69 | end 70 | 71 | def y_from_latp(latp) 72 | y=latp-@baselatp 73 | y/=@boxscale 74 | y+@boxoriginy 75 | end 76 | 77 | def inside(entity) 78 | if entity.instance_of?(OSM::Node) then 79 | # Node - is it within the bbox? 80 | return (entity.lat.to_f>=@minlat and entity.lat.to_f<=@maxlat and 81 | entity.lon.to_f>=@minlon and entity.lon.to_f<=@maxlon) 82 | elsif entity.instance_of?(OSM::Way) then 83 | # Way - do any of the segments cross the bbox? 84 | nodes = entity.node_objects 85 | for i in 1..(nodes.length-1) 86 | x1=[nodes[i-1].lon.to_f, nodes[i].lon.to_f].min 87 | x2=[nodes[i-1].lon.to_f, nodes[i].lon.to_f].max 88 | y1=[nodes[i-1].lat.to_f, nodes[i].lat.to_f].min 89 | y2=[nodes[i-1].lat.to_f, nodes[i].lat.to_f].max 90 | if (((x1>@minlon and x1<@maxlon) or 91 | (x2>@minlon and x2<@maxlon) or 92 | (x1<@minlon and x2>@maxlon)) and 93 | ((y1>@minlat and y1<@maxlat) or 94 | (y2>@minlat and y2<@maxlat) or 95 | (y1<@minlon and y2>@maxlon))) then 96 | return true 97 | end 98 | end 99 | false 100 | end 101 | end 102 | 103 | def properties(way) 104 | unless @properties[way.id] then 105 | cx=0; cy=0 106 | pathlength=0; patharea=0; heading=[] 107 | lx = way.node_objects[-1].lon.to_f 108 | ly = MapSpec.lat2latp(way.node_objects[-1].lat.to_f) 109 | 110 | for i in 0..(way.nodes.length-1) 111 | node = way.node_objects[i] 112 | latp = MapSpec.lat2latp(node.lat.to_f) 113 | lon = node.lon.to_f 114 | 115 | # length and area 116 | if i>0 then pathlength+=Math.sqrt((lon-lx)**2+(latp-ly)**2) end 117 | sc = (lx*latp-lon*ly)/@boxscale 118 | cx += (lx+lon)*sc 119 | cy += (ly+latp)*sc 120 | patharea += sc 121 | # heading 122 | if (i>0) then heading[i-1]=Math.atan2((lon-lx),(latp-ly)) end 123 | 124 | lx=lon; ly=latp 125 | end 126 | heading[way.nodes.length-1]=heading[way.nodes.length-2] 127 | 128 | patharea/=2 129 | @properties[way.id]={} 130 | @properties[way.id]['heading']=heading 131 | @properties[way.id]['length' ]=pathlength/@boxscale 132 | @properties[way.id]['area' ]=patharea 133 | if patharea!=0 && way.is_closed? then 134 | @properties[way.id]['centroid_x']=x(cx/patharea/6) 135 | @properties[way.id]['centroid_y']=y_from_latp(cy/patharea/6) 136 | elsif pathlength>0 137 | @properties[way.id]['centroid_x'], @properties[way.id]['centroid_y'], dummy = point_at(way,0.5,pathlength) 138 | end 139 | 140 | end 141 | @properties[way.id] 142 | end 143 | 144 | def point_at(way,t,pathlength=nil,offset=0) 145 | if !pathlength then pathlength=properties(way)['length'] end 146 | totallen = t*pathlength 147 | curlen = 0 148 | points = coord_list(way,offset) 149 | for i in 1..(points.length-1) 150 | dx=points[i][0]-points[i-1][0] 151 | dy=points[i][1]-points[i-1][1] 152 | seglen=Math.sqrt(dx*dx+dy*dy) 153 | if (totallen>curlen+seglen) then 154 | curlen+=seglen 155 | else 156 | return [points[i-1][0]+(totallen-curlen)/seglen*dx, 157 | points[i-1][1]+(totallen-curlen)/seglen*dy, 158 | Math.atan2(dy,dx)] 159 | end 160 | end 161 | end 162 | 163 | def self.lat2latp(lat) # static method 164 | 180/Math::PI * Math.log(Math.tan(Math::PI/4+lat*(Math::PI/180)/2)); 165 | end 166 | 167 | def self.latp2lat(a) # static method 168 | 180/Math::PI * (2 * Math.atan(Math.exp(a*Math::PI/180)) - Math::PI/2); 169 | end 170 | 171 | def add_to_collide_map(x,y,xradius,yradius,item,sub_id=nil) 172 | begin 173 | @quadtree.add(QuadTreePayload.new(QuadVector.new(x,y), CollisionObject.new(x,y,xradius,yradius,item,sub_id))) 174 | rescue 175 | end 176 | end 177 | 178 | def space_at(x,y,xradius,yradius,scanmargin=10) 179 | begin 180 | @quadtree.payloads_in_region(QuadVector.new(x-xradius-scanmargin,y+yradius+scanmargin), 181 | QuadVector.new(x+xradius+scanmargin,y-yradius-scanmargin)).each do |payload| 182 | if payload.data.collides_with(x,y,xradius,yradius) then return false end 183 | end 184 | return true 185 | rescue 186 | return false 187 | end 188 | end 189 | 190 | private 191 | 192 | def coord_list(way,offset) 193 | points=[] 194 | nodes = way.node_objects 195 | for i in 0..(nodes.length-1) 196 | points << [x(nodes[i].lon.to_f), y(nodes[i].lat.to_f)] 197 | end 198 | if offset==0 then return points end 199 | 200 | unless @offsetways[way.id] then @offsetways[way.id]={} end 201 | if @offsetways[way.id][offset] then return @offsetways[way.id][offset] end 202 | parallel=[] 203 | offsetx=[] 204 | offsety=[] 205 | for i in 0..(nodes.length-1) 206 | j=(i+1) % nodes.length 207 | a=points[i][1] - points[j][1] 208 | b=points[j][0] - points[i][0] 209 | h=Math.sqrt(a*a+b*b) 210 | if h!=0 then a=a/h; b=b/h 211 | else a=0; b=0 end 212 | offsetx[i]=a 213 | offsety[i]=b 214 | end 215 | parallel << [ points[0][0] + offset*offsetx[0], 216 | points[0][1] + offset*offsety[0] ] 217 | for i in 0..(nodes.length-1) 218 | j=(i+1) % nodes.length 219 | k=i-1; if k==-1 then k=nodes.length-2 end 220 | a=det(offsetx[i]-offsetx[k], 221 | offsety[i]-offsety[k], 222 | points[j][0] - points[i][0], 223 | points[j][1] - points[i][1]) 224 | b=det(points[i][0] - points[k][0], 225 | points[i][1] - points[k][1], 226 | points[j][0] - points[i][0], 227 | points[j][1] - points[i][1]) 228 | if (b!=0) then df=a/b else df=0 end 229 | 230 | parallel << [ points[i][0] + offset*(offsetx[k]+df*(points[i][0] - points[k][0])), 231 | points[i][1] + offset*(offsety[k]+df*(points[i][1] - points[k][1])) ] 232 | end 233 | @offsetways[way.id][offset]=parallel 234 | end 235 | 236 | def det(a,b,c,d) 237 | a*d-b*c 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /lib/rquad/quadtree.rb: -------------------------------------------------------------------------------- 1 | # SOFTWARE INFO 2 | # 3 | # This file is part of the quadtree.rb Ruby quadtree library, distributed 4 | # subject to the 'MIT License' below. This software is available for 5 | # download at http://iterationlabs.com/free_software/quadtree. 6 | # 7 | # If you make modifications to this software and would be willing to 8 | # contribute them back to the community, please send them to us for 9 | # possible inclusion in future releases! 10 | # 11 | # LICENSE 12 | # 13 | # Copyright (c) 2008, Iteration Labs, LLC, http://iterationlabs.com 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a copy 16 | # of this software and associated documentation files (the "Software"), to deal 17 | # in the Software without restriction, including without limitation the rights 18 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | # copies of the Software, and to permit persons to whom the Software is 20 | # furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included in 23 | # all copies or substantial portions of the Software. 24 | # 25 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | # THE SOFTWARE. 32 | # 33 | 34 | require File.dirname(__FILE__) + '/quadvector' 35 | 36 | # A payload for a QuadTree. Hs accessors for vector, data, and node. 37 | class QuadTreePayload 38 | attr_accessor :vector, :data, :node 39 | 40 | # Initialize a QuadTreePayload with a Vector, some data (any class). 41 | def initialize(v, d, n = nil) 42 | self.node = n 43 | self.vector = v 44 | self.data = d 45 | end 46 | end 47 | 48 | # A quadtree node that can contain QuadTreePayloads and other QuadTree nodes. A quadtree is a tree that subdivides space into recursively defined quadrents that (in this implementation) can contain no more than one spacially-unique payload. Quadtrees are good for answering questions about neighborhoods of points in a space. 49 | # 50 | # Making a quadtree is simple, just initialize a new QuadTree with two vectors, its top left and bottom right points, then add QuadTreePayloads to it and it will store them in an efficiently constructed quadtree structure. You may then ask for all payloads in a region, all payloads near a point, etc. 51 | # 52 | # Example usage with longitudes and latitudes: 53 | # 54 | # qt = QuadTree.new(QuadVector.new(-180, 90), QuadVector.new(180, -90)) 55 | # qt.add(QuadTreePayload.new(QuadVector.new(lng1, lat1), entity1)) 56 | # qt.add(QuadTreePayload.new(QuadVector.new(lng2, lat2), entity2)) 57 | # qt.add(QuadTreePayload.new(QuadVector.new(lng3, lat3), entity3)) 58 | # qt.add(QuadTreePayload.new(QuadVector.new(lng4, lat4), entity4)) 59 | # 60 | class QuadTree 61 | # You may ask a QuadTree for its `payload`, `tl` point, `br` point, and `depth`. 62 | attr_accessor :payload, :tl, :br, :depth 63 | 64 | # Initialize a new QuadTree with two vectors: its top-left corner and its bottom-right corner. Optionally, you can also provide a reference to this node's parent node. 65 | def initialize(tl, br, parent_node = nil) 66 | @parent = parent_node 67 | @tl = tl 68 | @br = br 69 | @size = 0 70 | @summed_contained_vectors = QuadVector.new(0,0) 71 | @depth = 0 72 | end 73 | 74 | # Add a QuadTreePayload to this QuadTree. If this node is empty, it will be stored in this node. If not, both the new payload and the old one will be recursively added to the appropriate one of the four children of this node. There is a special case: if this node already has a payload and the new payload has an identical position to the existing payload, then the new payload will be stored here in ddition to the existing payload. 75 | # jitter_proc - a Proc object that, if provided, is used to jitter payloads with identical vectors (accepts and returns a QuadTreePayload). 76 | def add(geo_data, depth = 1, jitter_proc = nil) 77 | geo_data.node = self 78 | if size > 0 79 | if @payload && (@payload.first.vector == geo_data.vector) 80 | # The vectors match. 81 | if jitter_proc 82 | @payload << jitter_proc.call(geo_data) 83 | else 84 | @payload << geo_data 85 | end 86 | else 87 | # The vectors don't match. 88 | if payload 89 | @payload.each { |p| add_into_subtree(p, depth + 1, jitter_proc) } 90 | @payload = nil 91 | end 92 | add_into_subtree(geo_data, depth + 1, jitter_proc) 93 | end 94 | else 95 | @payload = [geo_data] 96 | @depth = depth 97 | end 98 | @summed_contained_vectors += geo_data.vector 99 | @size += 1 100 | end 101 | 102 | # This method returns the payloads contained under this node in the quadtree. It takes an options hash with the following optional keys: 103 | # :max_count - the maximum number of payloads to return, provided via a breadth-first search. 104 | # :details_proc - a Proc object to which every internal node at the maximum deoth achieved by the search is passed -- this is useful for providing summary statistics about subtrees that were not explored by this traversal due to a :max_count limitation. 105 | # :requirement_proc - a Proc object that, if provided, must return true when evaluating a payload in order for that payload to be returned. 106 | # Returns a Hash with keys :payloads, an array of all of the payloads below this node, and :details, the mapped result of applying :details_proc (if provided) to every internal node at the mximum depth achieved by the search. 107 | def get_contained(options = {}) 108 | payloads = [] 109 | internal_nodes = [] 110 | last_depth = -1 111 | breadth_first_each do |node, depth| 112 | break if options[:max_count] && payloads.length >= options[:max_count] && (!options[:details_proc] || depth != last_depth) 113 | 114 | if node.payload 115 | internal_nodes.delete_if {|i| i.parent_of?(node)} if options[:details_proc] 116 | node.payload.each do |entry| 117 | if !options[:requirement_proc] || options[:requirement_proc].call(entry) 118 | payloads << entry 119 | end 120 | end 121 | elsif options[:details_proc] && (node.tlq? || node.trq? || node.blq? || node.brq?) 122 | internal_nodes.delete_if {|i| i.parent_of?(node)} 123 | internal_nodes << node 124 | end 125 | last_depth = depth 126 | end 127 | { :payloads => payloads, :details => (options[:details_proc] ? internal_nodes.map {|i| options[:details_proc].call(i)} : nil) } 128 | end 129 | 130 | # Calls get_contained and only returns the :payloads key. Accepts the same options as get_contained except for the :details_proc option. 131 | def get_contained_payloads(options = {}) 132 | get_contained(options)[:payloads] 133 | end 134 | 135 | # Returns the centroid of the payloads contained in this quadtree. 136 | def center_of_mass 137 | @summed_contained_vectors / @size 138 | end 139 | 140 | # Performs a breath-first traversal of this quadtree, yielding [node, depth] for each node. 141 | def breadth_first_each 142 | queue = [self] 143 | while queue.length > 0 144 | node = queue.shift 145 | queue << node.tlq if node.tlq? 146 | queue << node.trq if node.trq? 147 | queue << node.blq if node.blq? 148 | queue << node.brq if node.brq? 149 | yield node, node.depth 150 | end 151 | end 152 | 153 | # Yields each payload in this quadtree via a breadth-first traversal. 154 | def each_payload 155 | breadth_first_each do |node, depth| 156 | next unless node.payload 157 | node.payload.each do |payload| 158 | yield payload 159 | end 160 | end 161 | end 162 | 163 | # True if this node is a direct parent of `node`. 164 | def parent_of?(node) 165 | node && node == tlq(false) || node == trq(false) || node == blq(false) || node == brq(false) 166 | end 167 | 168 | # True if this node is a direct child of `node`. 169 | def child_of?(node) 170 | node.parent_of?(self) 171 | end 172 | 173 | # Yields all pseudo-leaves formed when the graph is cut off at a certain depth, plus any leaves encountered before that depth. 174 | def leaves_each(leaf_depth) 175 | stack = [self] 176 | while stack.length > 0 177 | node = stack.pop 178 | start_size = stack.length 179 | stack << node.tlq if node.tlq? && node.tlq.depth < leaf_depth + depth 180 | stack << node.trq if node.trq? && node.trq.depth < leaf_depth + depth 181 | stack << node.blq if node.blq? && node.blq.depth < leaf_depth + depth 182 | stack << node.brq if node.brq? && node.brq.depth < leaf_depth + depth 183 | if node.depth == leaf_depth + depth - 1 || (!node.tlq? && !node.trq? && !node.blq? && !node.brq?) 184 | yield node 185 | end 186 | end 187 | end 188 | 189 | # Returns the size of this node: the number of contained payloads. 190 | def size 191 | @size 192 | end 193 | 194 | # Returns this node's parent node or nil if this node is a root node. 195 | def parent 196 | @parent 197 | end 198 | 199 | # Returns approxametly `approx_number` payloads near `location`. 200 | def approx_near(location, approx_number) 201 | if approx_number >= size 202 | return get_contained_payloads 203 | else 204 | get_containing_quad(location).approx_near(location, approx_number) 205 | end 206 | end 207 | 208 | # Returns up to `max_number` payloads inside of the region specified by `region_tl` and `region_br`. 209 | def payloads_in_region(region_tl, region_br, max_number = nil) 210 | quad1 = get_containing_quad(region_tl) 211 | quad2 = get_containing_quad(region_br) 212 | if quad1 == quad2 && payload.nil? 213 | quad1.payloads_in_region(region_tl, region_br, max_number) 214 | else 215 | region_containment_proc = lambda do |i| 216 | region_tl.x <= i.vector.x && region_br.x >= i.vector.x && region_tl.y >= i.vector.y && region_br.y <= i.vector.y 217 | end 218 | get_contained_payloads(:max_count => max_number, :requirement_proc => region_containment_proc) 219 | end 220 | end 221 | 222 | # Returns an array of [centroid (Vector), count] pairs summarizing the set of centroids at a certain tree depth. That is, it provides centroids and counts of all of the subtrees still available at depth `depth`, plus any that terminated above that depth. 223 | def center_of_masses_in_region(region_tl, region_br, depth) 224 | quad1 = get_containing_quad(region_tl) 225 | quad2 = get_containing_quad(region_br) 226 | if quad1 == quad2 && payload.nil? 227 | quad1.center_of_masses_in_region(region_tl, region_br, depth) 228 | else 229 | centers_of_mass = [] 230 | leaves_each(depth) do |node| 231 | centers_of_mass << [node.center_of_mass, node.size] 232 | end 233 | centers_of_mass 234 | end 235 | end 236 | 237 | # Returns a hash with keys :payloads and :details. The :payloads are payloads found, while details are for nodes that didn't get to be explored because the requisite number of payloads were already found. 238 | def payloads_and_centers_in_region(region_tl, region_br, approx_num_to_return) 239 | quad1 = get_containing_quad(region_tl) 240 | quad2 = get_containing_quad(region_br) 241 | if quad1 == quad2 && payload.nil? 242 | quad1.payloads_and_centers_in_region(region_tl, region_br, approx_num_to_return) 243 | else 244 | region_containment_proc = lambda do |i| 245 | region_tl.x <= i.vector.x && region_br.x >= i.vector.x && region_tl.y >= i.vector.y && region_br.y <= i.vector.y 246 | end 247 | details_proc = lambda do |i| 248 | [i.center_of_mass, i.size] 249 | end 250 | get_contained(:max_count => approx_num_to_return, :requirement_proc => region_containment_proc, :details_proc => details_proc) 251 | end 252 | end 253 | 254 | # The top-left quadrent of this quadtree. If `build` is true, this will make the quadrent quadtree if it doesn't alredy exist. 255 | def tlq(build = true) 256 | @tlq ||= QuadTree.new(QuadVector.new(tl), QuadVector.new(tl.x + (br.x - tl.x) / 2.0, br.y + (tl.y - br.y) / 2.0), self) if build 257 | @tlq 258 | end 259 | 260 | # The top-right quadrent of this quadtree. If `build` is true, this will make the quadrent quadtree if it doesn't alredy exist. 261 | def trq(build = true) 262 | @trq ||= QuadTree.new(QuadVector.new(tl.x + (br.x - tl.x) / 2.0, tl.y), QuadVector.new(br.x, br.y + (tl.y - br.y) / 2.0), self) if build 263 | @trq 264 | end 265 | 266 | # The bottom-left quadrent of this quadtree. If `build` is true, this will make the quadrent quadtree if it doesn't alredy exist. 267 | def blq(build = true) 268 | @blq ||= QuadTree.new(QuadVector.new(tl.x, br.y + (tl.y - br.y) / 2.0), QuadVector.new(tl.x + (br.x - tl.x) / 2.0, br.y), self) if build 269 | @blq 270 | end 271 | 272 | # The bottom-right quadrent of this quadtree. If `build` is true, this will make the quadrent quadtree if it doesn't alredy exist. 273 | def brq(build = true) 274 | @brq ||= QuadTree.new(QuadVector.new(tl.x + (br.x - tl.x) / 2.0, br.y + (tl.y - br.y) / 2.0), QuadVector.new(br), self) if build 275 | @brq 276 | end 277 | 278 | # Returns true if this quadtree has a top-left quadrent already defined. 279 | def tlq? 280 | @tlq && (@tlq.payload || (@tlq.tlq? || @tlq.trq? || @tlq.blq? || @tlq.brq?)) 281 | end 282 | 283 | # Returns true if this quadtree has a top-right quadrent already defined. 284 | def trq? 285 | @trq && (@trq.payload || (@trq.tlq? || @trq.trq? || @trq.blq? || @trq.brq?)) 286 | end 287 | 288 | # Returns true if this quadtree has a bottom-left quadrent already defined. 289 | def blq? 290 | @blq && (@blq.payload || (@blq.tlq? || @blq.trq? || @blq.blq? || @blq.brq?)) 291 | end 292 | 293 | # Returns true if this quadtree has a bottom-right quadrent already defined. 294 | def brq? 295 | @brq && (@brq.payload || (@brq.tlq? || @brq.trq? || @brq.blq? || @brq.brq?)) 296 | end 297 | 298 | # Returns true if Vector `v` falls inside of this quadtree. 299 | def inside?(v) 300 | # Greedy, so the order of comparison of quads will matter. 301 | tl.x <= v.x && br.x >= v.x && tl.y >= v.y && br.y <= v.y 302 | end 303 | 304 | # Clips Vector `v` to the bounds of this quadtree. 305 | def clip_vector(v) 306 | v = v.dup 307 | v.x = tl.x if v.x < tl.x 308 | v.y = tl.y if v.y > tl.y 309 | v.x = br.x if v.x > br.x 310 | v.y = br.y if v.y < br.y 311 | v 312 | end 313 | 314 | # Scans back up a quadtree from this node until a node is found that contains the region defined by `in_tl` and `in_br`, at which point the subtree size is returned from that point. 315 | # (Only scans back up the tree, won't scan down.) 316 | def family_size_at_width(in_tl, in_br) 317 | if (inside?(in_tl) && inside?(in_br)) || parent.nil? 318 | size 319 | else 320 | parent.family_size_at_width(in_tl, in_br) 321 | end 322 | end 323 | 324 | def to_s 325 | "[Quadtree #{object_id}, size: #{size}, depth: #{depth}]" 326 | end 327 | 328 | def inspect 329 | to_s 330 | end 331 | 332 | private 333 | 334 | def add_into_subtree(geo_data, depth = 1, jitter_proc = nil) 335 | get_containing_quad(geo_data.vector).add(geo_data, depth, jitter_proc) 336 | end 337 | 338 | def get_containing_quad(vector) 339 | if tlq.inside?(vector) 340 | tlq 341 | elsif trq.inside?(vector) 342 | trq 343 | elsif blq.inside?(vector) 344 | blq 345 | elsif brq.inside?(vector) 346 | brq 347 | else 348 | raise "This shouldn't happen! #{vector} isn't in any of my quads! (#{self.to_s})" 349 | end 350 | end 351 | end 352 | --------------------------------------------------------------------------------