glug 0.0.5 → 0.0.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b05a6948c910e308dea8ebd778d54f9c78780040
4
- data.tar.gz: 5b8b0ee7600b6d5eea6e1e3dd036d00488fa7e0c
2
+ SHA256:
3
+ metadata.gz: 5340ccbd34dc2766a2f0ed580bcca372cb71cd35d1f6c33009003ca112cd1bc5
4
+ data.tar.gz: 1946ed3fc268779af7f929a1648893b83dcd4883f96be5ca4c9f2db3947c783c
5
5
  SHA512:
6
- metadata.gz: 34a2fb6fcbbd168477d8ada1cd77db7c9992b45f435aa1f0e91f997d229bba28e3ff8cc7793329321e2fe6ba8bf358bc9152ef75a6c1bdfd6741301ad776955a
7
- data.tar.gz: 7751afb79f71087aa6e6e7389ec485169aba0525767003c64e1898eea0f9c628421c8e5232af373dd4bb404f3d975dce887d23e17c2eec9ab3852ab8949ee40d
6
+ metadata.gz: 1591989f74b37a697e9ddf4dbd0345fcaddfe71d157e74a3825b383a1cfb5c7cf4bd14a1c3b12178b554f5493f34daaa432f38c62007e4002ddd8189bd8a25ee
7
+ data.tar.gz: 7c8b934f20ef7fe44d8d4aee3a46abd3a0c57f2d8ec86ff231f567cef08a22196e060541762b6704ba6e6ba01b1206d904bb1f79125b73965f56ed125309505c
@@ -0,0 +1,129 @@
1
+ module Glug # :nodoc:
2
+
3
+ # ----- Subscriptable
4
+ # allows us to create conditions with syntax
5
+ # any[(highway=='primary'),(highway=='trunk')]
6
+
7
+ class Subscriptable
8
+ def initialize(type)
9
+ @type=type
10
+ end
11
+ def [](*arguments)
12
+ Condition.new.from_list(@type, arguments)
13
+ end
14
+ end
15
+
16
+ # ----- Condition
17
+ # represents a GL filter of the form [operator, key, value] (etc.)
18
+ # can be merged with other conditions via the & and | operators
19
+
20
+ class Condition
21
+ attr_accessor :values, :operator
22
+
23
+ # GL operators we can't use verbatim (mostly Ruby reserved words)
24
+ SUBSTITUTIONS = {
25
+ string_format: "format",
26
+ is_in: "in",
27
+ case_when: "case",
28
+ _!: "!",
29
+ subtract: "-", divide: "/", pow: "^", # so we can write 'subtract(100,height)'
30
+ feature_id: "id" # Glug already uses 'id'
31
+ }
32
+
33
+ # GL operators that make sense to use as suffixed dot methods
34
+ DOT_METHODS = [
35
+ :array, :boolean, :string_format, :image, :number, :number_format, :object, :string,
36
+ :to_boolean, :to_color, :to_number, :to_string, :typeof,
37
+ :length, :slice, :match,
38
+ :downcase, :upcase, :is_supported_script, :to_rgba,
39
+ :abs, :acos, :asin, :atan, :ceil, :cos, :floor, :ln, :log10, :log2, :round, :sin, :sqrt, :tan
40
+ ]
41
+
42
+ def is(*args); Condition.new.from_key(:==, self, args) end
43
+ def ==(*args); Condition.new.from_key(:==, self, args) end
44
+ def !=(*args); Condition.new.from_key(:!=, self, args) end
45
+ def <(*args); Condition.new.from_key(:< , self, args) end
46
+ def >(*args); Condition.new.from_key(:> , self, args) end
47
+ def <=(*args); Condition.new.from_key(:<=, self, args) end
48
+ def >=(*args); Condition.new.from_key(:>=, self, args) end
49
+ def %(*args); Condition.new.from_key(:% , self, args) end
50
+ def +(*args); Condition.new.from_key(:+ , self, args) end
51
+ def -(*args); Condition.new.from_key(:- , self, args) end
52
+ def *(*args); Condition.new.from_key(:* , self, args) end
53
+ def /(*args); Condition.new.from_key(:/ , self, args) end
54
+ def **(*args); Condition.new.from_key(:^ , self, args) end
55
+ def in(*args); Condition.new.from_key(:in, self, [[:literal,args.flatten]]) end
56
+ def [](*args); Condition.new.from_key(:at, args[0], [self]) end
57
+ def coerce(other); [Condition.new.just_value(other), self] end
58
+
59
+ def initialize
60
+ @values=[]
61
+ end
62
+ def from_key(operator, key, list)
63
+ @operator = SUBSTITUTIONS[operator] || operator.to_s.gsub('_','-')
64
+ @values = [key].concat(list)
65
+ self
66
+ end
67
+ def from_list(operator, list)
68
+ @operator = SUBSTITUTIONS[operator] || operator.to_s.gsub('_','-')
69
+ @values = list
70
+ self
71
+ end
72
+ def just_value(val)
73
+ @operator = nil
74
+ @values = [val]
75
+ self
76
+ end
77
+
78
+ def &(cond); merge(:all,cond) end
79
+ def |(cond); merge(:any,cond) end
80
+ def merge(op,cond)
81
+ if cond.nil?
82
+ self
83
+ elsif @operator==op
84
+ Condition.new.from_list(op, @values + [cond])
85
+ elsif cond.operator==op
86
+ Condition.new.from_list(op, [self] + cond.values)
87
+ else
88
+ Condition.new.from_list(op, [self, cond])
89
+ end
90
+ end
91
+ def <<(cond); @values << cond.encode; self end
92
+
93
+ # Support dot access for most methods
94
+ def method_missing(method_sym, *args)
95
+ if DOT_METHODS.include?(method_sym)
96
+ Condition.new.from_key(method_sym, self, args)
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ # Encode into an array for GL JSON (recursive)
103
+ def encode
104
+ transform_underscores
105
+ values = @values.map { |v| v.is_a?(Condition) ? v.encode : v }
106
+ @operator.nil? ? values[0] : [@operator.to_s, *values]
107
+ end
108
+ def to_json(opts)
109
+ encode.to_json(opts)
110
+ end
111
+ def to_s
112
+ "<Condition #{@operator} #{@values}>"
113
+ end
114
+
115
+ # Transform nested { font_scale: 0.8 } to { "font-scale"=>0.8 }
116
+ def transform_underscores
117
+ @values.map! do |v|
118
+ if v.is_a?(Hash)
119
+ new_hash = {}
120
+ v.each { |hk,hv| new_hash[hk.is_a?(Symbol) ? hk.to_s.gsub('_','-') : hk] = hv }
121
+ new_hash
122
+ else
123
+ v
124
+ end
125
+ end
126
+ end
127
+
128
+ end # class Condition
129
+ end # module Glug
@@ -0,0 +1,27 @@
1
+ require 'chroma'
2
+ require 'hsluv'
3
+
4
+ # Colour methods on Integer
5
+
6
+ class Integer
7
+ def chroma_hex(op,p)
8
+ ("#"+to_s(16).rjust(6,'0')).paint.send(op,p).to_hex
9
+ end
10
+ def chroma(op,p)
11
+ chroma_hex(op,p).gsub('#','0x').to_i(16)
12
+ end
13
+ def to_hex_color
14
+ '#' + to_s(16).rjust(6,'0')
15
+ end
16
+ end
17
+
18
+ # Top-level colour generators
19
+
20
+ def hsluv(h,s,l)
21
+ arr = Hsluv.rgb_prepare(Hsluv.hsluv_to_rgb(h,s*100,l*100))
22
+ (arr[0]*256 + arr[1])*256 + arr[2]
23
+ end
24
+ def hsl(h,s,l)
25
+ rgb = Chroma::RgbGenerator::FromHslValues.new('hex6',h,s,l).generate[0]
26
+ ((rgb.r).to_i*256 + (rgb.g).to_i)*256 + rgb.b.to_i
27
+ end
data/lib/glug/layer.rb ADDED
@@ -0,0 +1,261 @@
1
+ module Glug # :nodoc:
2
+
3
+ # ----- Layer
4
+ # a layer in a GL style
5
+ # this is where most of the hard work happens, including 'method_missing' and 'on' calls to provide the grammar
6
+
7
+ class Layer
8
+
9
+ # GL properties (as distinct from OSM keys)
10
+ LAYOUT = [ :visibility,
11
+ :line_cap, :line_join, :line_miter_limit, :line_round_limit,
12
+ :symbol_placement, :symbol_spacing, :symbol_avoid_edges, :symbol_z_order,
13
+ :icon_allow_overlap, :icon_ignore_placement, :icon_optional, :icon_rotation_alignment, :icon_size,
14
+ :icon_image, :icon_rotate, :icon_padding, :icon_keep_upright, :icon_offset,
15
+ :icon_text_fit, :icon_text_fit_padding, :icon_anchor, :icon_pitch_alignment,
16
+ :text_rotation_alignment, :text_field, :text_font, :text_size, :text_max_width, :text_line_height,
17
+ :text_letter_spacing, :text_justify, :text_anchor, :text_max_angle, :text_rotate, :text_padding,
18
+ :text_keep_upright, :text_transform, :text_offset, :text_allow_overlap, :text_ignore_placement, :text_optional,
19
+ :text_pitch_alignment ]
20
+ PAINT = [ :background_color, :background_pattern, :background_opacity,
21
+ :fill_antialias, :fill_opacity, :fill_color, :fill_outline_color, :fill_translate, :fill_translate_anchor, :fill_pattern,
22
+ :line_opacity, :line_color, :line_translate, :line_translate_anchor, :line_width, :line_gap_width, :line_offset,
23
+ :line_blur, :line_dasharray, :line_pattern, :line_gradient,
24
+ :icon_opacity, :icon_color, :icon_halo_color, :icon_halo_width, :icon_halo_blur, :icon_translate, :icon_translate_anchor,
25
+ :text_opacity, :text_color, :text_halo_color, :text_halo_width, :text_halo_blur, :text_translate, :text_translate_anchor,
26
+ :raster_opacity, :raster_hue_rotate, :raster_brightness_min, :raster_brightness_max, :raster_saturation, :raster_contrast, :raster_resampling, :raster_fade_duration,
27
+ :circle_radius, :circle_color, :circle_blur, :circle_opacity, :circle_translate, :circle_translate_anchor,
28
+ :circle_pitch_scale, :circle_pitch_alignment, :circle_stroke_width, :circle_stroke_color, :circle_stroke_opacity,
29
+ :fill_extrusion_opacity, :fill_extrusion_color, :fill_extrusion_translate, :fill_extrusion_translate_anchor,
30
+ :fill_extrusion_pattern, :fill_extrusion_height, :fill_extrusion_base, :fill_extrusion_vertical_gradient,
31
+ :heatmap_radius, :heatmap_weight, :heatmap_intensity, :heatmap_color, :heatmap_opacity,
32
+ :hillshade_illumination_direction, :hillshade_illumination_anchor, :hillshade_exaggeration,
33
+ :hillshade_shadow_color, :hillshade_highlight_color, :hillshade_accent_color ]
34
+ TOP_LEVEL = [ :metadata, :zoom, :interactive ]
35
+ HIDDEN = [ :ref, :source, :source_layer, :id, :type, :filter, :layout, :paint ] # top level, not settable by commands
36
+ EXPRESSIONS=[ :array, :boolean, :collator, :string_format, :image, :literal, :number,
37
+ :number_format, :object, :string, :to_boolean, :to_color, :to_number, :to_string,
38
+ :typeof, :accumulated, :feature_state, :geometry_type, :feature_id,
39
+ :line_progress, :properties, :at, :get, :has, :is_in, :index_of,
40
+ :length, :slice,
41
+ :all, :any, :case_when, :coalesce, :match, :within,
42
+ :interpolate, :interpolate_hcl, :interpolate_lab, :step,
43
+ :let, :var, :concat, :downcase, :upcase,
44
+ :is_supported_script, :resolved_locale,
45
+ :rgb, :rgba, :to_rgba, :abs, :acos, :asin, :atan, :ceil, :cos, :distance,
46
+ :e, :floor, :ln, :ln2, :log10, :log2, :max, :min, :pi, :round, :sin, :sqrt, :tan,
47
+ :distance_from_center, :pitch, :zoom, :heatmap_density,
48
+ :subtract, :divide, :pow, :_! ]
49
+
50
+ # Shared properties that can be recalled by using a 'ref'
51
+ REF_PROPERTIES = ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout']
52
+
53
+ attr_accessor :kv # key-value pairs for layout, paint, and top level
54
+ attr_accessor :condition # filter condition
55
+ attr_accessor :stylesheet # parent stylesheet object
56
+
57
+ def initialize(stylesheet, args={})
58
+ @stylesheet = stylesheet
59
+ @condition = args[:condition]
60
+ @kv = args[:kv] || {}
61
+ @kv[:id] = args[:id]
62
+ if args[:zoom] then @kv[:zoom]=args[:zoom] end
63
+
64
+ @type = nil # auto-detected layer type
65
+ @write = true # write this layer out, or has it been suppressed?
66
+ @cascade_cond = nil # are we currently evaluating a cascade directive?
67
+ @cascades = args[:cascades] || [] # cascade list to apply to all subsequent layers
68
+ @uncascaded = nil # condition to add to non-cascaded layers
69
+
70
+ @kv[:source] ||= stylesheet.sources.find {|k,v| v[:default] }[0]
71
+ @kv[:source_layer] ||= args[:id] if stylesheet.sources[@kv[:source]][:type]=="vector"
72
+ @child_num = 0 # incremented sublayer suffix
73
+ end
74
+
75
+ # Handle all missing 'method' calls
76
+ # If we can match it to a GL property, it's an assignment:
77
+ # otherwise it's an OSM key
78
+ def method_missing(method_sym, *arguments)
79
+ if EXPRESSIONS.include?(method_sym)
80
+ return Condition.new.from_list(method_sym, arguments)
81
+ elsif LAYOUT.include?(method_sym) || PAINT.include?(method_sym) || TOP_LEVEL.include?(method_sym)
82
+ v = arguments.length==1 ? arguments[0] : arguments
83
+ if v.is_a?(Proc) then v=v.call(@kv[method_sym]) end
84
+ if @cascade_cond.nil?
85
+ @kv[method_sym] = v
86
+ else
87
+ _add_cascade_condition(method_sym, v)
88
+ end
89
+ else
90
+ return Condition.new.from_list("get", [method_sym])
91
+ end
92
+ end
93
+
94
+ # Convenience so we can write literal(1,2,3) rather than literal([1,2,3])
95
+ def literal(*args)
96
+ if args.length==1 && args[0].is_a?(Hash)
97
+ # Hashes - literal(frog: 1, bill: 2)
98
+ Condition.new.from_list(:literal, [args[0]])
99
+ else
100
+ # Arrays - literal(1,2,3)
101
+ Condition.new.from_list(:literal, [args])
102
+ end
103
+ end
104
+
105
+ # Return a current value from @kv
106
+ # This allows us to do: line_width current_value(:line_width)/2.0
107
+ def current_value(key)
108
+ @kv[key]
109
+ end
110
+
111
+ # Add a sublayer with an additional filter
112
+ def on(*args, &block)
113
+ @child_num+=1
114
+ r = Layer.new(@stylesheet,
115
+ :id => "#{@kv[:id]}__#{@child_num}".to_sym,
116
+ :kv => @kv.dup, :cascades => @cascades.dup)
117
+
118
+ # Set zoom level
119
+ if args[0].is_a?(Range) || args[0].is_a?(Integer)
120
+ r.kv[:zoom] = args.shift
121
+ end
122
+
123
+ # Set condition
124
+ sub_cond = nil
125
+ if args.empty?
126
+ sub_cond = @condition # just inherit parent layer's condition
127
+ else
128
+ sub_cond = (args.length==1) ? args[0] : Condition.new.from_list(:any,args)
129
+ sub_cond = nilsafe_merge(sub_cond, @condition)
130
+ end
131
+ r._set_filter(nilsafe_merge(sub_cond, @uncascaded))
132
+ r.instance_eval(&block)
133
+ @stylesheet._add_layer(r)
134
+
135
+ # Create cascaded layers
136
+ child_chr='a'
137
+ @cascades.each do |c|
138
+ c_cond, c_kv = c
139
+ l = Layer.new(@stylesheet, :id=>"#{r.kv[:id]}__#{child_chr}", :kv=>r.kv.dup)
140
+ l._set_filter(nilsafe_merge(sub_cond, c_cond))
141
+ l.kv.merge!(c_kv)
142
+ @stylesheet._add_layer(l)
143
+ child_chr.next!
144
+ end
145
+ end
146
+
147
+ # Nil-safe merge
148
+ def nilsafe_merge(a,b)
149
+ a.nil? ? b : (a & b)
150
+ end
151
+
152
+ # Add a cascading condition
153
+ def cascade(*args, &block)
154
+ cond = (args.length==1) ? args[0] : Condition.new.from_list(:any,args)
155
+ @cascade_cond = cond
156
+ self.instance_eval(&block)
157
+ @cascade_cond = nil
158
+ end
159
+ def _add_cascade_condition(k, v)
160
+ if @cascades.length>0 && @cascades[-1][0].to_s==@cascade_cond.to_s
161
+ @cascades[-1][1][k]=v
162
+ else
163
+ @cascades << [@cascade_cond, { k=>v }]
164
+ end
165
+ end
166
+ def uncascaded(*args)
167
+ cond = case args.length
168
+ when 0; nil
169
+ when 1; args[0]
170
+ else; Condition.new.from_list(:any,args)
171
+ end
172
+ @uncascaded = cond
173
+ end
174
+
175
+ # Setters for @condition (making sure we copy when inheriting)
176
+ def filter(*args)
177
+ _set_filter(args.length==1 ? args[0] : Condition.new.from_list(:any,args))
178
+ end
179
+ def _set_filter(condition)
180
+ @condition = condition.nil? ? nil : condition.dup
181
+ end
182
+
183
+ # Set layer name
184
+ def id(name)
185
+ @kv[:id] = name
186
+ end
187
+
188
+ # Suppress output of this layer
189
+ def suppress; @write = false end
190
+ def write?; @write end
191
+
192
+ # Square-bracket filters (any[...], all[...])
193
+ def any ; return Subscriptable.new(:any ) end
194
+ def all ; return Subscriptable.new(:all ) end
195
+
196
+ # Deduce 'type' attribute from style attributes
197
+ def set_type_from(s)
198
+ return unless s.include?('-')
199
+ t = (s=~/^fill-extrusion/ ? "fill-extrusion" : s.split('-')[0]).to_sym
200
+ if t==:icon || t==:text then t=:symbol end
201
+ if @type && @type!=t then raise "Attribute #{s} conflicts with deduced type #{@type} in layer #{@kv[:id]}" end
202
+ @type=t
203
+ end
204
+
205
+ # Create a GL-format hash from a layer definition
206
+ def to_hash
207
+ hash = { :layout=> {}, :paint => {} }
208
+
209
+ # Assign key/values to correct place
210
+ @kv.each do |k,v|
211
+ s = k.to_s.gsub('_','-')
212
+ if s.include?('-color') && v.is_a?(Integer) then v = "#%06x" % v end
213
+ if v.respond_to?(:encode) then v=v.encode end
214
+
215
+ if LAYOUT.include?(k)
216
+ hash[:layout][s]=v
217
+ set_type_from s
218
+ elsif PAINT.include?(k)
219
+ hash[:paint][s]=v
220
+ set_type_from s
221
+ elsif TOP_LEVEL.include?(k) || HIDDEN.include?(k)
222
+ hash[s]=v
223
+ else raise "#{s} isn't a recognised layer attribute"
224
+ end
225
+ end
226
+
227
+ hash['type'] = @type
228
+ if @condition then hash['filter'] = @condition.encode end
229
+
230
+ # Convert zoom level
231
+ if (v=hash['zoom'])
232
+ hash['minzoom'] = v.is_a?(Range) ? v.first : v
233
+ hash['maxzoom'] = v.is_a?(Range) ? v.last : v
234
+ hash.delete('zoom')
235
+ end
236
+
237
+ # See if we can reuse an earlier layer's properties
238
+ mk = ref_key(hash)
239
+ if stylesheet.refs[mk]
240
+ REF_PROPERTIES.each { |k| hash.delete(k) }
241
+ hash['ref'] = stylesheet.refs[mk]
242
+ else
243
+ stylesheet.refs[mk] = hash['id']
244
+ end
245
+
246
+ if hash[:layout].empty? && hash[:paint].empty?
247
+ nil
248
+ else
249
+ hash.delete(:layout) if hash[:layout].empty?
250
+ hash.delete(:paint) if hash[:paint].empty?
251
+ hash
252
+ end
253
+ end
254
+
255
+ # Key to identify matching layer properties (slow but...)
256
+ def ref_key(hash)
257
+ (REF_PROPERTIES.collect { |k| hash[k] } ).to_json
258
+ end
259
+
260
+ end # class Layer
261
+ end # module Glug
@@ -0,0 +1,57 @@
1
+ module Glug # :nodoc:
2
+
3
+ # ----- Stylesheet
4
+ # the main document object
5
+
6
+ class Stylesheet
7
+ attr_accessor :sources, :kv, :refs, :base_dir, :params
8
+
9
+ def initialize(base_dir: nil, params: nil, &block)
10
+ @sources = {}
11
+ @kv = {}
12
+ @layers = []
13
+ @refs = {}
14
+ @base_dir = base_dir || ''
15
+ @params = params || {}
16
+ instance_eval(&block)
17
+ end
18
+
19
+ # Set a property, e.g. 'bearing 29'
20
+ def method_missing(method_sym, *arguments)
21
+ @kv[method_sym] = arguments[0]
22
+ end
23
+
24
+ # Add a source
25
+ def source(source_name, opts={})
26
+ @sources[source_name] = opts
27
+ end
28
+
29
+ # Add a layer
30
+ # creates a new Layer object using the block supplied
31
+ def layer(id, opts={}, &block)
32
+ r = Layer.new(self, :id=>id, :kv=>opts)
33
+ @layers << r
34
+ r.instance_eval(&block)
35
+ end
36
+
37
+ # Assemble into GL JSON format
38
+ def to_hash
39
+ out = @kv.dup
40
+ out['sources'] = @sources.dup
41
+ out['sources'].each { |k,v| v.delete(:default); out['sources'][k] = v }
42
+ out['layers'] = @layers.select { |r| r.write? }.collect { |r| r.to_hash }.compact
43
+ out
44
+ end
45
+ def to_json(*args); JSON.neat_generate(to_hash) end
46
+
47
+ # Setter for Layer to add sublayers
48
+ def _add_layer(layer)
49
+ @layers << layer
50
+ end
51
+
52
+ # Load file
53
+ def include_file(fn)
54
+ instance_eval(File.read(File.join(@base_dir, fn)))
55
+ end
56
+ end
57
+ end
data/lib/glug.rb CHANGED
@@ -1,335 +1,10 @@
1
1
  require 'json'
2
2
  require 'neatjson'
3
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.dup
96
- out['sources'].each { |k,v| v.delete(:default); out['sources'][k] = v }
97
- out['layers'] = @layers.select { |r| r.write? }.collect { |r| r.to_hash }.compact
98
- out
99
- end
100
- def to_json(*args); JSON.neat_generate(to_hash) end
101
-
102
- # Setter for Layer to add sublayers
103
- def _add_layer(layer)
104
- @layers << layer
105
- end
106
- end
107
-
108
- # ----- OSMKey
109
- # enables us to write "population<30000" and have it magically converted into a Condition
110
-
111
- class OSMKey
112
- def initialize(k)
113
- @k=k
114
- end
115
- def is(*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 >=(*args); Condition.new.from_key(:>=,@k,args) end
122
- def in(*args); Condition.new.from_key(:in,@k,args) end
123
- def not_in(*args); Condition.new.from_key('!in',@k,args) end
124
- end
125
-
126
- # ----- Layer
127
- # a layer in an Mapbox GL style
128
- # this is where most of the hard work happens, including 'method_missing' and 'on'
129
- # calls to provide the grammar
130
-
131
- class Layer
132
-
133
- # Mapbox GL properties (as distinct from OSM keys)
134
- 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 ]
135
- 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 ]
136
- TOP_LEVEL = [ :metadata, :zoom, :interactive ]
137
- HIDDEN = [ :ref, :source, :source_layer, :id, :type, :filter, :layout, :paint ] # top level, not settable by commands
138
-
139
- # Shared properties that can be recalled by using a 'ref'
140
- REF_PROPERTIES = ['type', 'source', 'source-layer', 'minzoom', 'maxzoom', 'filter', 'layout']
141
-
142
- attr_accessor :kv # key-value pairs for layout, paint, and top level
143
- attr_accessor :condition # filter condition
144
- attr_accessor :stylesheet # parent stylesheet object
145
-
146
- def initialize(stylesheet, args={})
147
- @stylesheet = stylesheet
148
- @condition = args[:condition]
149
- @kv = args[:kv] || {}
150
- @kv[:id] = args[:id]
151
- if args[:zoom] then @kv[:zoom]=args[:zoom] end
152
-
153
- @type = nil # auto-detected layer type
154
- @write = true # write this layer out, or has it been suppressed?
155
- @cascade_cond = nil # are we currently evaluating a cascade directive?
156
- @cascades = args[:cascades] || [] # cascade list to apply to all subsequent layers
157
- @uncascaded = nil # condition to add to non-cascaded layers
158
-
159
- @kv[:source] ||= stylesheet.sources.find {|k,v| v[:default] }[0]
160
- @kv[:source_layer] ||= args[:id]
161
- @child_num = 0 # incremented sublayer suffix
162
- end
163
-
164
- # Handle all missing 'method' calls
165
- # If we can match it to a Mapbox GL property, it's an assignment:
166
- # otherwise it's an OSM key
167
- def method_missing(method_sym, *arguments)
168
- if LAYOUT.include?(method_sym) || PAINT.include?(method_sym) || TOP_LEVEL.include?(method_sym)
169
- v = arguments.length==1 ? arguments[0] : arguments
170
- if v.is_a?(Proc) then v=v.call(@kv[method_sym]) end
171
- if @cascade_cond.nil?
172
- @kv[method_sym] = v
173
- else
174
- _add_cascade_condition(method_sym, v)
175
- end
176
- else
177
- return OSMKey.new(method_sym.to_s)
178
- end
179
- end
180
-
181
- # Add a sublayer with an additional filter
182
- def on(*args, &block)
183
- @child_num+=1
184
- r = Layer.new(@stylesheet,
185
- :id => "#{@kv[:id]}__#{@child_num}".to_sym,
186
- :kv => @kv.dup, :cascades => @cascades.dup)
187
-
188
- # Set zoom level
189
- if args[0].is_a?(Range) || args[0].is_a?(Fixnum)
190
- r.kv[:zoom] = args.shift
191
- end
192
-
193
- # Set condition
194
- sub_cond = nil
195
- if args.empty?
196
- sub_cond = @condition # just inherit parent layer's condition
197
- else
198
- sub_cond = (args.length==1) ? args[0] : Condition.new.from_list(:any,args)
199
- sub_cond = nilsafe_merge(sub_cond, @condition)
200
- end
201
- r._set_filter(nilsafe_merge(sub_cond, @uncascaded))
202
- r.instance_eval(&block)
203
- @stylesheet._add_layer(r)
204
-
205
- # Create cascaded layers
206
- child_chr='a'
207
- @cascades.each do |c|
208
- c_cond, c_kv = c
209
- l = Layer.new(@stylesheet, :id=>"#{r.kv[:id]}__#{child_chr}", :kv=>r.kv.dup)
210
- l._set_filter(nilsafe_merge(sub_cond, c_cond))
211
- l.kv.merge!(c_kv)
212
- @stylesheet._add_layer(l)
213
- child_chr.next!
214
- end
215
- end
216
-
217
- # Short-form key constructor - for reserved words
218
- def tag(k)
219
- OSMKey.new(k)
220
- end
221
-
222
- # Nil-safe merge
223
- def nilsafe_merge(a,b)
224
- a.nil? ? b : (a & b)
225
- end
226
-
227
- # Add a cascading condition
228
- def cascade(*args, &block)
229
- cond = (args.length==1) ? args[0] : Condition.new.from_list(:any,args)
230
- @cascade_cond = cond
231
- self.instance_eval(&block)
232
- @cascade_cond = nil
233
- end
234
- def _add_cascade_condition(k, v)
235
- if @cascades.length>0 && @cascades[-1][0].to_s==@cascade_cond.to_s
236
- @cascades[-1][1][k]=v
237
- else
238
- @cascades << [@cascade_cond, { k=>v }]
239
- end
240
- end
241
- def uncascaded(*args)
242
- cond = case args.length
243
- when 0; nil
244
- when 1; args[0]
245
- else; Condition.new.from_list(:any,args)
246
- end
247
- @uncascaded = cond
248
- end
249
-
250
- # Setters for @condition (making sure we copy when inheriting)
251
- def filter(*args)
252
- _set_filter(args.length==1 ? args[0] : Condition.new.from_list(:any,args))
253
- end
254
- def _set_filter(condition)
255
- @condition = condition.nil? ? nil : condition.dup
256
- end
257
-
258
- # Set layer name
259
- def id(name)
260
- @kv[:id] = name
261
- end
262
-
263
- # Suppress output of this layer
264
- def suppress; @write = false end
265
- def write?; @write end
266
-
267
- # Square-bracket filters (any[...], all[...], none[...])
268
- def any ; return Subscriptable.new(:any ) end
269
- def all ; return Subscriptable.new(:all ) end
270
- def none; return Subscriptable.new(:none) end
271
-
272
- # Deduce 'type' attribute from style attributes
273
- def set_type_from(s)
274
- return unless s.include?('-')
275
- t = s.split('-')[0].to_sym
276
- if t==:icon || t==:text then t=:symbol end
277
- if @type && @type!=t then raise "Attribute #{s} conflicts with deduced type #{@type} in layer #{@kv[:id]}" end
278
- @type=t
279
- end
280
-
281
- # Create a Mapbox GL-format hash from a layer definition
282
- def to_hash
283
- hash = { :layout=> {}, :paint => {} }
284
-
285
- # Assign key/values to correct place
286
- @kv.each do |k,v|
287
- s = k.to_s.gsub('_','-')
288
- if s.include?('-color') && v.is_a?(Fixnum) then v = "#%06x" % v end
289
-
290
- if LAYOUT.include?(k)
291
- hash[:layout][s]=v
292
- set_type_from s
293
- elsif PAINT.include?(k)
294
- hash[:paint][s]=v
295
- set_type_from s
296
- elsif TOP_LEVEL.include?(k) || HIDDEN.include?(k)
297
- hash[s]=v
298
- else raise "#{s} isn't a recognised layer attribute"
299
- end
300
- end
301
-
302
- hash['type'] = @type
303
- if @condition then hash['filter'] = @condition.encode end
304
-
305
- # Convert zoom level
306
- if (v=hash['zoom'])
307
- hash['minzoom'] = v.is_a?(Range) ? v.first : v
308
- hash['maxzoom'] = v.is_a?(Range) ? v.last : v
309
- hash.delete('zoom')
310
- end
311
-
312
- # See if we can reuse an earlier layer's properties
313
- mk = ref_key(hash)
314
- if stylesheet.refs[mk]
315
- REF_PROPERTIES.each { |k| hash.delete(k) }
316
- hash['ref'] = stylesheet.refs[mk]
317
- else
318
- stylesheet.refs[mk] = hash['id']
319
- end
320
-
321
- if hash[:layout].empty? && hash[:paint].empty?
322
- nil
323
- else
324
- hash.delete(:layout) if hash[:layout].empty?
325
- hash.delete(:paint) if hash[:paint].empty?
326
- hash
327
- end
328
- end
329
-
330
- # Key to identify matching layer properties (slow but...)
331
- def ref_key(hash)
332
- (REF_PROPERTIES.collect { |k| hash[k] } ).to_json
333
- end
334
- end
4
+ module Glug
335
5
  end
6
+
7
+ require_relative 'glug/condition'
8
+ require_relative 'glug/stylesheet'
9
+ require_relative 'glug/layer'
10
+ require_relative 'glug/extensions'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glug
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Fairhurst
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-15 00:00:00.000000000 Z
11
+ date: 2022-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: neatjson
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: chroma
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hsluv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  description: Text-based markup for Mapbox GL styles
28
56
  email: richard@systemeD.net
29
57
  executables:
@@ -33,6 +61,10 @@ extra_rdoc_files: []
33
61
  files:
34
62
  - bin/glug
35
63
  - lib/glug.rb
64
+ - lib/glug/condition.rb
65
+ - lib/glug/extensions.rb
66
+ - lib/glug/layer.rb
67
+ - lib/glug/stylesheet.rb
36
68
  homepage: http://github.com/systemed/glug
37
69
  licenses:
38
70
  - FTWPL
@@ -52,8 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
84
  - !ruby/object:Gem::Version
53
85
  version: '0'
54
86
  requirements: []
55
- rubyforge_project:
56
- rubygems_version: 2.6.13
87
+ rubygems_version: 3.1.2
57
88
  signing_key:
58
89
  specification_version: 4
59
90
  summary: Glug