it.geosolutions.geocollect.android.core.mission.utils.MissionUtils.java Source code

Java tutorial

Introduction

Here is the source code for it.geosolutions.geocollect.android.core.mission.utils.MissionUtils.java

Source

/*
 * GeoSolutions - GeoCollect
 * Copyright (C) 2014 - 2015  GeoSolutions (www.geo-solutions.it)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package it.geosolutions.geocollect.android.core.mission.utils;

import it.geosolutions.android.map.utils.MapFilesProvider;
import it.geosolutions.android.map.wfs.WFSGeoJsonFeatureLoader;
import it.geosolutions.android.map.wfs.geojson.GeoJson;
import it.geosolutions.android.map.wfs.geojson.feature.Feature;
import it.geosolutions.geocollect.android.app.BuildConfig;
import it.geosolutions.geocollect.android.app.R;
import it.geosolutions.geocollect.android.core.login.LoginActivity;
import it.geosolutions.geocollect.android.core.mission.Mission;
import it.geosolutions.geocollect.android.core.mission.MissionFeature;
import it.geosolutions.geocollect.android.core.mission.PendingMissionListActivity;
import it.geosolutions.geocollect.model.config.MissionTemplate;
import it.geosolutions.geocollect.model.source.XDataType;
import it.geosolutions.geocollect.model.viewmodel.Field;
import it.geosolutions.geocollect.model.viewmodel.Form;
import it.geosolutions.geocollect.model.viewmodel.Page;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jsqlite.Database;
import jsqlite.Exception;
import jsqlite.Stmt;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.google.gson.Gson;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.PrecisionModel;
import com.vividsolutions.jts.io.WKBReader;

/**
 * @author Lorenzo Natali (lorenzo.natali@geo-solutions.it)
 * Utilities class for Mission
 */
public class MissionUtils {

    /**
     * TAG for Logging
     */
    private static String TAG = "MissionUtils";

    /**
     * The regex to parse the tags in the json
     */
    private static final String TAG_REGEX = "\\$\\{(.*?)\\}";
    private static final Pattern pattern = Pattern.compile(TAG_REGEX);

    /**
     * Patterns to replace in various templates
     */
    public static String HEX_COLOR_PATTERN = "#XXXXXX";
    public static String TABLE_NAME_PATTERN = "XXNAMEXX";

    /**
     * Feature GCID string name 
     */
    public static final String GCID_STRING = "GCID";

    /**
     * Create a loader getting the source of the mission
     * @param missionTemplate
     * @param page
     * @param pagesize
     * @return
     */
    public static Loader<List<MissionFeature>> createMissionLoader(MissionTemplate missionTemplate,
            SherlockFragmentActivity activity, int page, int pagesize, Database db) {

        // Retrieve saved credentials for BasicAuth
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
        String username = prefs.getString(LoginActivity.PREFS_USER_EMAIL, null);
        String password = prefs.getString(LoginActivity.PREFS_PASSWORD, null);

        WFSGeoJsonFeatureLoader wfsl = new WFSGeoJsonFeatureLoader(activity, missionTemplate.schema_seg.URL,
                missionTemplate.schema_seg.baseParams, missionTemplate.schema_seg.typeName, page * pagesize + 1,
                pagesize, username, password);

        return new SQLiteCascadeFeatureLoader(activity, wfsl, db, missionTemplate.schema_seg.localSourceStore,
                missionTemplate.schema_sop.localFormStore,
                missionTemplate.schema_seg.orderingField != null ? missionTemplate.schema_seg.orderingField
                        : missionTemplate.orderingField,
                missionTemplate.priorityField, missionTemplate.priorityValuesColors);
    }

    /**
     * Provide the default template as configured in the raw folder.
     * @param c
     * @return
     */
    public static MissionTemplate getDefaultTemplate(Context c) {

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
        boolean usesDownloaded = prefs.getBoolean(PendingMissionListActivity.PREFS_USES_DOWNLOADED_TEMPLATE, false);

        if (usesDownloaded) {

            int index = prefs.getInt(PendingMissionListActivity.PREFS_DOWNLOADED_TEMPLATE_INDEX, 0);

            ArrayList<MissionTemplate> templates = PersistenceUtils.loadSavedTemplates(c);

            String selectedTemplateId = prefs.getString(PendingMissionListActivity.PREFS_SELECTED_TEMPLATE_ID,
                    null);
            if (selectedTemplateId != null && !selectedTemplateId.isEmpty()) {
                for (MissionTemplate t : templates) {
                    if (t.id != null && t.id.equalsIgnoreCase(selectedTemplateId)) {
                        return t;
                    }
                }
            }

            if (index >= templates.size()) {
                index = templates.size() - 1;
            }
            return templates.get(index);

        } else {

            InputStream inputStream = c.getResources().openRawResource(R.raw.defaulttemplate);
            if (inputStream != null) {
                final Gson gson = new Gson();
                final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
                // TODO: Catch JsonSyntaxException when template is malformed
                return gson.fromJson(reader, MissionTemplate.class);
            }
        }

        return null;
    }

    /**
     * converts the Json string to an inputstream and parses it using gson
     * @param json String
     * @return the parsed template
     */
    public static MissionTemplate getTemplateFromJSON(final String json) {

        final Gson gson = new Gson();
        final BufferedReader reader = new BufferedReader(
                new InputStreamReader(new ByteArrayInputStream(json.getBytes())));

        return gson.fromJson(reader, MissionTemplate.class);
    }

    /**
     * Parse the string to get the tags between {} (brackets) 
     * @param toParse the string to parse
     * @return the list of brackets
     */
    public static List<String> getTags(String toParse) {
        if (toParse == null) {
            return null;
        }
        Matcher matcher = pattern.matcher(toParse);
        //gets the 
        while (matcher.find()) {
            List<String> tags = new ArrayList<String>();

            int pos = -1;
            while (matcher.find(pos + 1)) {
                pos = matcher.start();
                tags.add(matcher.group(1));
            }
            return tags;
        }
        return null;
    }

    /**
     * @param dataMapping
     * @return
     */
    public static String generateJsonString(Map<String, String> dataMapping, Mission m) {
        Feature f = PersistenceUtils.loadFeatureById(m);
        GeoJson gson = new GeoJson();
        String c = gson.toJson(f);
        try {
            return new String(c.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            // return the original string
            return c;
        }
    }

    /**
     * checks if mandatory fields were compiled using the mandatory field of the defaulttemplate
     * 
     * @param form the form to check
     * @param id to refer to
     * @param db to read from
     * @param tableName to refer to
     * @return ArrayList with the fields label which was not compiled or an empty list if all was done
     */
    public static ArrayList<String> checkIfAllMandatoryFieldsAreSatisfied(final Form form, final String id,
            final Database db, final String tableName) {

        ArrayList<String> missingEntries = new ArrayList<String>();

        Stmt st = null;
        //find mandatory fields
        ArrayList<Pair<String, String>> missingFieldIDs = new ArrayList<Pair<String, String>>();
        for (Page page : form.pages) {
            for (Field f : page.fields) {
                if (f.mandatory) {
                    missingFieldIDs.add(new Pair<String, String>(f.fieldId, f.label));
                }
            }
        }
        //if no mandatory fields no need to continue
        if (missingFieldIDs.isEmpty()) {
            return missingEntries;
        }

        //create selection
        String selection = "";
        for (int i = 0; i < missingFieldIDs.size(); i++) {
            selection += missingFieldIDs.get(i).first;
            if (i < missingFieldIDs.size() - 1) {
                selection += ",";
            }
        }
        //create query
        final String s = "SELECT " + selection + " FROM '" + tableName + "' WHERE ORIGIN_ID = '" + id + "';";

        //do the query
        if (jsqlite.Database.complete(s)) {
            try {
                st = db.prepare(s);
                if (st.step()) {
                    for (int j = 0; j < st.column_count(); j++) {

                        //if mandatory field is null or empty, add it to the missing entries
                        if (st.column_string(j) == null || st.column_string(j).equals("")) {
                            missingEntries.add(missingFieldIDs.get(j).second);
                        }
                    }
                }
            } catch (Exception e) {
                Log.d(MissionUtils.class.getSimpleName(), "Error checkIfAllMandatoryFieldsArsSatisfied", e);
            }
        } else {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "Query is not complete: " + s);
            }
        }

        return missingEntries;
    }

    /**
     * get "created" {@link MissionFeature} from the database
     * @param tableName
     * @param db
     * @return a list of created {@link MissionFeature}
     */
    public static ArrayList<MissionFeature> getMissionFeatures(final String tableName, final Database db) {
        return getMissionFeatures(tableName, db, null);
    }

    /**
     * get "created" {@link MissionFeature} from the database, adding the distance property if possible
     * @param tableName
     * @param db
     * @return a list of created {@link MissionFeature}
     */
    public static ArrayList<MissionFeature> getMissionFeatures(final String mTableName, final Database db,
            Context ctx) {

        ArrayList<MissionFeature> mFeaturesList = new ArrayList<MissionFeature>();

        String tableName = mTableName;

        // Reader for the Geometry field
        WKBReader wkbReader = new WKBReader();

        //create query
        ////////////////////////////////////////////////////////////////
        // SQLite Geometry cannot be read with direct wkbreader
        // We must do a double conversion with ST_AsBinary and CastToXY 
        ////////////////////////////////////////////////////////////////

        // Cycle all the columns to find the "Point" type one
        HashMap<String, String> columns = SpatialiteUtils.getPropertiesFields(db, tableName);
        if (columns == null) {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "Cannot retrieve columns from database");
            }
            return mFeaturesList;
        }

        List<String> selectFields = new ArrayList<String>();
        for (String columnName : columns.keySet()) {
            //Spatialite custom field point
            if ("point".equalsIgnoreCase(columns.get(columnName))) {
                selectFields.add("ST_AsBinary(CastToXY(" + columnName + ")) AS GEOMETRY");
            } else {
                selectFields.add(columnName);
            }
        }

        // Merge all the column names
        String selectString = TextUtils.join(",", selectFields);

        if (ctx != null) {

            SharedPreferences prefs = ctx.getSharedPreferences(SQLiteCascadeFeatureLoader.PREF_NAME,
                    Context.MODE_PRIVATE);

            boolean useDistance = prefs.getBoolean(SQLiteCascadeFeatureLoader.ORDER_BY_DISTANCE, false);
            double posX = Double.longBitsToDouble(
                    prefs.getLong(SQLiteCascadeFeatureLoader.LOCATION_X, Double.doubleToLongBits(0)));
            double posY = Double.longBitsToDouble(
                    prefs.getLong(SQLiteCascadeFeatureLoader.LOCATION_Y, Double.doubleToLongBits(0)));

            if (useDistance) {
                selectString = selectString + ", Distance(ST_Transform(GEOMETRY,4326), MakePoint(" + posX + ","
                        + posY + ", 4326)) * 111195 AS '" + MissionFeature.DISTANCE_VALUE_ALIAS + "'";
            }

            //Add Spatial filtering
            int filterSrid = prefs.getInt(SQLiteCascadeFeatureLoader.FILTER_SRID, -1);
            // If the SRID is not defined, skip the filter
            if (filterSrid != -1) {
                double filterN = Double.longBitsToDouble(
                        prefs.getLong(SQLiteCascadeFeatureLoader.FILTER_N, Double.doubleToLongBits(0)));
                double filterS = Double.longBitsToDouble(
                        prefs.getLong(SQLiteCascadeFeatureLoader.FILTER_S, Double.doubleToLongBits(0)));
                double filterW = Double.longBitsToDouble(
                        prefs.getLong(SQLiteCascadeFeatureLoader.FILTER_W, Double.doubleToLongBits(0)));
                double filterE = Double.longBitsToDouble(
                        prefs.getLong(SQLiteCascadeFeatureLoader.FILTER_E, Double.doubleToLongBits(0)));

                tableName += " WHERE MbrIntersects(GEOMETRY, BuildMbr(" + filterW + ", " + filterN + ", " + filterE
                        + ", " + filterS + ")) ";
            }

        }

        // Build the query
        StringWriter queryWriter = new StringWriter();
        queryWriter.append("SELECT ").append(selectString).append(" FROM ").append(tableName).append(";");

        // The resulting query
        String query = queryWriter.toString();

        Stmt stmt;
        //do the query
        if (jsqlite.Database.complete(query)) {
            try {
                if (BuildConfig.DEBUG) {
                    Log.i("getCreatedMissionFeatures", "Loading from query: " + query);
                }
                stmt = db.prepare(query);
                MissionFeature f;
                while (stmt.step()) {
                    f = new MissionFeature();

                    SpatialiteUtils.populateFeatureFromStmt(wkbReader, stmt, f);

                    if (f.geometry == null) {
                        //workaround for a bug which does not read out the "Point" geometry in WKBreader
                        //read single x and y coordinates instead and create the geometry by hand
                        double[] xy = PersistenceUtils.getXYCoord(db, tableName, f.id);
                        if (xy != null) {
                            GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326);
                            f.geometry = geometryFactory.createPoint(new Coordinate(xy[0], xy[1]));
                        }
                    }
                    f.typeName = mTableName;
                    mFeaturesList.add(f);
                }
                stmt.close();
            } catch (Exception e) {
                Log.d(TAG, "Error getCreatedMissions", e);
            }
        } else {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "Query is not complete: " + query);
            }
        }

        return mFeaturesList;
    }

    /**
     * Modifies the Feature aligning the field types with the give ones
     * @param inputFeature
     * @param fieldTypes
     */
    public static void alignPropertiesTypes(Feature inputFeature, HashMap<String, XDataType> fieldTypes) {

        if (inputFeature == null || fieldTypes == null || inputFeature.properties == null
                || inputFeature.properties.isEmpty()) {
            // Nothing to do
            return;
        }

        ArrayList<String> propList = new ArrayList<String>();
        for (String s : inputFeature.properties.keySet()) {
            propList.add(s);
        }

        // Cycle the properties
        for (String propertyString : propList) {
            if (fieldTypes.containsKey(propertyString)) {
                if (inputFeature.properties.get(propertyString) instanceof String) {
                    // Get property value
                    String pValue = (String) inputFeature.properties.get(propertyString);

                    if (fieldTypes.get(propertyString) == XDataType.integer) {

                        if (pValue == null || pValue.isEmpty()) {
                            // 
                            inputFeature.properties.remove(propertyString);
                        }

                        try {
                            inputFeature.properties.put(propertyString, Integer.parseInt(pValue));
                        } catch (NumberFormatException nfe) {
                            Log.w(TAG, "Wrong Integer String format, removing property " + propertyString
                                    + "with value " + pValue);
                            inputFeature.properties.remove(propertyString);
                        }

                    } else if (fieldTypes.get(propertyString) == XDataType.decimal
                            || fieldTypes.get(propertyString) == XDataType.real) {

                        if (pValue == null || pValue.isEmpty()) {
                            // 
                            inputFeature.properties.remove(propertyString);
                        }

                        try {
                            inputFeature.properties.put(propertyString, Double.parseDouble(pValue));
                        } catch (NumberFormatException nfe) {
                            Log.w(TAG, "Wrong Double String format, removing property " + propertyString
                                    + "with value " + pValue);
                            inputFeature.properties.remove(propertyString);
                        }

                    }
                }
            }
        }

    }

    /**
     * Returns the "GCID" field of the origin feature of the given Mission
     * @param mMission
     * @return
     */
    public static String getMissionGCID(Mission mission) {

        if (mission == null || mission.getOrigin() == null) {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "WARNING: cannot find origin Feature");
            }
            return null;
        }

        return getFeatureGCID(mission.getOrigin());
    }

    /**
     * Returns the "GCID" or "gcid" field of the given Feature
     * Returns null otherwise
     * @param feature
     * @return
     */
    public static String getFeatureGCID(Feature feature) {

        if (feature == null) {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "WARNING: cannot find feature GCID (feature null)");
            }
            return null;
        }

        // Default ID
        String originIDString = feature.id;

        if (feature.properties != null) {

            String localGCID = null;
            if (feature.properties.containsKey(GCID_STRING)) {
                localGCID = GCID_STRING;
            } else if (feature.properties.containsKey(GCID_STRING.toLowerCase(Locale.US))) {
                localGCID = GCID_STRING.toLowerCase(Locale.US);
            }

            if (localGCID != null) {
                try {
                    Object objID = feature.properties.get(localGCID);
                    if (objID == null) {
                        if (BuildConfig.DEBUG) {
                            Log.w(TAG, "WARNING: Feature has a null GCID using feature id: " + originIDString);
                        }
                        return originIDString;
                    }
                    originIDString = (String) objID;

                } catch (ClassCastException cce) {
                    if (BuildConfig.DEBUG) {
                        Log.w(TAG, "WARNING: Feature has a GCID but it cannot be converted to String");
                    }
                    originIDString = null;
                }
            }
        }

        return originIDString;
    }

    /**
     * Check the existence of map styles relative to the given {@link MissionTemplate}
     * If they don't exist, it create them
     * @param missionTemplate
     */
    public static void checkMapStyles(Resources resources, MissionTemplate missionTemplate) {
        if (resources == null || missionTemplate == null) {
            return;
        }

        File styleDir = new File(MapFilesProvider.getStyleDirIn());
        if (!styleDir.exists()) {

            // Create the directory if not exists
            styleDir.mkdirs();

        } else if (!styleDir.isDirectory()) {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "Style directory is not a directory!");
            }
            return;
        }

        String baseString;
        try {
            baseString = PersistenceUtils.loadBaseStyleFile(resources);
        } catch (IOException e1) {
            Log.e(TAG, e1.getLocalizedMessage(), e1);
            return;
        }

        if (baseString == null) {
            return;
        }

        if (missionTemplate.schema_seg != null && missionTemplate.schema_seg.localSourceStore != null) {

            // Check for the main style
            File mainStyleFile = new File(styleDir, missionTemplate.schema_seg.localSourceStore + ".style");
            if (!mainStyleFile.exists()) {

                String noticeStyleString = baseString.replace(HEX_COLOR_PATTERN,
                        resources.getString(R.color.default_notice_color));
                noticeStyleString = noticeStyleString.replace(TABLE_NAME_PATTERN,
                        missionTemplate.schema_seg.localSourceStore);
                writeStyleFile(noticeStyleString, mainStyleFile);

            }

            // Check for the new notices style
            File newStyleFile = new File(styleDir,
                    missionTemplate.schema_seg.localSourceStore + MissionTemplate.NEW_NOTICE_SUFFIX + ".style");
            if (!newStyleFile.exists()) {

                String newStyleString = baseString.replace(HEX_COLOR_PATTERN,
                        resources.getString(R.color.default_new_notice_color));
                newStyleString = newStyleString.replace(TABLE_NAME_PATTERN,
                        missionTemplate.schema_seg.localSourceStore + MissionTemplate.NEW_NOTICE_SUFFIX);
                writeStyleFile(newStyleString, newStyleFile);

            }
        }

        if (missionTemplate.schema_sop != null && missionTemplate.schema_sop.localFormStore != null) {

            // Check for the Surveys style
            File surveyStyleFile = new File(styleDir, missionTemplate.schema_sop.localFormStore + ".style");
            if (!surveyStyleFile.exists()) {

                String surveyStyleString = baseString.replace(HEX_COLOR_PATTERN,
                        resources.getString(R.color.default_survey_color));
                surveyStyleString = surveyStyleString.replace(TABLE_NAME_PATTERN,
                        missionTemplate.schema_sop.localFormStore);
                writeStyleFile(surveyStyleString, surveyStyleFile);

            }

        }

    }

    /**
     * Write the String into the given File
     * @param baseString
     * @param mainStyleFile
     */
    public static void writeStyleFile(String baseString, File mainStyleFile) {
        FileWriter fw = null;
        try {
            fw = new FileWriter(mainStyleFile);
            fw.write(baseString);
            Log.i("STYLE", "Style File updated:" + mainStyleFile.getPath());
        } catch (FileNotFoundException e) {
            Log.e("STYLE", "unable to write open file:" + mainStyleFile.getPath());
        } catch (IOException e) {
            Log.e("STYLE", "error writing the file: " + mainStyleFile.getPath());
        } finally {
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                // ignored
            }
        }
    }

    /**
     * checks if all files which are defined in the config section of
     * the template are present in the applications sdcard folder
     * @param t the template to check
     * @return true if all files are present - false otherwise
     */
    @SuppressWarnings("rawtypes")
    public static boolean checkTemplateForBackgroundData(final Context context, final MissionTemplate t) {

        final String mount = MapFilesProvider.getEnvironmentDirPath(context);
        final String baseDir = MapFilesProvider.getBaseDir();

        final String appDir = mount + baseDir;

        final HashMap<String, Object> config = t.config;

        if (config.containsKey("bgFiles")) {
            ArrayList<Map> urls = (ArrayList<Map>) config.get("bgFiles");
            if (urls != null) {
                for (int i = 0; i < urls.size(); i++) {
                    Map ltm = urls.get(i);
                    //String url = (String) ltm.get("url");
                    ArrayList<Map> content = (ArrayList<Map>) ltm.get("content");

                    for (Map item : content) {

                        final String fileName = (String) item.get("file");
                        final File file = new File(appDir + "/" + fileName);

                        if (!file.exists()) {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    /**
     * creates and returns a HashMap containing entries consisting of url to zip files and the amount of files
     * these zips contain
     * @param t the template to parse
     * @return HashMap containing map of urls to fileAmounts
     */
    @SuppressWarnings("rawtypes")
    public static HashMap<String, Integer> getContentUrlsAndFileAmountForTemplate(final MissionTemplate t) {

        final HashMap<String, Integer> urls = new HashMap<String, Integer>();

        final HashMap<String, Object> config = t.config;

        if (config.containsKey("bgFiles")) {
            ArrayList<Map> files = (ArrayList<Map>) config.get("bgFiles");
            if (files != null) {
                for (int i = 0; i < files.size(); i++) {
                    Map ltm = files.get(i);
                    String url = (String) ltm.get("url");

                    ArrayList<Map> content = (ArrayList<Map>) ltm.get("content");

                    urls.put(url, content.size());

                }
            }

        }
        return urls;
    }

    /**
     * Generates a new {@link MissionFeature} with the correct properties casing suitable for the upload.
     * Removes unnecessary properties
     * Takes feature schema as input
     */
    public static MissionFeature alignMissionFeatureProperties(MissionFeature inputMissionFeature,
            HashMap<String, XDataType> schema) {

        if (inputMissionFeature == null || schema == null) {
            if (BuildConfig.DEBUG) {
                Log.w(TAG, "NULL MissionFeature or schema, cannot convert.");
            }
            return null;
        }

        MissionFeature output = new MissionFeature();

        output.typeName = inputMissionFeature.typeName;
        output.id = inputMissionFeature.id;
        output.displayColor = inputMissionFeature.displayColor;
        output.editing = inputMissionFeature.editing;
        if (inputMissionFeature.geometry != null) {
            output.geometry = (Geometry) inputMissionFeature.geometry.clone();
        }
        output.geometry_name = inputMissionFeature.geometry_name;
        output.type = inputMissionFeature.type;

        output.properties = new HashMap<String, Object>();

        if (inputMissionFeature.properties != null) {
            for (String inputKey : inputMissionFeature.properties.keySet()) {
                for (String schemaKey : schema.keySet()) {
                    if (schemaKey != null && schemaKey.equalsIgnoreCase(inputKey)) {
                        output.properties.put(schemaKey, inputMissionFeature.properties.get(inputKey));
                    }
                }
            }
        }

        return output;
    }

    /**
     * MissionTemplate Comparator
     * This is based on Template ID, all other fields are ignored
     * @author Lorenzo Pini (lorenzo.pini@geo-solutions.it)
     */
    public static class MissionTemplateComparator implements Comparator<MissionTemplate> {
        @Override
        public int compare(MissionTemplate o1, MissionTemplate o2) {
            if (o1 == o2) {
                return 0;
            }
            if (o1 == null) {
                return -1;
            }

            if (o2 == null) {
                return 1;
            }

            if (o1.id == null || o1.id.isEmpty()) {
                return -1;
            }

            if (o2.id == null || o2.id.isEmpty()) {
                return -1;
            }

            return o1.id.compareTo(o2.id);

        }
    }
}