Skip to main content

Configuring Custom.JS

note

Before we begin changing anything, please familiarise yourself with how to create your own Custom.JS by following this link Custom CSS & JavaScript. We will also be utilising the Features Selected event.

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

First, let's update the maptaskrReady event to listen to FeaturesSelected and subsequently call the following function to handle the features for processing.

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)
{
maptaskrControl.clientValidationFunction = clientValidationFunction;
maptaskrControl.on("FeaturesSelected", async function(featureArray) {
features = JSON.parse(featureArray);
features = features.filter(x => x.attributes && x.attributes.length > 0); //ensure we only pick up layers that have attributes
await handleSelectedFeature(controlId, features);
});
}
};

Now at the top of the file, let's declare a function called handleSelectedFeature. This function will be called when a feature is selected and will target the appropriate HTML element to find a given attribute property and also render a custom button with its own onclick function that will convert the featureArray passed from the FeaturesSelected event and call addSearchPolygon.

note

In the example code below, we are specifically targetting an attribute property called tenid. This field may not exist in the layer you are using so please adjust your code accordingly.

let layerAttributePanelWatcher;

function handleSelectedFeature(controlId, features) {
return new Promise((resolve, reject) => {
let maptaskrControl = globalThis && globalThis.top && globalThis.top.maptaskrCORE && globalThis.top.maptaskrCORE[controlId];

// Return early if maptaskrControl is not available
if (!maptaskrControl) {
console.error('maptaskrControl not found.');
reject();
}

// Set a timeout for 10 seconds to find the sidebar content and disable the interval if it fails
setTimeout(() => {
console.warn('.sidebar-content-right not found within 10 seconds.');
clearInterval(layerAttributePanelWatcher);
reject();
}, 10000);

clearInterval(layerAttributePanelWatcher);
layerAttributePanelWatcher = setInterval(() => {
let sidenavContent = document.querySelector('.sidebar-content-right');
// If .sidebar-content-right is found and visible, proceed
if (sidenavContent && !sidenavContent.classList.contains('ng-hide')) {
// Remove existing button if it exists
let existingButton = sidenavContent.querySelector('#selectProperty');
if (existingButton) {
existingButton.remove();
}

// Check for 'tenid' in the sidebar content
let tenid = null;
// Ensure rows exist
tenid = findTenidInsideAttributeTable(sidenavContent);

// Ensure tenid is found before proceeding
if (!tenid) {
console.warn('tenid not found in sidebar content.');
reject();
}

// Find the feature that matches the 'tenid'
const selectedFeature = features.find(feature =>
feature.attributes?.some(attr => {
return attr.Text === 'tenid' && String(attr.Value).trim() === String(tenid).trim();
})
);

// Only proceed if a matching feature is found
if (selectedFeature) {

createButton(sidenavContent, selectedFeature, maptaskrControl);

// Resolve the promise when the button is created and ready
clearInterval(layerAttributePanelWatcher);
resolve();
} else {
console.warn('No matching feature found for tenid: ' + tenid);
reject('No matching feature found for tenid');
}
}
}, 100); // Check every 100ms
});
}

Next, we need to add a function called findTenIdInsideAttributeTable that will iterate through the attribute pop-up table and find the tenid row and return the value. We can use this to determine if the attribute pop-up is eligible to render the button, as well as generally retrieving the tenid value.

function findTenidInsideAttributeTable(sidenavContent) {
let tenid = null;
let rows = sidenavContent.querySelectorAll('table tr');
if(rows.length === 0) return tenid;
rows.forEach((row) => {
let cells = row.querySelectorAll('td');
if (cells.length > 1) {
if (cells[0].textContent.trim() === 'tenid') {
tenid = cells[1].textContent.trim();
}
}
});
return tenid;
}

Next, let's add a function called createButton that will be responsible for generating the button, its onclick logic and also retrieving the tenid value one more time to prevent accidentally picking up an old value in the case where you are continuously clicking on layer features without closing the attribute pop-up.

note

addSearchPolygon supports coordinates in either EPSG:3857 or EPSG:4326. We are passing true as a second parameter when calling addSearchPolygon because the coordinates returned by the FeaturesSelected function are in the EPSG:3857 coordinate system. If you are going to use the addSearchPolygon function separately and have coordinates in the EPSG:4326 coordinate system, you will need to pass false as the second parameter to ensure that the data is correctly transformed.

function createButton(sidenavContent, selectedFeature, maptaskrControl)
{
// Create the button element
let button = document.createElement('button');
button.id = 'selectProperty';
button.className = 'btn btn-primary';
button.type = 'button';
button.style.backgroundColor = 'var(--main-colour)';
button.style.padding = '10px';
button.style.margin = '10px';
button.style.width = 'calc(100% - 20px)';
button.style.fontSize = '14px';
button.textContent = 'Add Polygon to Search';
// Set the button's click event
button.onclick = function (event) {
event.preventDefault(); // Prevent default behavior

// Regrab the latest tenid from the element, in case it has changed
let tenid = findTenidInsideAttributeTable(sidenavContent);

// Proceed only if tenid is found
if (!tenid) {
return;
}

// Perform the action with the tenid
const geoJson = findAndConvertToGeoJSON(selectedFeature);
maptaskrControl.addSearchPolygon(geoJson, true);
};

// Add the button to the sidenav content
let targetDiv = document.querySelector(".sidebar-content-right .sidebar-section table");
if (targetDiv) {
targetDiv.insertBefore(button, targetDiv.firstChild);
}
}

Lastly, let's add another function called findAndConvertToGeoJSON. This function will accept the selectedFeature object that we extracted inside the addButtonToDialog function, and convert it to a geoJSON object that the addSearchPolygon function requires.

function findAndConvertToGeoJSON(selectedFeature) {

const geometryString = selectedFeature.geometry.geometry; // Geometry as a string
const coordinatesArray = geometryString.split(",").map(Number); // Split string and convert to numbers

// Helper to group flat array into coordinate pairs
const groupCoordinates = (flatArray) => {
const coordinates = [];
for (let i = 0; i < flatArray.length; i += 2) {
coordinates.push([flatArray[i], flatArray[i + 1]]);
}
return coordinates;
};

// Group coordinates into pairs
const groupedCoordinates = groupCoordinates(coordinatesArray);

// Split polygons based on closure (first and last coordinates matching)
const polygons = [];
let currentPolygon = [];

groupedCoordinates.forEach((coordinate) => {
currentPolygon.push(coordinate);
// If the current polygon is closed (first equals last), start a new one
if (currentPolygon.length > 1 &&
coordinate[0] === currentPolygon[0][0] &&
coordinate[1] === currentPolygon[0][1]) {
polygons.push(currentPolygon);
currentPolygon = [];
}
});


// Construct GeoJSON
const geoJSON = {
type: "Feature",
properties: {},
geometry: {
type: polygons.length > 1 ? "MultiPolygon" : "Polygon",
coordinates: polygons.length > 1 ? polygons.map(coords => [coords]) : [polygons[0]]
}
};

return geoJSON;
}

Let's see this code in action!

What a layer feature is selected, an attribute pop-up appears on the right-hand side of the screen and we should see our custom button appear on the top (as long as the layer meets our requirements of containing a tenid attribute property). Clicking this button should generate a polygon, and switch our Search panel tab to Draw. Executing search will now use this polygon as part of the search area.

Below is a gif demonstrating this functionality:

Success! Using the code snippets above we have successfully listened to the FeaturesSelected event and extended its capability to convert the geometry to a geoJSON object, generate a button and call the addSearchPolygon function to add the geometry to our search area.