/*
* 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);
}
}
}