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, split_bbox_if_flipped/2]).
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        bounds = Bounds,
183        range = Range
184    } = Args,
185
186    % In case a 'bbox` is given, the value of `bbox` will be stored in `range`
187    % and `bbox` will be set to `nil` after validation.
188    Range2 = case {Bbox, Range} of
189    {nil, nil} ->
190        nil;
191    {Bbox, nil} ->
192        Bbox;
193    {nil, Range} ->
194        Range;
195    {Bbox, Range} ->
196        Msg = <<"A bounding box *and* a range were specified."
197                " Please use either of them.">>,
198        throw({query_parse_error, Msg})
199    end,
200
201    case Bbox =:= nil orelse
202        length(Bbox) == 2 of
203    true ->
204        case {Bbox, Bounds} of
205        % Coordinates of the bounding box are flipped and no bounds for the
206        % cartesian plane were set
207        {[{W, E}, {S, N}], nil} when E < W; N < S ->
208            Msg2 = <<"Coordinates of the bounding box are flipped, but no "
209                    "bounds for the cartesian plane were specified "
210                    "(use the `plane_bounds` parameter)">>,
211            parse_error(Msg2);
212        _ ->
213            ok
214        end;
215    false ->
216        parse_error(<<"`bbox` must have 2 dimensions.">>)
217    end,
218
219    case Bounds =:= nil orelse
220            length(Bounds) == 2 of
221        true -> ok;
222        false -> parse_error(<<"`plane_bounds` must have 2 dimensions.">>)
223    end,
224
225    case Args#spatial_args.limit > 0 of
226        true -> ok;
227        false -> parse_error(<<"`limit` must be a positive integer.">>)
228    end,
229
230    case Args#spatial_args.skip >= 0 of
231        true -> ok;
232        false -> parse_error(<<"`skip` must be >= 0.">>)
233    end,
234
235    Args#spatial_args{
236        bbox = nil,
237        range = Range2
238    }.
239
240
241parse_error(Msg) ->
242    throw({query_parse_error, Msg}).
243
244
245make_header(State) ->
246    #spatial_state{
247        update_seq = Seq,
248        purge_seq = PurgeSeq,
249        id_btree = IdBtree,
250        views = Views
251    } = State,
252    ViewStates = [
253        {
254            #vtree_state{
255                root = V#spatial.vtree#vtree.root,
256                kp_chunk_threshold = V#spatial.vtree#vtree.kp_chunk_threshold,
257                kv_chunk_threshold = V#spatial.vtree#vtree.kv_chunk_threshold,
258                min_fill_rate = V#spatial.vtree#vtree.min_fill_rate
259            },
260            V#spatial.update_seq,
261            V#spatial.purge_seq
262        } || V <- Views
263    ],
264    #spatial_header{
265        seq = Seq,
266        purge_seq = PurgeSeq,
267        id_btree_state = couch_btree:get_state(IdBtree),
268        view_states = ViewStates
269    }.
270
271
272index_file(DbName, Sig) ->
273    FileName = couch_index_util:hexsig(Sig) ++ ".spatial",
274    couch_index_util:index_file(spatial, DbName, FileName).
275
276
277compaction_file(DbName, Sig) ->
278    FileName = couch_index_util:hexsig(Sig) ++ ".compact.spatial",
279    couch_index_util:index_file(spatial, DbName, FileName).
280
281
282% This is a verbatim copy from couch_mrview_utils
283open_file(FName) ->
284    case couch_file:open(FName) of
285        {ok, Fd} -> {ok, Fd};
286        {error, enoent} -> couch_file:open(FName, [create]);
287        Error -> Error
288    end.
289
290
291% This is a verbatim copy from couch_mrview_utils
292delete_files(DbName, Sig) ->
293    delete_index_file(DbName, Sig),
294    delete_compaction_file(DbName, Sig).
295
296
297% This is a verbatim copy from couch_mrview_utils
298delete_index_file(DbName, Sig) ->
299    delete_file(index_file(DbName, Sig)).
300
301
302% This is a verbatim copy from couch_mrview_utils
303delete_compaction_file(DbName, Sig) ->
304    delete_file(compaction_file(DbName, Sig)).
305
306
307% This is a verbatim copy from couch_mrview_utils
308delete_file(FName) ->
309    case filelib:is_file(FName) of
310        true ->
311            RootDir = couch_index_util:root_dir(),
312            couch_file:delete(RootDir, FName);
313        _ ->
314            ok
315    end.
316
317
318reset_index(Db, Fd, State) ->
319    ok = couch_file:truncate(Fd, 0),
320    ok = couch_file:write_header(Fd, {State#spatial_state.sig, nil}),
321    init_state(Db, Fd, reset_state(State), nil).
322
323
324reset_state(State) ->
325    Views = [View#spatial{vtree = #vtree{}} || View <- State#spatial_state.views],
326    State#spatial_state{
327        fd = nil,
328        query_server = nil,
329        update_seq = 0,
330        id_btree = nil,
331        views = Views
332    }.
333
334
335expand_dups([], Acc) ->
336    lists:reverse(Acc);
337expand_dups([#kv_node{body={dups, Vals}}=Node | Rest], Acc) ->
338    Expanded = [Node#kv_node{body=Val} || Val <- Vals],
339    expand_dups(Rest, Expanded ++ Acc);
340expand_dups([KV | Rest], Acc) ->
341    expand_dups(Rest, [KV | Acc]).
342
343
344row_to_ejson({Mbb, DocId, Geom, Value}) ->
345    GeomData = case Geom of
346    <<>> ->
347        [];
348    _ ->
349        {ok, JsonGeom} = wkb_reader:wkb_to_geojson(Geom),
350        [{<<"geometry">>, JsonGeom}]
351    end,
352    {[
353        {<<"id">>, DocId},
354        {<<"key">>, [[Min, Max] || {Min, Max} <- Mbb]}
355    ] ++ GeomData ++ [{<<"value">>, Value}]}.
356
357
358split_bbox_if_flipped([{W, E}, {S, N}]=Bbox, [{BW, BE}, {BS, BN}]=_Bounds) ->
359    case bbox_is_flipped(Bbox) of
360    {flipped, Direction} ->
361        Bboxes = case Direction of
362        both ->
363            [[{W, BE}, {S, BN}], [{W, BE}, {BS, N}],
364                [{BW, E}, {S, BN}], [{BW, E}, {BS, N}]];
365        x ->
366            [[{W, BE}, {S, N}], [{BW, E}, {S, N}]];
367        y ->
368            [[{W, E}, {S, BN}], [{W, E}, {BS, N}]]
369        end,
370        % if boxes are still flipped, they are out of the bounds
371        lists:foldl(fun(B, Acc) ->
372           case bbox_is_flipped(B) of
373               {flipped, _} -> Acc;
374               not_flipped -> [B|Acc]
375           end
376        end, [], Bboxes);
377    not_flipped ->
378        [Bbox]
379    end.
380
381
382bbox_is_flipped([{W, E}, {S, N}]) when E < W, N < S ->
383    {flipped, both};
384bbox_is_flipped([{W, E}, _]) when E < W ->
385    {flipped, x};
386bbox_is_flipped([_, {S, N}]) when N < S ->
387    {flipped, y};
388bbox_is_flipped(_Bbox) ->
389    not_flipped.
390