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 +5 -5
- data/lib/glug/condition.rb +129 -0
- data/lib/glug/extensions.rb +27 -0
- data/lib/glug/layer.rb +261 -0
- data/lib/glug/stylesheet.rb +57 -0
- data/lib/glug.rb +6 -331
- metadata +35 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5340ccbd34dc2766a2f0ed580bcca372cb71cd35d1f6c33009003ca112cd1bc5
|
4
|
+
data.tar.gz: 1946ed3fc268779af7f929a1648893b83dcd4883f96be5ca4c9f2db3947c783c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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:
|
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
|
-
|
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
|