1% Licensed under the Apache License, Version 2.0 (the "License"); you may not
2% use this file except in compliance with the License. You may obtain a copy of
3% the License at
4%
5%   http://www.apache.org/licenses/LICENSE-2.0
6%
7% Unless required by applicable law or agreed to in writing, software
8% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10% License for the specific language governing permissions and limitations under
11% the License.
12
13-module(couch_spatial_util).
14
15-export([get_view/4]).
16-export([ddoc_to_spatial_state/2, init_state/4, reset_index/3]).
17-export([make_header/1]).
18-export([index_file/2, compaction_file/2, open_file/1]).
19-export([delete_files/2, delete_index_file/2, delete_compaction_file/2]).
20% NOTE vmx 2012-10-19: get_row_count should really be removed as it's so
21%     unefficient
22-export([get_row_count/1]).
23-export([validate_args/1]).
24-export([expand_dups/2]).
25-export([row_to_ejson/1]).
26
27-include("couch_db.hrl").
28-include("couch_spatial.hrl").
29-include_lib("vtree/include/vtree.hrl").
30
31-define(MOD, couch_spatial_index).
32
33get_view(Db, DDoc, ViewName, Args) ->
34    ArgCheck = fun(_InitState) ->
35        {ok, validate_args(Args)}
36    end,
37    {ok, Pid, Args2} = couch_index_server:get_index(?MOD, Db, DDoc, ArgCheck),
38    DbUpdateSeq = couch_util:with_db(Db, fun(WDb) ->
39        couch_db:get_update_seq(WDb)
40    end),
41    MinSeq = case Args2#spatial_args.stale of
42        ok -> 0; update_after -> 0; _ -> DbUpdateSeq
43    end,
44    {ok, State} = case couch_index:get_state(Pid, MinSeq) of
45        {ok, _} = Resp -> Resp;
46        Error -> throw(Error)
47    end,
48    couch_ref_counter:add(State#spatial_state.ref_counter),
49    if Args2#spatial_args.stale == update_after ->
50        spawn(fun() -> catch couch_index:get_state(Pid, DbUpdateSeq) end);
51        true -> ok
52    end,
53    {ok, View} = extract_view(ViewName, State#spatial_state.views),
54    Sig = view_sig(State, View, Args2),
55    {ok, View, Sig, Args2}.
56
57
58ddoc_to_spatial_state(DbName, DDoc) ->
59    #doc{id=Id, body={Fields}} = DDoc,
60    Language = couch_util:get_value(<<"language">>, Fields, <<"javascript">>),
61    {DesignOpts} = couch_util:get_value(<<"options">>, Fields, {[]}),
62    {RawIndexes} = couch_util:get_value(<<"spatial">>, Fields, {[]}),
63    % RawViews is only needed to get the "lib" property
64    {RawViews} = couch_util:get_value(<<"views">>, Fields, {[]}),
65    Lib = couch_util:get_value(<<"lib">>, RawViews, {[]}),
66
67    % add the views to a dictionary object, with the map source as the key
68    DictBySrc =
69    lists:foldl(fun({Name, IndexSrc}, DictBySrcAcc) ->
70        Index =
71        case dict:find({IndexSrc}, DictBySrcAcc) of
72            {ok, Index0} -> Index0;
73            error -> #spatial{def=IndexSrc} % create new spatial index object
74        end,
75        Index2 = Index#spatial{view_names=[Name|Index#spatial.view_names]},
76        dict:store({IndexSrc}, Index2, DictBySrcAcc)
77    end, dict:new(), RawIndexes),
78    % number the views
79    {Indexes, _N} = lists:mapfoldl(
80        fun({_Src, Index}, N) ->
81            {Index#spatial{id_num=N},N+1}
82        end, 0, lists:sort(dict:to_list(DictBySrc))),
83
84    IdxState = #spatial_state{
85        db_name = DbName,
86        idx_name = Id,
87        lib = Lib,
88        views = Indexes,
89        language = Language,
90        design_options = DesignOpts
91    },
92    SigInfo = {Indexes, Language, DesignOpts, couch_index_util:sort_lib(Lib)},
93    {ok, IdxState#spatial_state{sig=couch_util:md5(term_to_binary(SigInfo))}}.
94
95
96extract_view(_Name, []) ->
97    {not_found, missing_named_view};
98extract_view(Name, [#spatial{view_names=ViewNames}=View|Rest]) ->
99    case lists:member(Name, ViewNames) of
100        true -> {ok, View};
101        false -> extract_view(Name, Rest)
102    end.
103
104
105view_sig(State, View, Args0) ->
106    Sig = State#spatial_state.sig,
107    #spatial{
108        update_seq = UpdateSeq,
109        purge_seq = PurgeSeq
110    } = View,
111    Args = Args0#spatial_args{
112        preflight_fun=undefined,
113        extra=[]
114    },
115    Bin = term_to_binary({Sig, UpdateSeq, PurgeSeq, Args}),
116    couch_index_util:hexsig(couch_util:md5(Bin)).
117
118
119init_state(Db, Fd, State, nil) ->
120    Header = #spatial_header{
121        seq = 0,
122        purge_seq = couch_db:get_purge_seq(Db),
123        id_btree_state = nil,
124        view_states = [
125            % vtree state, update_seq, purge_seq
126            {#vtree_state{}, 0, 0} || _ <- State#spatial_state.views]
127    },
128    init_state(Db, Fd, State, Header);
129init_state(Db, Fd, State, Header) ->
130    #spatial_header{
131        seq = Seq,
132        purge_seq = PurgeSeq,
133        id_btree_state = IdBtreeState,
134        view_states = ViewStates
135    } = Header,
136
137    % There's currently only one less function supported, hence we can
138    % hard-code it
139    Less = fun(A, B) -> A < B end,
140
141    Views = lists:zipwith(
142       fun({VtreeState, UpdateSeq, PurgeSeq2}, View) ->
143            Vtree = #vtree{
144                root = VtreeState#vtree_state.root,
145                kp_chunk_threshold = VtreeState#vtree_state.kp_chunk_threshold,
146                kv_chunk_threshold = VtreeState#vtree_state.kv_chunk_threshold,
147                min_fill_rate = VtreeState#vtree_state.min_fill_rate,
148                less = Less,
149                fd = Fd
150            },
151            View#spatial{
152                vtree = Vtree,
153                update_seq = UpdateSeq,
154                purge_seq = PurgeSeq2,
155                fd = Fd
156            }
157        end,
158        ViewStates, State#spatial_state.views),
159
160    {ok, IdBtree} = couch_btree:open(
161        IdBtreeState, Fd, [{compression, couch_db:compression(Db)}]),
162
163    State#spatial_state{
164        fd = Fd,
165        update_seq = Seq,
166        purge_seq = PurgeSeq,
167        id_btree = IdBtree,
168        views = Views
169    }.
170
171
172get_row_count(View) ->
173    Count = vtree_search:count_all(View#spatial.vtree),
174    {ok, Count}.
175
176
177validate_args(Args) ->
178    % `stale` and `count` got already validated during parsing
179
180    #spatial_args{
181        bbox = Bbox,
182        range = Range
183    } = Args,
184
185    % In case a 'bbox` is given, the value of `bbox` will be stored in `range`
186    % and `bbox` will be set to `nil` after validation.
187    Range2 = case {Bbox, Range} of
188    {nil, nil} ->
189        nil;
190    {Bbox, nil} ->
191        Bbox;
192    {nil, Range} ->
193        Range;
194    {Bbox, Range} ->
195        Msg = <<"A bounding box *and* a range were specified."
196                " Please use either of them.">>,
197        throw({query_parse_error, Msg})
198    end,
199
200    case Bbox =:= nil orelse
201        length(Bbox) == 2 of
202    true ->
203        ok;
204    false ->
205        parse_error(<<"`bbox` must have 2 dimensions.">>)
206    end,
207
208    case Args#spatial_args.limit > 0 of
209        true -> ok;
210        false -> parse_error(<<"`limit` must be a positive integer.">>)
211    end,
212
213    case Args#spatial_args.skip >= 0 of
214        true -> ok;
215        false -> parse_error(<<"`skip` must be >= 0.">>)
216    end,
217
218    Args#spatial_args{
219        bbox = nil,
220        range = Range2
221    }.
222
223
224parse_error(Msg) ->
225    throw({query_parse_error, Msg}).
226
227
228make_header(State) ->
229    #spatial_state{
230        update_seq = Seq,
231        purge_seq = PurgeSeq,
232        id_btree = IdBtree,
233        views = Views
234    } = State,
235    ViewStates = [
236        {
237            #vtree_state{
238                root = V#spatial.vtree#vtree.root,
239                kp_chunk_threshold = V#spatial.vtree#vtree.kp_chunk_threshold,
240                kv_chunk_threshold = V#spatial.vtree#vtree.kv_chunk_threshold,
241                min_fill_rate = V#spatial.vtree#vtree.min_fill_rate
242            },
243            V#spatial.update_seq,
244            V#spatial.purge_seq
245        } || V <- Views
246    ],
247    #spatial_header{
248        seq = Seq,
249        purge_seq = PurgeSeq,
250        id_btree_state = couch_btree:get_state(IdBtree),
251        view_states = ViewStates
252    }.
253
254
255index_file(DbName, Sig) ->
256    FileName = couch_index_util:hexsig(Sig) ++ ".spatial",
257    couch_index_util:index_file(spatial, DbName, FileName).
258
259
260compaction_file(DbName, Sig) ->
261    FileName = couch_index_util:hexsig(Sig) ++ ".compact.spatial",
262    couch_index_util:index_file(spatial, DbName, FileName).
263
264
265% This is a verbatim copy from couch_mrview_utils
266open_file(FName) ->
267    case couch_file:open(FName) of
268        {ok, Fd} -> {ok, Fd};
269        {error, enoent} -> couch_file:open(FName, [create]);
270        Error -> Error
271    end.
272
273
274% This is a verbatim copy from couch_mrview_utils
275delete_files(DbName, Sig) ->
276    delete_index_file(DbName, Sig),
277    delete_compaction_file(DbName, Sig).
278
279
280% This is a verbatim copy from couch_mrview_utils
281delete_index_file(DbName, Sig) ->
282    delete_file(index_file(DbName, Sig)).
283
284
285% This is a verbatim copy from couch_mrview_utils
286delete_compaction_file(DbName, Sig) ->
287    delete_file(compaction_file(DbName, Sig)).
288
289
290% This is a verbatim copy from couch_mrview_utils
291delete_file(FName) ->
292    case filelib:is_file(FName) of
293        true ->
294            RootDir = couch_index_util:root_dir(),
295            couch_file:delete(RootDir, FName);
296        _ ->
297            ok
298    end.
299
300
301reset_index(Db, Fd, State) ->
302    ok = couch_file:truncate(Fd, 0),
303    ok = couch_file:write_header(Fd, {State#spatial_state.sig, nil}),
304    init_state(Db, Fd, reset_state(State), nil).
305
306
307reset_state(State) ->
308    Views = [View#spatial{vtree = #vtree{}} || View <- State#spatial_state.views],
309    State#spatial_state{
310        fd = nil,
311        query_server = nil,
312        update_seq = 0,
313        id_btree = nil,
314        views = Views
315    }.
316
317
318expand_dups([], Acc) ->
319    lists:reverse(Acc);
320expand_dups([#kv_node{body={dups, Vals}}=Node | Rest], Acc) ->
321    Expanded = [Node#kv_node{body=Val} || Val <- Vals],
322    expand_dups(Rest, Expanded ++ Acc);
323expand_dups([KV | Rest], Acc) ->
324    expand_dups(Rest, [KV | Acc]).
325
326
327row_to_ejson({Mbb, DocId, Geom, Value}) ->
328    GeomData = case Geom of
329    <<>> ->
330        [];
331    _ ->
332        {ok, JsonGeom} = wkb_reader:wkb_to_geojson(Geom),
333        [{<<"geometry">>, JsonGeom}]
334    end,
335    {[
336        {<<"id">>, DocId},
337        {<<"key">>, [[Min, Max] || {Min, Max} <- Mbb]}
338    ] ++ GeomData ++ [{<<"value">>, Value}]}.
339