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).
14
15-export([query_view/6, query_view_count/4]).
16-export([get_info/2]).
17-export([compact/2, compact/3, cancel_compaction/2]).
18-export([cleanup/1]).
19
20-include("couch_db.hrl").
21-include("couch_spatial.hrl").
22-include_lib("vtree/include/vtree.hrl").
23
24-record(acc, {
25    meta_sent = false,
26    offset,
27    limit,
28    skip,
29    callback,
30    user_acc,
31    last_go = ok,
32    update_seq = 0
33}).
34
35
36query_view(Db, DDoc, ViewName, Args, Callback, Acc0) ->
37    {ok, View, Sig, Args2} = couch_spatial_util:get_view(
38        Db, DDoc, ViewName, Args),
39    {ok, Acc} = case Args#spatial_args.preflight_fun of
40        PFFun when is_function(PFFun, 2) -> PFFun(Sig, Acc0);
41        _ -> {ok, Acc0}
42    end,
43    spatial_fold(View, Args2, Callback, Acc).
44
45
46query_view_count(Db, DDoc, ViewName, Args) ->
47    {ok, View, _, Args2} = couch_spatial_util:get_view(
48        Db, DDoc, ViewName, Args),
49
50    case Args2#spatial_args.range of
51    nil ->
52        vtree_search:count_all(View#spatial.vtree);
53    Mbb ->
54        vtree_search:count_search(View#spatial.vtree, [Mbb])
55    end.
56
57
58get_info(Db, DDoc) ->
59    {ok, Pid} = couch_index_server:get_index(couch_spatial_index, Db, DDoc),
60    couch_index:get_info(Pid).
61
62
63compact(Db, DDoc) ->
64    compact(Db, DDoc, []).
65
66
67compact(Db, DDoc, Opts) ->
68    {ok, Pid} = couch_index_server:get_index(couch_spatial_index, Db, DDoc),
69    couch_index:compact(Pid, Opts).
70
71
72cancel_compaction(Db, DDoc) ->
73    {ok, IPid} = couch_index_server:get_index(couch_spatial_index, Db, DDoc),
74    {ok, CPid} = couch_index:get_compactor_pid(IPid),
75    ok = couch_index_compactor:cancel(CPid),
76
77    % Cleanup the compaction file if it exists
78    {ok, State} = couch_index:get_state(IPid, 0),
79    #spatial_state{
80        sig = Sig,
81        db_name = DbName
82    } = State,
83    couch_spatial_util:delete_compaction_file(DbName, Sig),
84    ok.
85
86
87cleanup(Db) ->
88    couch_spatial_cleanup:run(Db).
89
90
91spatial_fold(View, Args, Callback, UserAcc) ->
92    % TODO vmx 2016-06-10: Remove `bounds` completely
93    #spatial_args{
94        limit = Limit,
95        skip = Skip,
96        bounds = Bounds,
97        range = Range
98    } = Args,
99    Acc = #acc{
100        limit = Limit,
101        skip = Skip,
102        callback = Callback,
103        user_acc = UserAcc,
104        update_seq = View#spatial.update_seq
105    },
106    Acc2 = fold(View, fun do_fold/2, Acc, Range, Bounds),
107    finish_fold(Acc2, []).
108
109
110fold(Index, FoldFun, InitAcc, Range, _Bounds) ->
111    #spatial{
112       vtree = #vtree{
113           root = Root
114       } = Vt
115    } = Index,
116
117    WrapperFun = fun(Node, Acc) ->
118        % NOTE vmx 2012-11-28: in Apache CouchDB the body is stored as
119        %     Erlang terms
120        Value = binary_to_term(Node#kv_node.body),
121        Expanded = couch_spatial_util:expand_dups(
122            [Node#kv_node{body=Value}], []),
123        fold_fun(FoldFun, Expanded, Acc)
124    end,
125
126    case Root of
127    nil ->
128        InitAcc;
129    % Use the original MBB for comparison as the key is not necessarily
130    % set. The original MBB is good enough for this check as it will have
131    % the same dimesionality.
132    #kp_node{mbb_orig = MbbOrig} ->
133        case Range of
134        [] ->
135            vtree_search:all(Index#spatial.vtree, WrapperFun, InitAcc);
136        _ when length(Range) =/= length(MbbOrig) ->
137            throw(list_to_binary(io_lib:format(
138                "The query range must have the same dimensionality as "
139                "the index. Your range was `~10000p`, but the index has a "
140                "dimensionality of `~p`.", [Range, length(MbbOrig)])));
141        Range ->
142            vtree_search:search(Vt, [Range], WrapperFun, InitAcc)
143        end
144    end.
145
146
147% This is like a normal fold that can be interrupted in the middle
148fold_fun(_Fun, [], Acc) ->
149    {ok, Acc};
150fold_fun(Fun, [KV|Rest], Acc) ->
151    case Fun(KV, Acc) of
152    {ok, Acc2} ->
153        fold_fun(Fun, Rest, Acc2);
154    {stop, Acc2} ->
155        {stop, Acc2}
156    end.
157
158do_fold(_Kv, #acc{skip=N}=Acc) when N > 0 ->
159    {ok, Acc#acc{skip=N-1, last_go=ok}};
160do_fold(Kv, #acc{meta_sent=false}=Acc) ->
161    #acc{
162        callback = Callback,
163        user_acc = UserAcc,
164        update_seq = UpdateSeq
165    } = Acc,
166    Meta = make_meta(UpdateSeq, []),
167    {Go, UserAcc2} = Callback(Meta, UserAcc),
168    Acc2 = Acc#acc{meta_sent=true, user_acc=UserAcc2, last_go=Go},
169    case Go of
170        ok -> do_fold(Kv, Acc2);
171        stop -> {stop, Acc2}
172    end;
173do_fold(_Kv, #acc{limit=0}=Acc) ->
174    {stop, Acc};
175do_fold(#kv_node{}=Node, Acc) ->
176    #kv_node{
177        key = Mbb,
178        docid = DocId,
179        geometry = Geom,
180        body = Value
181    } = Node,
182    #acc{
183        limit = Limit,
184        callback = Callback,
185        user_acc = UserAcc
186    } = Acc,
187    Row = {Mbb, DocId, Geom, Value},
188    {Go, UserAcc2} = Callback({row, Row}, UserAcc),
189    {Go, Acc#acc{
190        limit = Limit-1,
191        user_acc = UserAcc2,
192        last_go = Go
193    }}.
194
195
196finish_fold(#acc{last_go=ok}=Acc, ExtraMeta) ->
197    #acc{
198        callback = Callback,
199        user_acc = UserAcc,
200        update_seq = UpdateSeq,
201        meta_sent = MetaSent
202    }=Acc,
203    % Possible send meta info
204    Meta = make_meta(UpdateSeq, ExtraMeta),
205    {Go, UserAcc1} = case MetaSent of
206        false -> Callback(Meta, UserAcc);
207        _ -> {ok, UserAcc}
208    end,
209    % Notify callback that the fold is complete.
210    {_, UserAcc2} = case Go of
211        ok -> Callback(complete, UserAcc1);
212        _ -> {ok, UserAcc1}
213    end,
214    {ok, UserAcc2};
215finish_fold(Acc, _ExtraMeta) ->
216    {ok, Acc#acc.user_acc}.
217
218
219make_meta(UpdateSeq, Base) ->
220    {meta, Base ++ [{update_seq, UpdateSeq}]}.
221