Skip to main content

Appendix B: Custom JavaScript

Initmap.js

/* 
* VARIABLES FOR THE INITMAPS
*/
const maptaskrEntityDefinition = [
{
pathName: "create-case", //url of the page the PCF control is loaded on
relationshipName: "contoso_Incident", //case sensitive, recommended to use the lookup field logical name
entityName: "incident", //case sensitive, recommended to use the lookup field logical name
entityPluralName: "incidents" //case sensitive
},
{
pathName: "edit-case", //url of the page the PCF control is loaded on
relationshipName: "contoso_Incident", //case sensitive, recommended to use the lookup field logical name
entityName: "incident", //case sensitive, recommended to use the lookup field logical name
entityPluralName: "incidents" //case sensitive
},
{
pathName: "account", //url of the page the PCF control is loaded on
relationshipName: "maptaskr_Account", //case sensitive, recommended to use the lookup field logical name
entityName: "account", //case sensitive, recommended to use the lookup field logical name
entityPluralName: "accounts" //case sensitive
}
]
const onValidateFailErrorMessage = 'Please upload a file or draw a shape'; //error message to display when the map validation errors
const onUploadFailErrorMessage = 'Something has gone wrong during submission, please check your connection and try again.';
const onIntersectionMessage = 'Intersections / Exclusions have been found, please check your shapes and try again.';

const clientValidationFunction = async() => {
//user defined validation function, can use the following for getting the list of shapes, annotations and uploaded files.
//if the user needs to fix the shapes, throw an error.

//let shapes = _getShapes();
//let annotation = _getAnnotation();
//let uploads = _getUploadedFiles();

//testing shape intersections and determine what to do with them
const shapeIntersections = await _getShapeIntersections();
if (shapeIntersections && shapeIntersections.length > 0){

if (shapeIntersections.some(res => res.intersectionType == 'Warning'))
{
//decide what to do with warnings
//if you want them resolved, throw an error here..
//throw new Error(onIntersectionMessage);
}
if (shapeIntersections.some(res => res.intersectionType == 'Error'))
{
//decide what to do with warnings
//if you want them resolved, throw an error here..
throw new Error(onIntersectionMessage);
}
if (shapeIntersections.some(res => res.intersectionType == 'Exclusion'))
{
//decide what to do with warnings
//if you want them resolved, throw an error here..
throw new Error(onIntersectionMessage);
}
}

//you can also test to make sure your shapes are in the correct position, orintation, contained within eachother, any geometric tests here.

//shapes will come in the format:
// {
// "type": "FeatureCollection",
// "features": [
// {
// "type": "Feature",
// "geometry": {
// "type": "Polygon",
// "coordinates": [
// [
// [
// 12899598.276481498,
// -3758060.96802893
// ],
// ...
// ]
// ]
// },
// "properties": {
// "uploadDocType": "Envelope",
// "markerType": "MARKER_SHAPE"
// }
// }
// ],
// "DocumentType": "Envelope",
// "annotationId": "1ffb72d6-c7c3-ed11-83fd-002248e1bcf1",
// "longlat": [
// 12899440.776481498,
// -3758143.46802893
// ],
// "styleProperty": {
// "geometry_": null,
// "fill_": {
// "color_": "rgba(149,255,0,0.1)"
// },
// ...
// }
// }

//if you require a specific subset of objects, please look into the shapes, annotations, or uploads to ensure specific number of shapes or named shapes are included.
}
/*
* STOP EDITING HERE - Unless you require customized validation and saving functionality.
*
*
*/

(function (webapi, $) {
function safeAjax(ajaxOptions) {
var deferredAjax = $.Deferred();
shell.getTokenDeferred().done(function (token) {
// add headers for AJAX
if (!ajaxOptions.headers) {
$.extend(ajaxOptions, {
headers: {
"__RequestVerificationToken": token
}
});
} else {
ajaxOptions.headers["__RequestVerificationToken"] = token;
}
$.ajax(ajaxOptions)
.done(function (data, textStatus, jqXHR) {
validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
}).fail(deferredAjax.reject); //AJAX
}).fail(function () {
deferredAjax.rejectWith(this, arguments); // on token error pass the token AJAX and args
});
return deferredAjax.promise();
}
webapi.safeAjax = safeAjax;
})(window.webapi = window.webapi || {}, jQuery);

//helpers to get the current entity and id
function _getCurrentEntityName() {
var currentPageEntity = maptaskrEntityDefinition.filter((x) => window.location.pathname.includes(x.pathName));

if (currentPageEntity.length == 1)
return currentPageEntity[0];

return { pathname: null, entityPluralName: null };
}
function _getParentId(checkProposal) {
var url = new URL(window.location.href);
const urlSearchParams = new URLSearchParams(url.search);
var parentId = $("#EntityFormView_EntityID").val();
if (checkProposal) {
var recordId = $("#id").val();
}
if (recordId != null || recordId != undefined) { parentId = recordId; }
if (!parentId) { //if you want to use custom query params to read ID of the record
var parentId = urlSearchParams.get("id");
}
return parentId;
}


//VALIDATE SHAPE CONTENT
async function validateAnnotationsAndShapes() {
window.maptaskrPCF.handleLoadingSpinner(true, "Validating shape...");
try {
await clientValidationFunction();
return true;
}
catch(err) {
showPortalWarning(err);
}
return false;
}
//SAVE CONTENT
function saveAnnotationsAndShapes() {
return new Promise(async (resolve, reject) => {
var parentId = _getParentId(false);
try {
let clientValidationSuccess = await validateAnnotationsAndShapes();
if (clientValidationSuccess)
{
window.maptaskrPCF.handleLoadingSpinner(true, "Saving shape...");
//if we have any lambpetIds - we have already loaded objects.
if (_getShapeLambpetIds() && _getShapeLambpetIds().length > 0) {
await _deleteRemovedShapes();
}
await _addNewShapes(parentId);
resolve();
}
else
{
reject();
}
} catch (err) {
showPortalWarning(onUploadFailErrorMessage);
reject(err)
}
});
}

//functions for deleting, adding, and creating in D365.
function _deleteRemovedShapes() {
return new Promise(async (resolve, reject) => {
var remainingShapes = _getShapes();
var remainingAnnotation = _getAnnotation();
var lambpets = _getShapeLambpetIds();

for (var i = 0; i < lambpets.length; i++) {
var existingAnnotationId = lambpets[i].id.split("___")[1]; //used in comparison against existing shapes
let existingLambpetId = lambpets[i].id.split("___")[0]; //used in the delete functionality
let existingShapeName = lambpets[i].name;
if (existingAnnotationId) {
try {
let shapeStillExists = false;

//if the user has deleted this lambpet... remove it from the system
if (remainingShapes && remainingShapes.filter((s)=> s.annotationId == existingAnnotationId)?.length > 0)
{
shapeStillExists = true;
}

//if the user has deleted this lambpet... remove it from the system
if (remainingAnnotation && remainingAnnotation.annotationId == existingAnnotationId)
{
shapeStillExists = true;
}

if (!shapeStillExists)
{
setProgressDialogMessage(`Deleting existing '${existingShapeName}'...`);
await webapi.safeAjax({
type: "DELETE",
url: `/_api/maptaskr_lambpets(${existingLambpetId})`,
contentType: "application/json",
});
window.maptaskrPCF.fire("LambpetDeleted", existingLambpetId);
}
} catch (err) {
reject(err);
}
}
}
//if we get through all shapes to be deleted, and succeed.
resolve();
});
}
async function _addNewShapes(parentId){
try{
await _addLambpets(parentId, _getShapes(true), true); //add shapes (as children)

//if (annotation is new)
let annotation = _getAnnotation();
if (annotation && annotation.annotationId == null)
{
await _addLambpet(parentId, annotation, false); //add the NEW annotations as D365
}
await _attachOriginalFilesToEntity(parentId, _getUploadedFiles());
return;
}catch(err){
throw err;
}
}

function _addLambpets(entityId, areas) {
var promises = [];
for (var i = 0; i < areas.length; i++) {
promises.push(_addLambpet(entityId, areas[i]));
}
return Promise.all(promises);
}
function _addLambpet(parentId, area, isChildShape = true) {
var tableNames = _getCurrentEntityName();
var data = {
"maptaskr_name": area.DocumentType,
"maptaskr_parententityid": parentId,
"maptaskr_json": area.styleProperty ? JSON.stringify(area.styleProperty) : "null",
"maptaskr_ischild": isChildShape,
"maptaskr_longitude": area.longlat[0],
"maptaskr_latitude": area.longlat[1],
};
data[`${tableNames.relationshipName}@odata.bind`] = `/${tableNames.entityPluralName}(${parentId})`;
setProgressDialogMessage(`Adding new '${area.DocumentType}'...`);
return new Promise((resolve, reject) => {
webapi.safeAjax({
type: "POST",
url: "/_api/maptaskr_lambpets",
contentType: "application/json",
data: JSON.stringify(data),
success: function (res, status, xhr) {
var lambpetId = xhr.getResponseHeader("entityid");
window.maptaskrPCF.fire("LambpetCreated", lambpetId, JSON.stringify(data));
_createAnnotation(lambpetId, JSON.stringify(area), area).then(() => {
resolve();
}).catch((err) => {
reject(err);
});
},
error: function () {
reject();
}
});
});

}
function _createAnnotation(parentId, geojson, rawgeojson) {
var data = {
filename: rawgeojson.DocumentType,
mimetype: "geojson",
documentbody: strToBase64(geojson),
objecttypecode: "maptaskr_lambpet",
subject: rawgeojson.DocumentType
};
data["objectid_maptaskr_lambpet@odata.bind"] =
"/maptaskr_lambpets(" + parentId + ")";
return new Promise((resolve, reject) => {
webapi.safeAjax({
type: "POST",
url: "/_api/annotations",
contentType: "application/json",
data: JSON.stringify(data),
success: function (res, status, xhr) {
var annotationId = xhr.getResponseHeader("entityid");
window.maptaskrPCF.fire("AnnotationCreated", annotationId, JSON.stringify(data));
resolve();
},
error: function () {
reject();
}
});
});
}
function _createAnnotationForParent(parentId, utf8encodedFile, file) {
var tableNames = _getCurrentEntityName();
var data = {
filename: `${file.name}`,
mimetype: `${file.type}`,
documentbody: window.btoa(utf8encodedFile),
objecttypecode: `${tableNames.entityName}`,
};
data[`objectid_${tableNames.entityName}@odata.bind`] = `/${tableNames.entityPluralName}(${parentId})`;
return new Promise((resolve, reject) => {
webapi.safeAjax({
type: "POST",
url: "/_api/annotations",
contentType: "application/json",
data: JSON.stringify(data),
success: function (res, status, xhr) {
var annotationId = xhr.getResponseHeader("entityid");
window.maptaskrPCF.fire("OriginalFileUploaded", annotationId, JSON.stringify(data));
resolve();
},
error: function () {
reject();
}
});
});
}
function _attachOriginalFilesToEntity(entityId, files) {
return new Promise(async (resolve, reject) => {
if (files.length == 0) {
resolve(); //resolve if no files as this function gets called regardless if there are files or not
return;
}
var uploadedOriginalShapes = 0;
for (var i = 0; i < files.length; i++) {
console.log('File', files[i]);
var file = files[i];
if (file) {
try {
var utf8DocBody = await _readFileAsBufferToUTF8(file);
await _createAnnotationForParent(entityId, utf8DocBody, file);
uploadedOriginalShapes++;
} catch (err) {
reject(err);
return;
}
}
}
if (uploadedOriginalShapes == files.length) {
resolve();
} else {
reject();
}
});
}

//Region to get shapes, annotations and files from the PCF.
function _getShapeLambpetIds() {
return window.maptaskrPCF.getLambpetIds();
}
function _getShapes(getNewShapesOnly = false) {
let uploadedPolygons = window.maptaskrPCF.getUploadedShapes();
let drawnPolygons = window.maptaskrPCF.getDrawnShapes();

let shapes = [];
uploadedPolygons.forEach(element => {
if (!getNewShapesOnly || element.annotationId == null)
shapes.push(element);
});
drawnPolygons.forEach(element => {
if (!getNewShapesOnly || element.annotationId == null)
shapes.push(element);
});
return shapes;
}

function _getAnnotation() {
return window.maptaskrPCF.getAnnotation();
}
function _getUploadedFiles() {
return window.maptaskrPCF.getUploadedFiles();
}

// helper function for loading a file as a UTF8 array.
function _readFileAsBufferToUTF8(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(_arrayBufferToUTF8(reader.result));
reader.readAsArrayBuffer(file);
reader.onerror = (error) => reject(error);
});
}
function _arrayBufferToUTF8(buffer) { // Convert Array Buffer to UTF8 string
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return binary;
}

//ui helpers for showing warnings and enabling buttons.
function showPortalWarning(msg) {
window.maptaskrPCF.showWarningMessage(msg, 5000);
$('html, body').animate({
'scrollTop': $('.validation-map').position().top
});
}
function enableButtons() {
var inputs = document.getElementsByTagName("input");
for (var i = 0, j = inputs.length; i < j; i++) {
if (inputs[i].type === 'submit' || inputs[i].type === 'button') {
inputs[i].disabled = false;
}
}
}

function _getShapeIntersections() {
return new Promise(async (resolve, reject) => {
try {
const response = await window.maptaskrPCF.getShapeIntersections();
resolve(response);
} catch {
reject();
}
});
}

//conversionHelper for the InitMap

function strToBase64(str) {
return window.btoa(unescape(encodeURIComponent(str)))
}

function setProgressDialogMessage(message) {
window.maptaskrPCF.updateLoadingSpinnerMessage(message)
}

Form Submit Button

//id of the button to invoke save on.
const buttonId = "NextButton";
let button;
let buttonName;

function validateAndSave() {
//validate our shapes.
var buttonValue = ''
clearIsDirty();
disableButtons();
buttonValue = button.value;
button.value = 'Processing...';

//if our shapes validate, start the upload, and return "false" allowing the async process to continue.
//async upload shape files
saveAnnotationsAndShapes().then(()=> {
//once uploaded, skip validation and postback the button
//invoke on the main thread.
setProgressDialogMessage("Saving complete!");
setTimeout(function () {
WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(buttonName, "", true, "", "", false, true));
}, 100);
}).catch(() => {
clearIsDirty();
enableButtons();
button.value = buttonValue;
window.maptaskrPCF.handleLoadingSpinner(false);
});
return false;
}
//ADVANCED and BASIC combined
//once the document is loaded
$(document).ready(function () {

//find the button and save the name for posting back the form
button = document.getElementById(buttonId);
buttonName = button.getAttribute("name");

//if there is already a webFormClientValidate function
if (typeof (webFormClientValidate) == "function") {
//save the original func
var baseValidationFunction = webFormClientValidate;
//wrap it in out new method
webFormClientValidate = function (arguments) {
//apply the original webFormClientValidate function
if (baseValidationFunction.apply(this, arguments)) {
validateAndSave();
}
return false;
}
}
else {
webFormClientValidate = validateAndSave;
}

if (typeof (entityFormClientValidate) == "function") {
//save the original func
var baseValidationFunction = entityFormClientValidate;
//wrap it in out new method
entityFormClientValidate = function (arguments) {
//apply the original webFormClientValidate function
if (baseValidationFunction.apply(this, arguments)) {
validateAndSave();
}
return false;
}
}
else {
entityFormClientValidate = validateAndSave;
}
});