1/*
2 * Copyright (C) 2008-2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.gesture;
18
19import android.util.Log;
20import android.os.SystemClock;
21
22import java.io.BufferedInputStream;
23import java.io.BufferedOutputStream;
24import java.io.IOException;
25import java.io.DataOutputStream;
26import java.io.DataInputStream;
27import java.io.InputStream;
28import java.io.OutputStream;
29import java.util.ArrayList;
30import java.util.HashMap;
31import java.util.Set;
32import java.util.Map;
33
34import static android.gesture.GestureConstants.LOG_TAG;
35
36/**
37 * GestureLibrary maintains gesture examples and makes predictions on a new
38 * gesture
39 */
40//
41//    File format for GestureStore:
42//
43//                Nb. bytes   Java type   Description
44//                -----------------------------------
45//    Header
46//                2 bytes     short       File format version number
47//                4 bytes     int         Number of entries
48//    Entry
49//                X bytes     UTF String  Entry name
50//                4 bytes     int         Number of gestures
51//    Gesture
52//                8 bytes     long        Gesture ID
53//                4 bytes     int         Number of strokes
54//    Stroke
55//                4 bytes     int         Number of points
56//    Point
57//                4 bytes     float       X coordinate of the point
58//                4 bytes     float       Y coordinate of the point
59//                8 bytes     long        Time stamp
60//
61public class GestureStore {
62    public static final int SEQUENCE_INVARIANT = 1;
63    // when SEQUENCE_SENSITIVE is used, only single stroke gestures are currently allowed
64    public static final int SEQUENCE_SENSITIVE = 2;
65
66    // ORIENTATION_SENSITIVE and ORIENTATION_INVARIANT are only for SEQUENCE_SENSITIVE gestures
67    public static final int ORIENTATION_INVARIANT = 1;
68    // at most 2 directions can be recognized
69    public static final int ORIENTATION_SENSITIVE = 2;
70    // at most 4 directions can be recognized
71    static final int ORIENTATION_SENSITIVE_4 = 4;
72    // at most 8 directions can be recognized
73    static final int ORIENTATION_SENSITIVE_8 = 8;
74
75    private static final short FILE_FORMAT_VERSION = 1;
76
77    private static final boolean PROFILE_LOADING_SAVING = false;
78
79    private int mSequenceType = SEQUENCE_SENSITIVE;
80    private int mOrientationStyle = ORIENTATION_SENSITIVE;
81
82    private final HashMap<String, ArrayList<Gesture>> mNamedGestures =
83            new HashMap<String, ArrayList<Gesture>>();
84
85    private Learner mClassifier;
86
87    private boolean mChanged = false;
88
89    public GestureStore() {
90        mClassifier = new InstanceLearner();
91    }
92
93    /**
94     * Specify how the gesture library will handle orientation.
95     * Use ORIENTATION_INVARIANT or ORIENTATION_SENSITIVE
96     *
97     * @param style
98     */
99    public void setOrientationStyle(int style) {
100        mOrientationStyle = style;
101    }
102
103    public int getOrientationStyle() {
104        return mOrientationStyle;
105    }
106
107    /**
108     * @param type SEQUENCE_INVARIANT or SEQUENCE_SENSITIVE
109     */
110    public void setSequenceType(int type) {
111        mSequenceType = type;
112    }
113
114    /**
115     * @return SEQUENCE_INVARIANT or SEQUENCE_SENSITIVE
116     */
117    public int getSequenceType() {
118        return mSequenceType;
119    }
120
121    /**
122     * Get all the gesture entry names in the library
123     *
124     * @return a set of strings
125     */
126    public Set<String> getGestureEntries() {
127        return mNamedGestures.keySet();
128    }
129
130    /**
131     * Recognize a gesture
132     *
133     * @param gesture the query
134     * @return a list of predictions of possible entries for a given gesture
135     */
136    public ArrayList<Prediction> recognize(Gesture gesture) {
137        Instance instance = Instance.createInstance(mSequenceType,
138                mOrientationStyle, gesture, null);
139        return mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector);
140    }
141
142    /**
143     * Add a gesture for the entry
144     *
145     * @param entryName entry name
146     * @param gesture
147     */
148    public void addGesture(String entryName, Gesture gesture) {
149        if (entryName == null || entryName.length() == 0) {
150            return;
151        }
152        ArrayList<Gesture> gestures = mNamedGestures.get(entryName);
153        if (gestures == null) {
154            gestures = new ArrayList<Gesture>();
155            mNamedGestures.put(entryName, gestures);
156        }
157        gestures.add(gesture);
158        mClassifier.addInstance(
159                Instance.createInstance(mSequenceType, mOrientationStyle, gesture, entryName));
160        mChanged = true;
161    }
162
163    /**
164     * Remove a gesture from the library. If there are no more gestures for the
165     * given entry, the gesture entry will be removed.
166     *
167     * @param entryName entry name
168     * @param gesture
169     */
170    public void removeGesture(String entryName, Gesture gesture) {
171        ArrayList<Gesture> gestures = mNamedGestures.get(entryName);
172        if (gestures == null) {
173            return;
174        }
175
176        gestures.remove(gesture);
177
178        // if there are no more samples, remove the entry automatically
179        if (gestures.isEmpty()) {
180            mNamedGestures.remove(entryName);
181        }
182
183        mClassifier.removeInstance(gesture.getID());
184
185        mChanged = true;
186    }
187
188    /**
189     * Remove a entry of gestures
190     *
191     * @param entryName the entry name
192     */
193    public void removeEntry(String entryName) {
194        mNamedGestures.remove(entryName);
195        mClassifier.removeInstances(entryName);
196        mChanged = true;
197    }
198
199    /**
200     * Get all the gestures of an entry
201     *
202     * @param entryName
203     * @return the list of gestures that is under this name
204     */
205    public ArrayList<Gesture> getGestures(String entryName) {
206        ArrayList<Gesture> gestures = mNamedGestures.get(entryName);
207        if (gestures != null) {
208            return new ArrayList<Gesture>(gestures);
209        } else {
210            return null;
211        }
212    }
213
214    public boolean hasChanged() {
215        return mChanged;
216    }
217
218    /**
219     * Save the gesture library
220     */
221    public void save(OutputStream stream) throws IOException {
222        save(stream, false);
223    }
224
225    public void save(OutputStream stream, boolean closeStream) throws IOException {
226        DataOutputStream out = null;
227
228        try {
229            long start;
230            if (PROFILE_LOADING_SAVING) {
231                start = SystemClock.elapsedRealtime();
232            }
233
234            final HashMap<String, ArrayList<Gesture>> maps = mNamedGestures;
235
236            out = new DataOutputStream((stream instanceof BufferedOutputStream) ? stream :
237                    new BufferedOutputStream(stream, GestureConstants.IO_BUFFER_SIZE));
238            // Write version number
239            out.writeShort(FILE_FORMAT_VERSION);
240            // Write number of entries
241            out.writeInt(maps.size());
242
243            for (Map.Entry<String, ArrayList<Gesture>> entry : maps.entrySet()) {
244                final String key = entry.getKey();
245                final ArrayList<Gesture> examples = entry.getValue();
246                final int count = examples.size();
247
248                // Write entry name
249                out.writeUTF(key);
250                // Write number of examples for this entry
251                out.writeInt(count);
252
253                for (int i = 0; i < count; i++) {
254                    examples.get(i).serialize(out);
255                }
256            }
257
258            out.flush();
259
260            if (PROFILE_LOADING_SAVING) {
261                long end = SystemClock.elapsedRealtime();
262                Log.d(LOG_TAG, "Saving gestures library = " + (end - start) + " ms");
263            }
264
265            mChanged = false;
266        } finally {
267            if (closeStream) GestureUtils.closeStream(out);
268        }
269    }
270
271    /**
272     * Load the gesture library
273     */
274    public void load(InputStream stream) throws IOException {
275        load(stream, false);
276    }
277
278    public void load(InputStream stream, boolean closeStream) throws IOException {
279        DataInputStream in = null;
280        try {
281            in = new DataInputStream((stream instanceof BufferedInputStream) ? stream :
282                    new BufferedInputStream(stream, GestureConstants.IO_BUFFER_SIZE));
283
284            long start;
285            if (PROFILE_LOADING_SAVING) {
286                start = SystemClock.elapsedRealtime();
287            }
288
289            // Read file format version number
290            final short versionNumber = in.readShort();
291            switch (versionNumber) {
292                case 1:
293                    readFormatV1(in);
294                    break;
295            }
296
297            if (PROFILE_LOADING_SAVING) {
298                long end = SystemClock.elapsedRealtime();
299                Log.d(LOG_TAG, "Loading gestures library = " + (end - start) + " ms");
300            }
301        } finally {
302            if (closeStream) GestureUtils.closeStream(in);
303        }
304    }
305
306    private void readFormatV1(DataInputStream in) throws IOException {
307        final Learner classifier = mClassifier;
308        final HashMap<String, ArrayList<Gesture>> namedGestures = mNamedGestures;
309        namedGestures.clear();
310
311        // Number of entries in the library
312        final int entriesCount = in.readInt();
313
314        for (int i = 0; i < entriesCount; i++) {
315            // Entry name
316            final String name = in.readUTF();
317            // Number of gestures
318            final int gestureCount = in.readInt();
319
320            final ArrayList<Gesture> gestures = new ArrayList<Gesture>(gestureCount);
321            for (int j = 0; j < gestureCount; j++) {
322                final Gesture gesture = Gesture.deserialize(in);
323                gestures.add(gesture);
324                classifier.addInstance(
325                        Instance.createInstance(mSequenceType, mOrientationStyle, gesture, name));
326            }
327
328            namedGestures.put(name, gestures);
329        }
330    }
331
332    Learner getLearner() {
333        return mClassifier;
334    }
335}
336