Skip to main content

Configuring Custom.JS

Below is a snippet of the Custom.JS code for your reference:

/* place your custom JS Code here. */
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.';

async function clientValidationFunction(executionContext, controlId) {
//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.

console.log('Validating shapes with:');
console.log(executionContext);
console.log(controlId);

let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];
if (!maptaskrControl) {
console.error('Maptaskr Control not found');
return;
}

//let shapes = _getShapes();
//let annotation = _getAnnotation();
//let uploads = _getUploadedFiles();
//testing shape intersections and determine what to do with them
const shapeIntersections = await maptaskrControl.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.
}

if (globalThis && globalThis.top){
globalThis.top.maptaskrReady = function (pageContext, controlId) {
console.log('Maptaskr Map ID: ' + controlId + ' has Loaded');

let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];

if (maptaskrControl) {
/* Use the following console logs to uniquely identify your map */
// console.log(pageContext);
// console.log(maptaskrControl.registeredLocation);
// console.log(maptaskrControl.webresourceLocation);

/* register the correct client validation function here */
maptaskrControl.clientValidationFunction = clientValidationFunction;

/* put your setup methods here */
/* e.g. maptaskrControl.disableSaving = true; - this will disable the inbuilt save methods, you an use maptaskrControl.saveShapes() to save your own shapes.*/

/* put your event registrations here. */
/* e.g. maptaskrControl.on("FeaturesSelected", ...) */
}
};
}

To achieve shapefile conversion and saving to the root record, we need to add a couple of extra functions.

At the very bottom of the file, let's declare a variable that we will use to track the shapes that need to be saved.

//local variable used to determine if we need to save new drawn attachments.
let shapesToBeSaved = [];

The original source code for Custom.JS contains a block of code at the bottom that waits for the Maptaskr map to be ready using the maptaskrReady extension function. Within this block we need to add a piece of code to retrieve a third-party library called shpwrite. We also want to include two additional extension functions to listen to when shapes are drawn and when Maptaskr finishes its saving, namely ShapeDrawingCompleted and SavingCompleted. The ShapeDrawingCompleted function fires when a user finishes drawing a shape and we append the shape name into the shapesToBeSaved array we defined earlier. The SavingCompleted function fires when Maptaskr finishes its standard saving methods, we will utilise this to execute a secondary function which will convert the drawn shapes to shapefiles and save them into Dynamics.

note

Please ensure that retrieving third-party libraries does not breach any corporate policy that may be in place for your implementation.

Additionally, during testing, please ensure that the shpwrite script loads correctly, and does not get rejected by CORS policies that may be in place.

if (globalThis && globalThis.top) {
globalThis.top.maptaskrReady = function (pageContext, controlId) {
//load the shape library and attach the drawn shape as a SHP file attachment.
fetch('https://unpkg.com/@mapbox/shp-write@0.4.3/shpwrite.js')
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then((scriptContent) => {
// Once script content is fetched, you can append it to a script tag and add it to the document's head
const scriptElement = document.createElement('script');
scriptElement.textContent = scriptContent;
document.head.appendChild(scriptElement);
})
.catch((error) => {
console.error('Error fetching script:', error);
});
console.log('Maptaskr Map ID: ' + controlId + ' has Loaded');

let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];

if (maptaskrControl) {
maptaskrControl.clientValidationFunction = clientValidationFunction;

maptaskrControl.on('ShapeDrawingCompleted', function (shapeName, finishLat, finishLong, polygon) {
console.log('Shape Drawing Completed - We need to save: ' + shapeName);
shapesToBeSaved.push(shapeName);
});

maptaskrControl.on('SavingCompleted', function () {
console.log('Shape Drawing Completed - Saving Shape Files');
attachDrawnShapeFilesAsShapeFileAttachment(controlId);
});
}
};
}

Now let's add the shape conversion function. This will grab the relevant record data from the form context, grab the drawn shapes using the getDrawnShapes extension function. Then we iterate through drawn shapes and compare against the list of shape names we stored in the shapesToBeSaved array (This ensures we pick up shapes that are still present on the map and haven't been deleted since the firing of ShapeDrawingCompleted).

Next, the coordinate reference for the shapes is converted from EPSG:3857 to EPSG:4326 to be compatible with other GIS tools.

note

This conversion can be changed to suit your businesses specification.

Finally, we use the shpwrite function to convert the drawn shape into a shapefile and create a request to save an annotation containing the .zip shapefile directly to the record.

function attachDrawnShapeFilesAsShapeFileAttachment(controlId) {
// Get the record ID, entity name and attachment URL
var recordId = Xrm.Page.data.entity.getId().replace('{', '').replace('}', '');
let entityName = Xrm.Page.data.entity.getEntityName();
var attachFileUrl = Xrm.Utility.getGlobalContext().getClientUrl() + '/api/data/v9.1/annotations';

//load all drawn shapes
let drawnShapes = window.top.maptaskrCORE[controlId].getDrawnShapes();

//for each drawn shape
drawnShapes.forEach((s) => {
if (shapesToBeSaved.find((sname) => sname == s.DocumentType)) {
console.log('Saving Shape file for: ' + s.DocumentType);

//convert the drawn shapes to EPSG:4326 from EPSG:3857
s.features[0].geometry.coordinates[0].forEach((c) => {
//convert the coordinate to latlng.
let latlng = window.top.maptaskrCORE[controlId].getLatLongFromCoordinate(c[0], c[1]);
c[0] = latlng.longitude;
c[1] = latlng.latitude;
});

//create a ZIP file of shape data.
shpwrite.zip(s).then((zipData) => {
//once you have the zipData as a Base64 file, you can send it anywhere, We will attach it to the record.

// Construct the XMLHTTP request to upload the zip file
var xhr = new XMLHttpRequest();
xhr.open('POST', attachFileUrl, true);
xhr.setRequestHeader('OData-MaxVersion', '4.0');
xhr.setRequestHeader('OData-Version', '4.0');
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
xhr.setRequestHeader('Prefer', 'return=representation');

// Set the request body with the zip file as base64 string
var body = JSON.stringify({
['objectid_' + entityName + '@odata.bind']: '/' + entityName + 's(' + recordId + ')', //note the "S" for the pluralname of the entity
objecttypecode: entityName,
subject: s.DocumentType,
documentbody: zipData,
filename: s.DocumentType.replace(' ', '_') + '.zip',
mimetype: 'application/zip',
});

// Send the request
xhr.send(body);
});
}
});
}

Let's see this new functionality in action!

First, let's draw a shape on the map:

After finishing drawing, click on Save on the record's ribbon.

We've opened the developer toolbar to make sure we're seeing the console.log that have been included in the code. As shown below we can see that the shape is being picked up by Custom.JS.

Next, let's refresh the record and go to the timeline control to see if the shapefile has been correctly saved.

Lastly, let's check that this shapefile is working in other GIS tools. For this we will use QGIS.

Success! Using the code snippets above we have successfully grabbed the drawn shape, converted it to a shapefile and saved it against the record.