Files
ayanova7/source/Plugins/AyaNova.Plugin.V8/util.cs
2021-08-10 20:23:32 +00:00

855 lines
34 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.Security.Cryptography;
using biz = GZTW.AyaNova.BLL;
namespace AyaNova.PlugIn.V8
{
class util
{
public static string PRE_RELEASE_VERSION_STRING { get; set; }
public static GZTW.AyaNova.BLL.LocalizedTextTable LocaleText = null;
const string TEST_ROUTE = "notify/hello";
const string API_BASE_ROUTE = "api/v8/";
static HttpClient client = new HttpClient();
//url once known to be good
internal static string ApiBaseUrl { get; set; }
internal static string JWT { get; set; }
public static bool Initialized { get; set; }
internal static string ServerState { get; set; }
public util()
{
Initialized = false;
JWT = string.Empty;
}
#region INIT / AUTH
private static void InitClient()
{
if (Initialized) return;
client.BaseAddress = new Uri(ApiBaseUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// client.DefaultRequestHeaders.Add("X-AY-Import-Mode", "true");
Initialized = true;
}
/// <summary>
/// Only a return value of "OK" is ok
/// </summary>
/// <param name="serverUrl"></param>
/// <returns></returns>
public static async Task<string> TestUrlAsync(string serverUrl)
{
if (string.IsNullOrEmpty(serverUrl)) return "Server url required";
if (!serverUrl.Contains("/api/"))
{
if (!serverUrl.EndsWith("/")) serverUrl += "/";
serverUrl += API_BASE_ROUTE;
}
if (!serverUrl.StartsWith("http")) serverUrl = "http://" + serverUrl;
//try to connect, ping the server api
if (!Initialized)
{
client.BaseAddress = new Uri(serverUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Initialized = true;
}
try
{
HttpResponseMessage response = await client.GetAsync(serverUrl + TEST_ROUTE);
if (response.IsSuccessStatusCode)
{
ApiBaseUrl = serverUrl;
return "OK";
}
else
{
return "Failed: " + response.StatusCode.ToString();
}
}
catch { return "Failed"; }
// return "failed";
}
public async static Task<bool> AuthenticateAsync(string login, string password = null)
{
InitClient();
if (password == null)
password = login;
dynamic creds = new JObject();
creds.login = login;
creds.password = password;
ApiResponse a = await PostAsync("auth", creds.ToString());
if (a.HttpResponse.IsSuccessStatusCode)
{
JWT = a.ObjectResponse["data"]["token"].Value<string>();
//Must be *the* SuperUser to continue:
a = await GetAsync("user/amsu");
var IsSuperUser = a.ObjectResponse["data"].Value<bool>();
if (!IsSuperUser)
{
JWT = string.Empty;
return false;
}
a = await GetAsync("server-state/");
ServerState = a.ObjectResponse["data"]["serverState"].Value<string>();
return true;
}
return false;
}
public async static Task<ApiResponse> GetAsync(string route)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, route);
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
HttpResponseMessage response = await client.SendAsync(requestMessage);
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception("GET error, route: " + route + "\n" + responseAsString + "\n" + response.ReasonPhrase);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
//
public async static Task<ApiResponse> PostAsync(string route, string postJson = null)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, route);
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
if (!string.IsNullOrWhiteSpace(postJson))
requestMessage.Content = new StringContent(postJson, System.Text.Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.SendAsync(requestMessage);
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (string.IsNullOrWhiteSpace(postJson))
postJson = "n/a";
throw new Exception("POST error, route: " + route + "\n" + responseAsString + "\n" + response.ReasonPhrase + "\nPOSTED OBJECT:\n" + postJson);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
public async static Task<ApiResponse> PutAsync(string route, string putJson = null)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Put, route);
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
if (!string.IsNullOrWhiteSpace(putJson))
requestMessage.Content = new StringContent(putJson, System.Text.Encoding.UTF8, "application/json");
HttpResponseMessage response = await client.SendAsync(requestMessage);
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (string.IsNullOrWhiteSpace(putJson))
putJson = "n/a";
throw new Exception("PUT error, route: " + route + "\n" + responseAsString + "\n" + response.ReasonPhrase + "\nPUT OBJECT:\n" + putJson);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
public async static Task<ApiResponse> PostFormDataAsync(string route, MultipartFormDataContent formContent)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, route);
requestMessage.Headers.Add("X-AY-Import-Mode", "1");
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
requestMessage.Content = formContent;
HttpResponseMessage response = await client.SendAsync(requestMessage);
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception("POST FORMDATA error, route: " + route + "\n" + responseAsString + "\n" + response.ReasonPhrase);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
//eoc
public class ApiResponse
{
public HttpResponseMessage HttpResponse { get; set; }
public JObject ObjectResponse { get; set; }
public string CompactResponse
{
get
{
return ObjectResponse.ToString(Newtonsoft.Json.Formatting.None);
}
}
}
//make an event log entry to preserve the original v7 history such as it is
public async static Task EventLog(AyaType RavenType, long RavenId, long RavenCreatorId, long RavenModifierId, string sCreated, string sModified)
{
//DateTime Created = DateTime.UtcNow;
//if (!string.IsNullOrWhiteSpace(sCreated))
// Created = DateTime.Parse(sCreated).ToUniversalTime();
//DateTime Modified = DateTime.UtcNow;
//if (!string.IsNullOrWhiteSpace(sCreated))
// Modified = DateTime.Parse(sModified).ToUniversalTime();
dynamic d = new JObject();
d.ayaType = (int)RavenType;
d.ayId = RavenId;
d.creator = RavenCreatorId;
d.modifier = RavenModifierId;
d.created = DateToV8(sCreated, true);
d.modified = DateToV8(sModified, true);
await PostAsync("event-log/v7", d.ToString());
}
/// <summary>
///
/// </summary>
/// <param name="jsonString"></param>
/// <returns></returns>
public static JObject Parse(string jsonString)
{
if (string.IsNullOrWhiteSpace(jsonString))
{
return null;
}
return JObject.Parse(jsonString);
}
public static long IdFromResponse(ApiResponse a)
{
return a.ObjectResponse["data"]["id"].Value<long>();
}
public static uint CTokenFromResponse(ApiResponse a)
{
return a.ObjectResponse["data"]["concurrency"].Value<uint>();
}
#endregion
#region Misc utils
//Used to ensure a unique name generated by appending -nnn is within length requirements by splitting and chopping part of text to keep name
public static string UniqueNameBuilder(string oldName, long appendValue, int maxLength)
{
//deadman switch
if (appendValue > int.MaxValue)
{
throw new System.OverflowException("UniqueNameBuilder: Unique name could not be generated for item \"" + oldName + "\" after " + int.MaxValue.ToString() + " attempts");
}
var appendString = "-" + appendValue.ToString();
string ret = oldName + appendString;
var diff = maxLength - ret.Length;
if (diff < 0)
{
ret = oldName.Substring(0, Math.Abs(diff)) + appendString;
}
return ret;
}
//used to set nonsense values from imported user login and password
//just in case they are set active after import but never have their passwords set
//so they can't be as easily hacked into
public static string RandomString()
{
var b = new byte[32];
var random = RandomNumberGenerator.Create();
random.GetNonZeroBytes(b);
return Convert.ToBase64String(b);
}
public static string NormalizeTag(string inObj)
{
if (string.IsNullOrWhiteSpace(inObj)) return null;
//Must be lowercase per rules
//This may be naive when we get international cust omers but for now supporting utf-8 and it appears it's safe to do this with unicode
inObj = inObj.ToLowerInvariant();
//No spaces in tags, replace with dashes
inObj = inObj.Replace(" ", "-");
//Remove multiple dash sequences
inObj = System.Text.RegularExpressions.Regex.Replace(inObj, "-+", "-");
//Ensure doesn't start or end with a dash
inObj = inObj.Trim('-');
//No longer than 255 characters
inObj = MaxLength(inObj, 255);
return inObj;
}
//clean up tags from client submission
//remove dupes, substitute dashes for spaces, lowercase and shorten if exceed 255 chars
//and sorts before returning to ensure consistent ordering
public static List<string> NormalizeTags(List<string> inTags)
{
if (inTags == null || inTags.Count == 0) return inTags;
List<string> outTags = new List<string>();
foreach (var tag in inTags)
outTags.Add(NormalizeTag(tag));
outTags.Sort();
return outTags.Distinct().ToList();
}
/// <summary>
/// Trim a string if necessary
/// </summary>
/// <param name="s"></param>
/// <param name="maxLength"></param>
/// <returns></returns>
public static string MaxLength(string s, int maxLength)
{
if (s.Length > maxLength)
s = s.Substring(0, maxLength);
return s;
}
///////////////////////////////
// v7 to v8 Date conversion
//NOTE: ensure null for empty date
//
public static string DateToV8(object o, bool neverEmpty = false) { return DateToV8(o.ToString(), neverEmpty); }
public static string DateToV8(string s, bool neverEmpty = false)
{
if (string.IsNullOrWhiteSpace(s))
{
if (neverEmpty)
{
return DateTime.UtcNow.ToString("o");
}
else
{
return null;
}
}
DateTime result = new DateTime();
if (DateTime.TryParse(s, out result))
return result.ToUniversalTime().ToString("o");
//couldn't be parsed
if (neverEmpty)
return DateTime.UtcNow.ToString("o");
return null;
}
///////////////////////////////
// v7 to v8 Date conversion
//
//
public static bool DateIsPast(object o, bool neverEmpty = false) { return DateIsPast(o.ToString(), neverEmpty); }
public static bool DateIsPast(string s, bool neverEmpty = false)
{
if (string.IsNullOrWhiteSpace(s))
return false;//no date so not past
DateTime result = new DateTime();
if (DateTime.TryParse(s, out result))
return result < DateTime.Now;
//can't parse so no, it's not past
return false;
}
public enum AyaUiFieldDataType : int
{
NoType = 0,
DateTime = 1,
Date = 2,
Time = 3,
Text = 4,
Integer = 5,
Bool = 6,
Decimal = 7,
Currency = 8,
Tags = 9,
Enum = 10,
EmailAddress = 11
}
public static AyaType RootObjectToAyaType(biz.RootObjectTypes rot, biz.WorkorderTypes specificWoType = biz.WorkorderTypes.Unknown)
{
switch (rot)
{
case biz.RootObjectTypes.User:
return AyaType.User;
case biz.RootObjectTypes.Client:
return AyaType.Customer;
case biz.RootObjectTypes.Contract:
return AyaType.Contract;
case biz.RootObjectTypes.HeadOffice:
return AyaType.HeadOffice;
case biz.RootObjectTypes.LoanItem:
return AyaType.LoanUnit;
case biz.RootObjectTypes.Part:
return AyaType.Part;
case biz.RootObjectTypes.WorkorderPreventiveMaintenance:
return AyaType.PM;
case biz.RootObjectTypes.WorkorderItem:
switch (specificWoType)
{
case biz.WorkorderTypes.PreventiveMaintenance:
return AyaType.PMItem;
case biz.WorkorderTypes.Quote:
return AyaType.QuoteItem;
case biz.WorkorderTypes.Service:
return AyaType.WorkOrderItem;
default:
throw new NotImplementedException("V8:util:RootObjectToAyaType -> type " + rot.ToString() + "," + specificWoType.ToString() + " is not coded yet");
}
case biz.RootObjectTypes.Memo:
return AyaType.Memo;
case biz.RootObjectTypes.Project:
return AyaType.Project;
case biz.RootObjectTypes.PurchaseOrder:
return AyaType.PurchaseOrder;
case biz.RootObjectTypes.Unit:
return AyaType.Unit;
case biz.RootObjectTypes.UnitModel:
return AyaType.UnitModel;
case biz.RootObjectTypes.Vendor:
return AyaType.Vendor;
case biz.RootObjectTypes.WorkorderItemLabor:
return AyaType.WorkOrderItemLabor;
case biz.RootObjectTypes.WorkorderItemTravel:
return AyaType.WorkOrderItemTravel;
//case biz.RootObjectTypes.ServiceBank:
// return AyaType.ServiceBank;
default:
throw new NotImplementedException("V8:util:RootObjectToAyaType -> type " + rot.ToString() + " is not coded yet");
}
}
public enum AyaType : int
{
//COREBIZOBJECT attribute must be set on objects that are:
//Attachable objects can have attachments,
//wikiable objects can have a wiki
//reviewable objects can have a review which is basically the same as a Reminder but with an object attached (was follow up schedmarker in v7)
//PIckList-able (has picklist template)
//Pretty much everything that represents some kind of real world object is wikiable or attachable as long as it has an ID and a type
//exceptions would be utility type objects like datalistview, formcustom etc that are not
//NOTE: NEW CORE OBJECTS - All areas of server AND CLIENT code that require adding any new core objects have been tagged with the following comment:
//CoreBizObject add here
//Search for that IN SERVER AND CLIENT CODE and you will see all areas that need coding for the new object
//***IMPORTANT: Also need to add translations for any new biz objects added that don't match exactly the name here in the key
//because enumlist gets it that way, i.e. "Global" would be the expected key
NoType = 0,
Global = 1,
//corebizobject
Widget = 2,
//corebizobject
User = 3,
ServerState = 4,
License = 5,
LogFile = 6,
PickListTemplate = 7,
//corebizobject
Customer = 8,
ServerJob = 9,
//corebizobject
Contract = 10,
TrialSeeder = 11,
ServerMetrics = 12,
Translation = 13,
UserOptions = 14,
//corebizobject
HeadOffice = 15,
//corebizobject
LoanUnit = 16,
FileAttachment = 17,
DataListSavedFilter = 18,
FormCustom = 19,
//corebizobject
Part = 20,
//corebizobject
PM = 21,
//corebizobject
PMItem = 22,
//corebizobject
QuoteItemExpense = 23,
//corebizobject
QuoteItemLabor = 24,
//corebizobject
Project = 25,
//corebizobject
PurchaseOrder = 26,
//corebizobject
Quote = 27,
//corebizobject
QuoteItem = 28,
//corebizobject
QuoteItemLoan = 29,
//corebizobject
QuoteItemPart = 30,
//corebizobject
Unit = 31,
//corebizobject
UnitModel = 32,
//corebizobject
Vendor = 33,
//--- WorkOrder
//corebizobject
WorkOrder = 34,
//corebizobject
WorkOrderItem = 35,
//corebizobject
WorkOrderItemExpense = 36,
//corebizobject
WorkOrderItemLabor = 37,
//corebizobject
WorkOrderItemLoan = 38,
//corebizobject
WorkOrderItemPart = 39,
//corebizobject
WorkOrderItemPartRequest = 40,
//corebizobject
WorkOrderItemScheduledUser = 41,
//corebizobject
WorkOrderItemTask = 42,
//corebizobject
WorkOrderItemTravel = 43,
//corebizobject
WorkOrderItemUnit = 44,
//---
//corebizobject
QuoteItemScheduledUser = 45,
//corebizobject
QuoteItemTask = 46,
GlobalOps = 47,//really only used for rights, not an object type of any kind
BizMetrics = 48,//deprecate? Not used for anything as of nov 2020
Backup = 49,
Notification = 50,
NotifySubscription = 51,
//corebizobject
Reminder = 52,
UnitMeterReading = 53,
//corebizobject
CustomerServiceRequest = 54,
// ServiceBank = 55,
OpsNotificationSettings = 56,
Report = 57,
DashboardView = 58,
//corebizobject
CustomerNote = 59,
//corebizobject
Memo = 60,
//corebizobject
Review = 61,
//corebizobject
ServiceRate = 62,
//corebizobject
TravelRate = 63,
//corebizobject
TaxCode = 64,
//corebizobject
PartAssembly = 65,
//corebizobject
PartWarehouse = 66,
PartInventory = 67,
DataListColumnView = 68,
PartInventoryRestock = 69,//for list only, synthetic object
PartInventoryRequest = 70,//for list only not, synthetic object
WorkOrderStatus = 71,
TaskGroup = 72,
WorkOrderItemOutsideService = 73,
WorkOrderItemPriority = 74,
WorkOrderItemStatus = 75,
//corebizobject
QuoteItemTravel = 76,
//corebizobject
QuoteItemUnit = 77,
QuoteStatus = 78,
QuoteItemOutsideService = 79,
//corebizobject
PMItemExpense = 80,
//corebizobject
PMItemLabor = 81,
//corebizobject
PMItemLoan = 82,
//corebizobject
PMItemPart = 83,
//corebizobject
PMItemPartRequest = 84,
//corebizobject
PMItemScheduledUser = 85,
//corebizobject
PMItemTask = 86,
//corebizobject
PMItemTravel = 87,
//corebizobject
PMItemUnit = 88,
PMItemOutsideService = 89
//NOTE: New objects added here need to also be added to the following classes:
//AyaNova.Biz.BizObjectExistsInDatabase
//AyaNova.Biz.BizObjectFactory
//AyaNova.Biz.BizRoles
//AyaNova.Biz.BizObjectNameFetcherDIRECT
//and in the CLIENT in ayatype.js
//and their model needs to have the ICoreBizObjectModel interface
//and need TRANSLATION KEYS because any type could show in the event log at the client end
}
public static bool LocaleIsCustomized(string localeName, GZTW.AyaNova.BLL.LocalizedTextTable lt, ProgressForm progress)
{
//checksum locales
//if a recognized checksum then don't import because it means it wasn't modified from stock
/*
Stock fresh trial db (fb) values:
Locale Custom English is customized [signature: -2007218741] exporting
Locale Deutsch is customized [signature: -605335883] exporting
Locale English is customized [signature: -2007218741] exporting
Locale Español is customized [signature: -1164206951] exporting
Locale Français is customized [signature: -720441286] exporting
Scotts:
Locale Custom English is customized [signature: 117115560] exporting
Locale Deutsch is customized [signature: 772843262] exporting
Locale English is customized [signature: 1382500515] exporting
Locale Español is customized [signature: 438715542] exporting
Locale Français is customized [signature: 804154061] exporting
Do not use these keys in hash calculation which were added / modified during various schema updates
Global.Label.UseInventory.Description
Locale.Label.LocaleFile
UnitNameDisplayFormats.Label.ModelSerial
UnitNameDisplayFormats.Label.SerialModel
UnitNameDisplayFormats.Label.SerialModelVendor
UnitNameDisplayFormats.Label.VendorModelSerial
O.PartBin
*/
List<int> StockLocaleHashes = new List<int>();
StockLocaleHashes.Add(-605335883);
StockLocaleHashes.Add(-2007218741);
StockLocaleHashes.Add(-1164206951);
StockLocaleHashes.Add(-720441286);
//calculate hash
List<string> allStrings = new List<string>();
foreach (var entry in lt.LT)
{
if (entry.Key == "Global.Label.UseInventory.Description") continue;
if (entry.Key == "Locale.Label.LocaleFile") continue;
if (entry.Key == "UnitNameDisplayFormats.Label.ModelSerial") continue;
if (entry.Key == "UnitNameDisplayFormats.Label.SerialModel") continue;
if (entry.Key == "UnitNameDisplayFormats.Label.SerialModelVendor") continue;
if (entry.Key == "UnitNameDisplayFormats.Label.VendorModelSerial") continue;
if (entry.Key == "O.PartBin") continue;
if (entry.Key == "UI.Label.CurrentUserName") continue;
allStrings.Add(entry.Value);
}
int CurrentLocaleHash = GetOrderIndependentHashCode<string>(allStrings);
if (StockLocaleHashes.Contains(CurrentLocaleHash)) return false;
progress.Append("Locale " + localeName + " is customized [signature: " + CurrentLocaleHash.ToString() + "] exporting");
return true;
}
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked(hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
public class NameIdItem
{
public long Id { get; set; }
public string Name { get; set; }
}
public static string v7StrokePathTov8SVG(string spath, int width, int height)
{
List<Tuple<int, int>> lPoints = new List<Tuple<int, int>>();
string[] sLines = spath.Split('X');
foreach (string sLine in sLines)
{
string[] sPoints = sLine.Trim().Split(' ');
if (sPoints.Length > 0)
{
foreach (string s in sPoints)
{
lPoints.Add(StringToPoint(s));
}
}
}
//"71.5,77.19999694824219 71.5,79.19999694824219 71.5,83.19999694824219 71.5,91.19999694824219 74.5,101.19999694824219 75.5,103.19999694824219 80.5,104.19999694824219 86.5,104.19999694824219 96.5,101.19999694824219 116.5,91.19999694824219 145.5,75.19999694824219 167.5,61.19999694824219 183.5,50.19999694824219 194.5,42.19999694824219 197.5,42.19999694824219 199.5,51.19999694824219 202.5,66.19999694824219 210.5,87.19999694824219 219.5,94.19999694824219 235.5,97.19999694824219 260.5,89.19999694824219 298.5,74.19999694824219 326.5,62.19999694824219 339.5,56.19999694824219 343.5,58.19999694824219 345.5,62.19999694824219 349.5,74.19999694824219 357.5,87.19999694824219 368.5,97.19999694824219 387.5,99.19999694824219 423.5,89.19999694824219 465.5,71.19999694824219 498.5,55.19999694824219 511.5,48.19999694824219 514.5,52.19999694824219 514.5,57.19999694824219 515.5,65.19999694824219 515.5,67.19999694824219 518.5,67.19999694824219 521.5,67.19999694824219 521.5,67.19999694824219"
//handmade equivalent
// <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 300 100" width="300" height="100"><path d="M 72,77 C 72,77 72,79 72,83 72,91 74,101 76,103 80,104 86,104 96,101 116,91 146,75 168,61 184,50 194,42 198,42 200,51 202,66 210,87 220,94 236,97 260,89 298,74 326,62 340,56 344,58 346,62 350,74 358,87 368,97 388,99 424,89 466,71 498,55 512,48 514,52 514,57 516,65 516,67 518,67 522,67 522,67 " stroke-width="5.525" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path></svg>
//Now turn lPoints into svg and return
// <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 552 120" width="552" height="120">
//<path d="M 79.400,15.300 C 82.236,17.326 82.400,16.800 85.400,18.300" stroke-width="5.525" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path>
//<path d="M 85.400,18.300 C 88.884,19.030 88.736,19.326 92.400,19.300" stroke-wke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path><path d="M 510.400,91.300 C 513.725,93.769 513.448,93.840 517.400,95.300" stroke-width="4.888" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path><path d="M 517.400,95.300 C 519.870,96.105 519.725,96.269 522.400,96.300" stroke-width="5.121" stroke="rgb(0, 0, 0)" fill="none" stroke-linecap="round"></path></svg>
//https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
//NOTE: v7 sigpad is 300 by 100 pixels, however the points drawn are way larger than that so going with v8 size
//If this proves to be an issue could perhaps look for the largest coordinates then use that as the size, though I think
System.Text.StringBuilder sb = new System.Text.StringBuilder();
//open
// sb.Append("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 300 100\" width=\"300\" height=\"100\">");
sb.Append("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"");
sb.Append(width.ToString());
sb.Append("\" height=\"");
sb.Append(height.ToString());
sb.Append("\">");
//draw
sb.Append("<path d=\"M ");
sb.Append(lPoints[0].Item1.ToString());
sb.Append(",");
sb.Append(lPoints[0].Item2.ToString());
sb.Append("C ");
foreach (var p in lPoints)
{
sb.Append(p.Item1.ToString());
sb.Append(",");
sb.Append(p.Item2.ToString());
}
sb.Append("\" stroke-width=\"3\" stroke=\"rgb(0, 255, 255)\" fill=\"none\" stroke-linecap=\"round\"></path>");
//close
sb.Append("</svg>");
return "data:image/svg+xml;base64," + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(sb.ToString()));
//todo append sb.ToString(); as base64 string
}
private static Tuple<int, int> StringToPoint(string s)
{
if (string.IsNullOrWhiteSpace(s))
return new Tuple<int, int>(0, 0);
if (!s.Contains(','))
return new Tuple<int, int>(0, 0);
string[] spoint = s.Split(',');
//case 1939 - can't treat the input in the parse as integers as they can apparently contain decimals now
//I suspect that the conversion with decimal is likely slower than checking to see if it has a decimal first so
//I'm breaking this out into two separate streams for performance
if (s.Contains('.'))
{
// if it contains a decimal then convert differently
return new Tuple<int, int>(
Convert.ToInt32(Math.Round(Convert.ToDouble(spoint[0]))),
Convert.ToInt32(Math.Round(Convert.ToDouble(spoint[1])))
);
}
else
{
//if it doesn't contain a decimal then use the original method
//case 1978 wrap this in a try catch block due to one customer having NaN values
//in their signature stroke path string.
try
{
return new Tuple<int, int>(int.Parse(spoint[0]), int.Parse(spoint[1]));
}
catch (System.FormatException)
{
return new Tuple<int, int>(0, 0);
}
}
}
#endregion
}//eoc
}//ens