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_httpd_spatial).
14-include("couch_db.hrl").
15-include("couch_spatial.hrl").
16
17-export([handle_spatial_req/3, spatial_etag/3, spatial_etag/4,
18         load_index/3, handle_compact_req/3, handle_design_info_req/3,
19         handle_spatial_cleanup_req/2, parse_spatial_params/1,
20         make_spatial_fold_funs/6]).
21
22-import(couch_httpd,
23        [send_json/2, send_json/3, send_method_not_allowed/2, send_chunk/2,
24         start_json_response/2, start_json_response/3, end_json_response/1]).
25
26% Either answer a normal spatial query, or keep dispatching if the path part
27% after _spatial starts with an underscore.
28handle_spatial_req(#httpd{
29        path_parts=[_, _, _Dname, _, SpatialName|_]}=Req, Db, DDoc) ->
30    case SpatialName of
31    % the path after _spatial starts with an underscore => dispatch
32    <<$_,_/binary>> ->
33        dispatch_sub_spatial_req(Req, Db, DDoc);
34    _ ->
35        handle_spatial(Req, Db, DDoc)
36    end.
37
38% the dispatching of endpoints below _spatial needs to be done manually
39dispatch_sub_spatial_req(#httpd{
40        path_parts=[_, _, _DName, Spatial, SpatialDisp|_]}=Req,
41        Db, DDoc) ->
42    Conf = couch_config:get("httpd_design_handlers",
43        ?b2l(<<Spatial/binary, "/", SpatialDisp/binary>>)),
44    Fun = geocouch_duplicates:make_arity_3_fun(Conf),
45    apply(Fun, [Req, Db, DDoc]).
46
47handle_spatial(#httpd{method='GET',
48        path_parts=[_, _, DName, _, SpatialName]}=Req, Db, DDoc) ->
49    ?LOG_DEBUG("Spatial query (~p): ~n~p", [DName, DDoc#doc.id]),
50    #spatial_query_args{
51        stale = Stale
52    } = QueryArgs = parse_spatial_params(Req),
53    {ok, Index, Group} = couch_spatial:get_spatial_index(
54                           Db, DDoc#doc.id, SpatialName, Stale),
55    output_spatial_index(Req, Index, Group, Db, QueryArgs);
56handle_spatial(Req, _Db, _DDoc) ->
57    send_method_not_allowed(Req, "GET,HEAD").
58
59% pendant is in couch_httpd_db
60handle_compact_req(#httpd{method='POST',
61        path_parts=[DbName, _ , DName|_]}=Req, Db, _DDoc) ->
62    ok = couch_db:check_is_admin(Db),
63    couch_httpd:validate_ctype(Req, "application/json"),
64    ok = couch_spatial_compactor:start_compact(DbName, DName),
65    send_json(Req, 202, {[{ok, true}]});
66handle_compact_req(Req, _Db, _DDoc) ->
67    send_method_not_allowed(Req, "POST").
68
69% pendant is in couch_httpd_db
70handle_spatial_cleanup_req(#httpd{method='POST'}=Req, Db) ->
71    % delete unreferenced index files
72    ok = couch_db:check_is_admin(Db),
73    couch_httpd:validate_ctype(Req, "application/json"),
74    ok = couch_spatial:cleanup_index_files(Db),
75    send_json(Req, 202, {[{ok, true}]});
76
77handle_spatial_cleanup_req(Req, _Db) ->
78    send_method_not_allowed(Req, "POST").
79
80% pendant is in couch_httpd_db
81handle_design_info_req(#httpd{
82            method='GET',
83            path_parts=[_DbName, _Design, DesignName, _, _]
84        }=Req, Db, _DDoc) ->
85    DesignId = <<"_design/", DesignName/binary>>,
86    {ok, GroupInfoList} = couch_spatial:get_group_info(Db, DesignId),
87    send_json(Req, 200, {[
88        {name, DesignName},
89        {spatial_index, {GroupInfoList}}
90    ]});
91handle_design_info_req(Req, _Db, _DDoc) ->
92    send_method_not_allowed(Req, "GET").
93
94
95load_index(Req, Db, {DesignId, SpatialName}) ->
96    QueryArgs = parse_spatial_params(Req),
97    Stale = QueryArgs#spatial_query_args.stale,
98    case couch_spatial:get_spatial_index(Db, DesignId, SpatialName, Stale) of
99    {ok, Index, Group} ->
100          {ok, Index, Group, QueryArgs};
101    {not_found, Reason} ->
102        throw({not_found, Reason})
103    end.
104
105%output_spatial_index(Req, Index, Group, Db,
106%                     QueryArgs#spatial_query_args{count=true}) ->
107output_spatial_index(Req, Index, Group, _Db, QueryArgs) when
108        QueryArgs#spatial_query_args.count == true ->
109    Count = vtree:count_lookup(Group#spatial_group.fd,
110                               Index#spatial.treepos,
111                               QueryArgs#spatial_query_args.bbox),
112    send_json(Req, {[{"count",Count}]});
113
114% counterpart in couch_httpd_view is output_map_view/6
115output_spatial_index(Req, Index, Group, Db, QueryArgs) ->
116    #spatial_query_args{
117        bbox = Bbox,
118        bounds = Bounds,
119        limit = Limit,
120        skip = SkipCount
121    } = QueryArgs,
122    CurrentEtag = spatial_etag(Db, Group, Index),
123    HelperFuns = #spatial_fold_helper_funs{
124        start_response = fun json_spatial_start_resp/3,
125        send_row = fun send_json_spatial_row/3
126    },
127    couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
128        FoldFun = make_spatial_fold_funs(
129                    Req, QueryArgs, CurrentEtag, Db,
130                    Group#spatial_group.current_seq, HelperFuns),
131        FoldAccInit = {Limit, SkipCount, undefined, ""},
132        % In this case the accumulator consists of the response (which
133        % might be undefined) and the actual accumulator we only care
134        % about in spatiallist functions)
135        {ok, {_, _, Resp, _}} = couch_spatial:fold(
136            Index, FoldFun, FoldAccInit, Bbox, Bounds),
137        finish_spatial_fold(Req, Resp)
138    end).
139
140% counterpart in couch_httpd_view is make_view_fold/7
141make_spatial_fold_funs(Req, _QueryArgs, Etag, _Db, UpdateSeq, HelperFuns) ->
142    #spatial_fold_helper_funs{
143        start_response = StartRespFun,
144        send_row = SendRowFun
145    } = HelperFuns,
146    % The Acc is there to output characters that belong to the previous line,
147    % but only if one line follows (think of a comma separated list which
148    % doesn't have a comma at the last item)
149    fun({{Bbox, DocId}, {Geom, Value}}, {AccLimit, AccSkip, Resp, Acc}) ->
150        case {AccLimit, AccSkip, Resp} of
151        {0, _, _} ->
152            % we've done "limit" rows, stop foldling
153            {stop, {0, 0, Resp, Acc}};
154        {_, AccSkip, _} when AccSkip > 0 ->
155            % just keep skipping
156            {ok, {AccLimit, AccSkip - 1, Resp, Acc}};
157        {_, _, undefined} ->
158            % rendering the first row, first we start the response
159            {ok, Resp2, BeginBody} = StartRespFun(Req, Etag, UpdateSeq),
160            {Go, Acc2} = SendRowFun(
161                Resp2, {{Bbox, DocId}, {Geom, Value}}, BeginBody),
162            {Go, {AccLimit - 1, 0, Resp2, Acc2}};
163        {AccLimit, _, Resp} when (AccLimit > 0) ->
164            % rendering all other rows
165            {Go, Acc2} = SendRowFun(Resp, {{Bbox, DocId}, {Geom, Value}}, Acc),
166            {Go, {AccLimit - 1, 0, Resp, Acc2}}
167        end
168    end.
169
170% counterpart in couch_httpd_view is finish_view_fold/5
171finish_spatial_fold(Req, Resp) ->
172    case Resp of
173    % no response was sent yet
174    undefined ->
175        send_json(Req, 200, {[{"rows", []}]});
176    Resp ->
177        % end the index
178        send_chunk(Resp, "\r\n]}"),
179        end_json_response(Resp)
180    end.
181
182% counterpart in couch_httpd_view is json_view_start_resp/6
183json_spatial_start_resp(Req, Etag, UpdateSeq) ->
184    {ok, Resp} = start_json_response(Req, 200, [{"Etag", Etag}]),
185    BeginBody = io_lib:format(
186            "{\"update_seq\":~w,\"rows\":[\r\n", [UpdateSeq]),
187    {ok, Resp, BeginBody}.
188
189% counterpart in couch_httpd_view is send_json_view_row/5
190send_json_spatial_row(Resp, {{Bbox, DocId}, {Geom, Value}}, RowFront) ->
191    JsonObj = {[
192        {<<"id">>, DocId},
193        {<<"bbox">>, erlang:tuple_to_list(Bbox)},
194        {<<"geometry">>, couch_spatial_updater:geocouch_to_geojsongeom(Geom)},
195        {<<"value">>, Value}]},
196    send_chunk(Resp, RowFront ++  ?JSON_ENCODE(JsonObj)),
197    {ok, ",\r\n"}.
198
199% counterpart in couch_httpd_view is view_group_etag/3 resp. /4
200spatial_etag(Db, Group, Index) ->
201    spatial_etag(Db, Group, Index, nil).
202spatial_etag(_Db, #spatial_group{sig=Sig},
203        #spatial{update_seq=UpdateSeq, purge_seq=PurgeSeq}, Extra) ->
204    couch_httpd:make_etag({Sig, UpdateSeq, PurgeSeq, Extra}).
205
206parse_spatial_params(Req) ->
207    QueryList = couch_httpd:qs(Req),
208    QueryParams = lists:foldl(fun({K, V}, Acc) ->
209        parse_spatial_param(K, V) ++ Acc
210    end, [], QueryList),
211    QueryArgs = lists:foldl(fun({K, V}, Args2) ->
212        validate_spatial_query(K, V, Args2)
213    end, #spatial_query_args{}, lists:reverse(QueryParams)),
214
215    #spatial_query_args{
216        bbox = Bbox,
217        bounds = Bounds
218    } = QueryArgs,
219    case {Bbox, Bounds} of
220    % Coordinates of the bounding box are flipped and no bounds for the
221    % cartesian plane were set
222    {{W, S, E, N}, nil} when E < W; N < S ->
223        Msg = <<"Coordinates of the bounding box are flipped, but no bounds "
224                "for the cartesian plane were specified "
225                "(use the `plane_bounds` parameter)">>,
226        throw({query_parse_error, Msg});
227    _ ->
228        QueryArgs
229    end.
230
231parse_spatial_param("bbox", Bbox) ->
232    [{bbox, list_to_tuple(?JSON_DECODE("[" ++ Bbox ++ "]"))}];
233parse_spatial_param("stale", Value) ->
234    case string:to_lower(Value) of
235    "false" ->
236        [{stale, false}];
237    "ok" ->
238        [{stale, ok}];
239    "update_after" ->
240        [{stale, update_after}];
241    _ ->
242        throw({query_parse_error,
243            <<"stale only available as stale=ok, stale=update_after or "
244              "stale=false">>})
245    end;
246parse_spatial_param("count", "true") ->
247    [{count, true}];
248parse_spatial_param("count", _Value) ->
249    throw({query_parse_error, <<"count only available as count=true">>});
250parse_spatial_param("plane_bounds", Bounds) ->
251    [{bounds, list_to_tuple(?JSON_DECODE("[" ++ Bounds ++ "]"))}];
252parse_spatial_param("limit", Value) ->
253    [{limit, geocouch_duplicates:parse_positive_int_param(Value)}];
254parse_spatial_param("skip", Value) ->
255    [{skip, geocouch_duplicates:parse_int_param(Value)}];
256parse_spatial_param(Key, Value) ->
257    [{extra, {Key, Value}}].
258
259validate_spatial_query(bbox, Value, Args) ->
260    Args#spatial_query_args{bbox=Value};
261validate_spatial_query(stale, false, Args) ->
262    Args#spatial_query_args{stale=false};
263validate_spatial_query(stale, ok, Args) ->
264    Args#spatial_query_args{stale=ok};
265validate_spatial_query(stale, update_after, Args) ->
266    Args#spatial_query_args{stale=update_after};
267validate_spatial_query(count, true, Args) ->
268    Args#spatial_query_args{count=true};
269validate_spatial_query(bounds, Value, Args) ->
270    Args#spatial_query_args{bounds=Value};
271validate_spatial_query(limit, Value, Args) ->
272    Args#spatial_query_args{limit=Value};
273validate_spatial_query(skip, Value, Args) ->
274    Args#spatial_query_args{skip=Value};
275validate_spatial_query(extra, _Value, Args) ->
276    Args.
277