glug 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/bin/glug +5 -0
  3. data/lib/glug.rb +317 -0
  4. metadata +60 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 58209224a893c7908aec8ff0b9f9c85211353842
4
+ data.tar.gz: 5b75fb0f41c1276914b0bddd27569be07c352002
5
+ SHA512:
6
+ metadata.gz: f3368acdea157ec8e53f69f9361d185d44dc35f97f3bf8ecd1e0edad04ddf16fb491429341cb4045beaaaacfa3a2547334a04704a6c0729608fd2c2746d13a7b
7
+ data.tar.gz: 1fbf8f7471e5a029400b8bc0239ccec3d2599867f8844203ea0d85b502299a929dc97ed8758e3e4382c96d9ca066b483f8bf230528837bc6bbae3911ff8d7db9
data/bin/glug ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'glug'
4
+
5
+ puts Glug::Stylesheet.new { instance_eval(File.read(ARGV[0])) }.to_json
data/lib/glug.rb ADDED
@@ -0,0 +1,317 @@
1
+ require 'json'
2
+ require 'neatjson'
3
+
4
+ module Glug # :nodoc:
5
+
6
+ # ----- Subscriptable
7
+ # allows us to create conditions with syntax
8
+ # any[(highway=='primary'),(highway=='trunk')]
9
+
10
+ class Subscriptable
11
+ def initialize(type)
12
+ @type=type
13
+ end
14
+ def [](*arguments)
15
+ Condition.new.from_list(@type, arguments)
16
+ end
17
+ end
18
+
19
+ # ----- Condition
20
+ # represents a Mapbox GL filter of the form [operator, key, value] (etc.)
21
+ # can be merged with other conditions via the & and | operators
22
+
23
+ class Condition
24
+ attr_accessor :values, :operator
25
+ def initialize
26
+ @values=[]
27
+ end
28
+ def from_key(operator, key, list)
29
+ @operator = operator
30
+ @values = [key].concat(list)
31
+ self
32
+ end
33
+ def from_list(operator, list)
34
+ @operator = operator
35
+ @values = list
36
+ self
37
+ end
38
+ def &(cond); merge(:all,cond) end
39
+ def |(cond); merge(:any,cond) end
40
+ def merge(op,cond)
41
+ if cond.nil?
42
+ self
43
+ elsif @operator==op
44
+ Condition.new.from_list(op, @values + [cond])
45
+ # @values << cond; self
46
+ elsif cond.operator==op
47
+ Condition.new.from_list(op, [self] + cond.values)
48
+ # cond.values << self; cond
49
+ else
50
+ Condition.new.from_list(op, [self, cond])
51
+ end
52
+ end
53
+ # Encode into an array for Mapbox GL JSON (recursive)
54
+ def encode
55
+ [@operator.to_s, *@values.map { |v| v.is_a?(Condition) ? v.encode : v } ]
56
+ end
57
+ def to_s; "<Condition #{@operator} #{@values}>" end
58
+ end
59
+
60
+ # ----- Stylesheet
61
+ # the main document object
62
+
63
+ class Stylesheet
64
+ attr_accessor :sources, :kv, :refs
65
+
66
+ def initialize(&block)
67
+ @sources = {}
68
+ @kv = {}
69
+ @layers = []
70
+ @refs = {}
71
+ instance_eval(&block)
72
+ end
73
+
74
+ # Set a property, e.g. 'bearing 29'
75
+ def method_missing(method_sym, *arguments)
76
+ @kv[method_sym] = arguments[0]
77
+ end
78
+
79
+ # Add a source
80
+ def source(source_name, opts={})
81
+ @sources[source_name] = opts
82
+ end
83
+
84
+ # Add a layer
85
+ # creates a new Layer object using the block supplied
86
+ def layer(id, opts={}, &block)
87
+ r = Layer.new(self, :id=>id, :kv=>opts)
88
+ @layers << r
89
+ r.instance_eval(&block)
90
+ end
91
+
92
+ # Assemble into Mapbox GL JSON format
93
+ def to_hash
94
+ out = @kv.dup
95
+ out['sources'] = @sources
96
+ out['layers'] = @layers.select { |r| r.write? }.collect { |r| r.to_hash }
97
+ out
98
+ end
99
+ def to_json(*args); JSON.neat_generate(to_hash) end
100
+
101
+ # Setter for Layer to add sublayers
102
+ def _add_layer(layer)
103
+ @layers << layer
104
+ end
105
+ end
106
+
107
+ # ----- OSMKey
108
+ # enables us to write "population<30000" and have it magically converted into a Condition
109
+
110
+ class OSMKey
111
+ def initialize(k)
112
+ @k=k
113
+ end
114
+ def is(*args); Condition.new.from_key(:==,@k,args) end
115
+ def ==(*args); Condition.new.from_key(:==,@k,args) end
116
+ def !=(*args); Condition.new.from_key(:!=,@k,args) end
117
+ def <(*args); Condition.new.from_key(:< ,@k,args) end
118
+ def >(*args); Condition.new.from_key(:> ,@k,args) end
119
+ def <=(*args); Condition.new.from_key(:<=,@k,args) end
120
+ def >=(*args); Condition.new.from_key(:>=,@k,args) end
121
+ def in(*args); Condition.new.from_key(:in,@k,args) end
122
+ def not_in(*args); Condition.new.from_key(:not_in,@k,args) end
123
+ end
124
+
125
+ # ----- Layer
126
+ # a layer in an Mapbox GL style
127
+ # this is where most of the hard work happens, including 'method_missing' and 'on'
128
+ # calls to provide the grammar
129
+
130
+ class Layer
131
+
132
+ # Mapbox GL properties (as distinct from OSM keys)
133
+ LAYOUT = [ :visibility, :line_cap, :line_join, :line_miter_limit, :line_round_limit, :symbol_placement, :symbol_spacing, :symbol_avoid_edges, :icon_allow_overlap, :icon_ignore_placement, :icon_optional, :icon_rotation_alignment, :icon_size, :icon_image, :icon_rotate, :icon_padding, :icon_keep_upright, :icon_offset, :text_rotation_alignment, :text_field, :text_font, :text_size, :text_max_width, :text_line_height, :text_letter_spacing, :text_justify, :text_anchor, :text_max_angle, :text_rotate, :text_padding, :text_keep_upright, :text_transform, :text_offset, :text_allow_overlap, :text_ignore_placement, :text_optional ]
134
+ PAINT = [ :background_color, :background_pattern, :background_opacity, :fill_antialias, :fill_opacity, :fill_color, :fill_outline_color, :fill_translate, :fill_translate_anchor, :fill_pattern, :line_opacity, :line_color, :line_translate, :line_translate_anchor, :line_width, :line_gap_width, :line_blur, :line_dasharray, :line_pattern, :icon_opacity, :icon_color, :icon_halo_color, :icon_halo_width, :icon_halo_blur, :icon_translate, :icon_translate_anchor, :text_opacity, :text_color, :text_halo_color, :text_halo_width, :text_halo_blur, :text_translate, :text_translate_anchor, :raster_opacity, :raster_hue_rotate, :raster_brightness_min, :raster_brightness_max, :raster_saturation, :raster_contrast, :raster_fade_duration, :circle_radius, :circle_color, :circle_blur, :circle_opacity, :circle_translate, :circle_translate_anchor ]
135
+ TOP_LEVEL = [ :metadata, :zoom, :interactive ]
136
+ HIDDEN = [ :ref, :source, :source_layer, :id, :type, :filter, :layout, :paint ] # top level, not settable by commands
137
+
138
+ # Shared properties that can be recalled by using a 'ref'
139
+ REF_PROPERTIES = ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout']
140
+
141
+ attr_accessor :kv # key-value pairs for layout, paint, and top level
142
+ attr_accessor :condition # filter condition
143
+ attr_accessor :stylesheet # parent stylesheet object
144
+
145
+ def initialize(stylesheet, args={})
146
+ @stylesheet = stylesheet
147
+ @condition = args[:condition]
148
+ @kv = args[:kv] || {}
149
+ @kv[:id] = args[:id]
150
+ if args[:zoom] then @kv[:zoom]=args[:zoom] end
151
+
152
+ @type = nil # auto-detected layer type
153
+ @write = true # write this layer out, or has it been suppressed?
154
+ @cascade_cond = nil # are we currently evaluating a cascade directive?
155
+ @cascades = args[:cascades] || [] # cascade list to apply to all subsequent layers
156
+ @uncascaded = nil # condition to add to non-cascaded layers
157
+
158
+ @kv[:source] ||= stylesheet.sources.find {|k,v| v[:default] }[0]
159
+ @kv[:source_layer] ||= args[:id]
160
+ @child_num = 0 # incremented sublayer suffix
161
+ end
162
+
163
+ # Handle all missing 'method' calls
164
+ # If we can match it to a Mapbox GL property, it's an assignment:
165
+ # otherwise it's an OSM key
166
+ def method_missing(method_sym, *arguments)
167
+ if LAYOUT.include?(method_sym) || PAINT.include?(method_sym) || TOP_LEVEL.include?(method_sym)
168
+ v = arguments.length==1 ? arguments[0] : arguments
169
+ if v.is_a?(Proc) then v=v.call(@kv[method_sym]) end
170
+ if @cascade_cond.nil?
171
+ @kv[method_sym] = v
172
+ else
173
+ _add_cascade_condition(method_sym, v)
174
+ end
175
+ else
176
+ return OSMKey.new(method_sym.to_s)
177
+ end
178
+ end
179
+
180
+ # Add a sublayer with an additional filter
181
+ def on(*args, &block)
182
+ @child_num+=1
183
+ r = Layer.new(@stylesheet,
184
+ :id => "#{@kv[:id]}__#{@child_num}".to_sym,
185
+ :kv => @kv.dup, :cascades => @cascades.dup)
186
+
187
+ # Set zoom level
188
+ if args[0].is_a?(Range) || args[0].is_a?(Fixnum)
189
+ r.kv[:zoom] = args.shift
190
+ end
191
+
192
+ # Set condition
193
+ sub_cond = nil
194
+ if args.empty?
195
+ sub_cond = @condition # just inherit parent layer's condition
196
+ else
197
+ sub_cond = (args.length==1) ? args[0] : Condition.new.from_list(:any,args)
198
+ sub_cond = sub_cond & @condition
199
+ end
200
+ r._set_filter(sub_cond & @uncascaded)
201
+ r.instance_eval(&block)
202
+ @stylesheet._add_layer(r)
203
+
204
+ # Create cascaded layers
205
+ child_chr='a'
206
+ @cascades.each do |c|
207
+ c_cond, c_kv = c
208
+ l = Layer.new(@stylesheet, :id=>"#{r.kv[:id]}__#{child_chr}", :kv=>r.kv.dup)
209
+ l._set_filter(sub_cond & c_cond)
210
+ l.kv.merge!(c_kv)
211
+ @stylesheet._add_layer(l)
212
+ child_chr.next!
213
+ end
214
+ end
215
+
216
+ # Add a cascading condition
217
+ def cascade(*args, &block)
218
+ cond = (args.length==1) ? args[0] : Condition.new.from_list(:any,args)
219
+ @cascade_cond = cond
220
+ self.instance_eval(&block)
221
+ @cascade_cond = nil
222
+ end
223
+ def _add_cascade_condition(k, v)
224
+ if @cascades.length>0 && @cascades[-1][0].to_s==@cascade_cond.to_s
225
+ @cascades[-1][1][k]=v
226
+ else
227
+ @cascades << [@cascade_cond, { k=>v }]
228
+ end
229
+ end
230
+ def uncascaded(*args)
231
+ cond = case args.length
232
+ when 0; nil
233
+ when 1; args[0]
234
+ else; Condition.new.from_list(:any,args)
235
+ end
236
+ @uncascaded = cond
237
+ end
238
+
239
+ # Setters for @condition (making sure we copy when inheriting)
240
+ def filter(*args)
241
+ _set_filter(args.length==1 ? args[0] : Condition.new.from_list(:any,args))
242
+ end
243
+ def _set_filter(condition)
244
+ @condition = condition.dup
245
+ end
246
+
247
+ # Set layer name
248
+ def id(name)
249
+ @kv[:id] = name
250
+ end
251
+
252
+ # Suppress output of this layer
253
+ def suppress; @write = false end
254
+ def write?; @write end
255
+
256
+ # Square-bracket filters (any[...], all[...], none[...])
257
+ def any ; return Subscriptable.new(:any ) end
258
+ def all ; return Subscriptable.new(:all ) end
259
+ def none; return Subscriptable.new(:none) end
260
+
261
+ # Deduce 'type' attribute from style attributes
262
+ def set_type_from(s)
263
+ return unless s.include?('-')
264
+ t = s.split('-')[0].to_sym
265
+ if t==:icon || t==:text then t=:symbol end
266
+ if @type && @type!=t then raise "Attribute #{s} conflicts with deduced type #{@type} in layer #{@kv[:id]}" end
267
+ @type=t
268
+ end
269
+
270
+ # Create a Mapbox GL-format hash from a layer definition
271
+ def to_hash
272
+ hash = { :layout=> {}, :paint => {} }
273
+
274
+ # Assign key/values to correct place
275
+ @kv.each do |k,v|
276
+ s = k.to_s.gsub('_','-')
277
+ if s.include?('-color') && v.is_a?(Fixnum) then v = "#%06x" % v end
278
+
279
+ if LAYOUT.include?(k)
280
+ hash[:layout][s]=v
281
+ set_type_from s
282
+ elsif PAINT.include?(k)
283
+ hash[:paint][s]=v
284
+ set_type_from s
285
+ elsif TOP_LEVEL.include?(k) || HIDDEN.include?(k)
286
+ hash[s]=v
287
+ else raise "#{s} isn't a recognised layer attribute"
288
+ end
289
+ end
290
+
291
+ hash['type'] = @type
292
+ if @condition then hash['filter'] = @condition.encode end
293
+
294
+ # Convert zoom level
295
+ if (v=hash['zoom'])
296
+ hash['minzoom'] = v.is_a?(Range) ? v.first : v
297
+ hash['maxzoom'] = v.is_a?(Range) ? v.last : v
298
+ hash.delete('zoom')
299
+ end
300
+
301
+ # See if we can reuse an earlier layer's properties
302
+ mk = ref_key(hash)
303
+ if stylesheet.refs[mk]
304
+ REF_PROPERTIES.each { |k| hash.delete(k) }
305
+ hash['ref'] = stylesheet.refs[mk]
306
+ else
307
+ stylesheet.refs[mk] = hash['id']
308
+ end
309
+ hash
310
+ end
311
+
312
+ # Key to identify matching layer properties (slow but...)
313
+ def ref_key(hash)
314
+ (REF_PROPERTIES.collect { |k| hash[k] } ).to_json
315
+ end
316
+ end
317
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glug
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Richard Fairhurst
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: neatjson
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: Text-based markup for Mapbox GL styles
28
+ email: richard@systemeD.net
29
+ executables:
30
+ - glug
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/glug.rb
35
+ - bin/glug
36
+ homepage: http://github.com/systemed/glug
37
+ licenses:
38
+ - FTWPL
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 2.0.14
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: Glug
60
+ test_files: []