Dynamically Map an Area of an Image using JavaScript/jQuery Tutorial

In this tutorial, you will learn how to Dynamically Map an Area of an Image using JavaScript/jQuery. This will help you to understand also on how to put a detection link on a specific part or area of the image. With the use of HTML Canvas, we can dynamically locate the specific position and size of the area we want to detect on the image.

Below, I have provided a simple source code of a simple web application. The application displays an image and allows users to add details on a specific area of an image. The user can simply click the "Map Area" button to start to draw in the image that will identify the area on the image, Next, the user will click the "Save Mapped Area" button to open a form modal where he/she will put the area details. Then, after saving the details the page will be reloaded and the mapped area will become a clickable area. When the area has been clicked, a modal will show on the window that displays the area details. Users can also update the area details and delete the mapped area.

Getting Started

The source code I created below, I used Bootstrap version 5 as the CSS Framework and jQuery Library. Please download the said framework and library in or display and run the application properly. The following are the links of the framework and library website to download.

After downloading the framework and library, compile them inside a single directory that will serve as your source code folder. Also, add an image file in the directory because the application does not have an upload feature.

Creating the Interface

The below code is a HTML Script that contains the user interfaces codes of the application. This holds the image, canvas, map, area, and modal HTML tag. Open a text editor and copy the script below and paste in into a new html file. Next, change the image source according to the image file location you desired to use. Save the file as index.html.

  1. <!DOCTYPE html>
  2. <html lang="en">
  3.  
  4.     <meta charset="UTF-8">
  5.     <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7.     <title>Mapping</title>
  8.     <link rel="stylesheet" href="./css/bootstrap.min.css">
  9.     <script src="./js/jquery-3.6.0.min.js"></script>
  10.     <script src="./js/bootstrap.min.js"></script>
  11.     <script src="./js/script.js"></script>
  12.     <!-- Custom CSS -->
  13.     <style>
  14.          :root {
  15.             --bs-success-rgb: 71, 222, 152 !important;
  16.         }
  17.        
  18.         html,
  19.         body {
  20.             height: 100%;
  21.             width: 100%;
  22.             font-family: Apple Chancery, cursive;
  23.         }
  24.        
  25.         #fp-canvas-container {
  26.             height: 65vh;
  27.             width: calc(100%);
  28.             position: relative;
  29.         }
  30.        
  31.         .fp-img,
  32.         .fp-canvas,
  33.         .fp-canvas-2 {
  34.             position: absolute;
  35.             width: calc(100%);
  36.             height: calc(100%);
  37.             top: 0;
  38.             left: 0;
  39.             z-index: 1;
  40.         }
  41.        
  42.         #fp-map {
  43.             position: absolute;
  44.             width: calc(100%);
  45.             height: calc(100%);
  46.             top: 0;
  47.             left: 0;
  48.             z-index: 1;
  49.         }
  50.        
  51.         .fp-canvas {
  52.             z-index: 2;
  53.             background: #0000000d;
  54.             cursor: crosshair;
  55.         }
  56.        
  57.         #fp-map {
  58.             z-index: 2;
  59.         }
  60.        
  61.         area {
  62.             position: absolute;
  63.         }
  64.        
  65.         area:hover {
  66.             background: #cbcbcb0f;
  67.         }
  68.        
  69.         #save,
  70.         #cancel {
  71.             display: none;
  72.         }
  73.     </style>
  74.     <!-- End of Custom Css -->
  75. </head>
  76.  
  77. <body class="bg-light">
  78.     <nav class="navbar navbar-expand-lg navbar-dark bg-primary bg-gradient" id="topNavBar">
  79.         <div class="container">
  80.             <a class="navbar-brand" href="https://sourcecodester.com">
  81.             Sourcecodester
  82.             </a>
  83.         </div>
  84.     </nav>
  85.     <div class="container py-3" id="page-container">
  86.         <h3>Dynamically Map Area on an Image</h3>
  87.         <hr>
  88.         <div class="content">
  89.             <div class="row my-2">
  90.                 <!-- Action Buttons -->
  91.                 <div class="col-md-12">
  92.                     <button class="btn btn-primary rounded-0" type="button" id="map_area">Map Area</button>
  93.                     <button class="btn btn-primary rounded-0" type="button" id="save">Save Mapped Area</button>
  94.                     <button class="btn btn-secondary rounded-0" type="button" id="cancel">Cancel</button>
  95.                 </div>
  96.                 <!-- End ofAction Buttons -->
  97.             </div>
  98.             <!-- Image/Canvas/Map Container -->
  99.             <div id="fp-canvas-container">
  100.                 <map name="fp-map" id="fp-map">
  101.                     </map>
  102.                 <img src="./devices.jpg" alt="Floor Plan" class='fp-img' id="fp-img" usemap="#fp-map">
  103.                 <canvas class="fp-canvas d-none" id="fp-canvas"></canvas>
  104.             </div>
  105.         </div>
  106.         <!-- End of Image/Canvas/Map Container -->
  107.     </div>
  108.     </div>
  109.     <!-- Mapped Area Add/Edit Details Form Modal -->
  110.     <div class="modal fade" id="form_modal" role='dialog' data-bs-backdrop="static" data-bs-keyboard="true">
  111.         <div class="modal-dialog modal-md modal-dialog-centered" role="document">
  112.             <div class="modal-content">
  113.                 <div class="modal-header">
  114.                     <h5 class="modal-title">Mapped Area Details</h5>
  115.                 </div>
  116.                 <div class="modal-body">
  117.                     <form action="" id="mapped-form">
  118.                         <input type="hidden" name="id" value="">
  119.                         <input type="hidden" name="coord_perc" value="">
  120.                         <div class="form-group">
  121.                             <label for="description" class="control-label text-primary">Description</label>
  122.                             <textarea name="description" id="description" cols="30" rows="4" class="form-control rounded-0"></textarea>
  123.                         </div>
  124.                     </form>
  125.                 </div>
  126.                 <div class="modal-footer py-1">
  127.                     <button type="submit" class="btn btn-sm rounded-0 btn-primary" form="mapped-form">Save</button>
  128.                     <button type="button" class="btn btn-sm rounded-0 btn-secondary" data-bs-dismiss="modal">Close</button>
  129.                 </div>
  130.             </div>
  131.         </div>
  132.     </div>
  133.     <!--End of Mapped Area Add/Edit Details Form Modal -->
  134.  
  135.     <!--Mapped Area View Details Modal -->
  136.     <div class="modal fade" id="view_modal" role='dialog' data-bs-backdrop="static" data-bs-keyboard="true">
  137.         <div class="modal-dialog modal-md modal-dialog-centered" role="document">
  138.             <div class="modal-content">
  139.                 <div class="modal-header">
  140.                     <h5 class="modal-title">Area Details</h5>
  141.                 </div>
  142.                 <div class="modal-body">
  143.  
  144.                 </div>
  145.                 <div class="modal-footer py-1">
  146.                     <button type="button" class="btn btn-sm rounded-0 btn-primary" data-id='' id="edit-area">Edit</button>
  147.                     <button type="button" class="btn btn-sm rounded-0 btn-danger" data-id='' id="delete-area">Delete</button>
  148.                     <button type="button" class="btn btn-sm rounded-0 btn-secondary" data-bs-dismiss="modal">Close</button>
  149.                 </div>
  150.             </div>
  151.         </div>
  152.     </div>
  153.     <!--End of Mapped Area View Details Modal -->
  154. </body>
  155.  
  156. </html>

Creating the Main Script

The below script is a JavaScript codes that contains the drawing, map area creation, buttons actions, and modal triggers codes. Save the following file as script.js.

  1. // Canvas Position Variables
  2. var cposX = 0,
  3.     cposY = 0;
  4. // Mouse Down Position Variables
  5. var posX = 0,
  6.     posY = 0;
  7. // Mouse Moving Position Variables
  8. var nposX = 0,
  9.     nposY = 0;
  10. // Coordinate Position in Percentage Variables
  11. var px1_perc = 0,
  12.     py1_perc = 0,
  13.     px2_perc = 0,
  14.     py2_perc = 0;
  15. // Canvas Variable
  16. var ctx;
  17. // Variable for checking if the mouse has started to draw or not
  18. var isDraw = false;
  19. // Auto Increment sequence for the Store Data Identification [id]
  20. var AI_seq = $.parseJSON(localStorage.getItem('seq')) || 0;
  21. // Stored Data [Mapped Area Details]
  22. var stored = $.parseJSON(localStorage.getItem('mapped')) || {};
  23.  
  24.  
  25. // Function that creates the Area tag and appends to the Map Tag
  26. function mapped_area() {
  27.     if (Object.keys(stored).length > 0) {
  28.         // Empty Map Tag First
  29.         $('#fp-map').html('')
  30.  
  31.         // Looping Data
  32.         Object.keys(stored).map(k => {
  33.             // Loop Current Data
  34.             var data = stored[k]
  35.                 // Creating New Area Tag
  36.             var area = $("<area shape='rect'>")
  37.             area.attr('href', "javascript:void(0)")
  38.                 // Coordinate Percentage
  39.             var perc = data.coord_perc
  40.             perc = perc.replace(" ", '')
  41.             perc = perc.split(",")
  42.  
  43.             // Configuring Area Position, Height, and Width
  44.             var x = $('#fp-img').width() * perc[0];
  45.             var y = $('#fp-img').height() * perc[1];
  46.             var width = Math.abs(($('#fp-img').width() * Math.abs(perc[2])) - x);
  47.             var height = Math.abs(($('#fp-img').height() * Math.abs(perc[3])) - y);
  48.             if (($('#fp-img').width() * perc[2]) - x < 0)
  49.                 x = x - width
  50.             if (($('#fp-img').height() * perc[3]) - y < 0)
  51.                 y = y - height
  52.             area.attr('coords', x + ", " + y + ", " + width + ", " + height)
  53.             area.addClass('fw-bolder text-muted')
  54.             area.css({
  55.                 'height': height + 'px',
  56.                 'width': width + 'px',
  57.                 'top': y + 'px',
  58.                 'left': x + 'px',
  59.             })
  60.  
  61.             $('#fp-map').append(area)
  62.  
  63.             // Action to make if the Area Tag has been clicked
  64.             area.click(function() {
  65.                 $('#view_modal').find('#edit-area,#delete-area').attr('data-id', data.id)
  66.                 data.description = data.description.replace(/\n/gi, "<br>")
  67.                 $('#view_modal').find('.modal-body').html(data.description)
  68.                 $('#view_modal').modal('show')
  69.             })
  70.         })
  71.     }
  72. }
  73.  
  74. $(function() {
  75.     cposX = $('#fp-canvas')[0].getBoundingClientRect().x
  76.     cposY = $('#fp-canvas')[0].getBoundingClientRect().y
  77.     ctx = $('#fp-canvas')[0].getContext('2d');
  78.  
  79.     // Creates the Map Area on the Image
  80.     mapped_area()
  81.  
  82.     // Re-initialize Map Area Creation when the window has been resized
  83.     $(window).on('resize', function() {
  84.         mapped_area()
  85.     })
  86.  
  87.     // Event Listener when the mouse is clicked on the canvas area
  88.     $('.fp-canvas').on('mousedown', function(e) {
  89.         px1_perc = (e.clientX - cposX) / $('#fp-canvas').width()
  90.         py1_perc = (e.clientY - cposY) / $('#fp-canvas').height()
  91.         posX = $('#fp-canvas')[0].width * ((e.clientX - cposX) / $('#fp-canvas').width());
  92.         posY = $('#fp-canvas')[0].height * ((e.clientY - cposY) / $('#fp-canvas').height());
  93.         isDraw = true
  94.     })
  95.  
  96.     // Event Listener when the mouse is moving on the canvas area. For drawing the rectangular Area
  97.     $('.fp-canvas').on('mousemove', function(e) {
  98.             if (isDraw == false)
  99.                 return false;
  100.             nposX = $('#fp-canvas')[0].width * ((e.clientX - cposX) / $('#fp-canvas').width());
  101.             nposY = $('#fp-canvas')[0].height * ((e.clientY - cposY) / $('#fp-canvas').height());
  102.             var height = nposY - posY;
  103.             var width = nposX - posX;
  104.             ctx.clearRect(0, 0, $('.fp-canvas')[0].width, $('.fp-canvas')[0].height);
  105.             ctx.beginPath();
  106.             ctx.lineWidth = ".3";
  107.             ctx.strokeStyle = "pink";
  108.             ctx.rect(posX, posY, width, height);
  109.             ctx.stroke();
  110.         })
  111.         // Event Listener when the mouse is up on the canvas area. End of Drawing
  112.     $('.fp-canvas').on('mouseup', function(e) {
  113.         px2_perc = (e.clientX - cposX) / $('#fp-canvas').width()
  114.         py2_perc = (e.clientY - cposY) / $('#fp-canvas').height()
  115.         nposX = $('#fp-canvas')[0].width * ((e.clientX - cposX) / $('#fp-canvas').width());
  116.         nposY = $('#fp-canvas')[0].height * ((e.clientY - cposY) / $('#fp-canvas').height());
  117.         var height = nposY - posY;
  118.         var width = nposX - posX;
  119.  
  120.         ctx.clearRect(0, 0, $('.fp-canvas')[0].width, $('.fp-canvas')[0].height);
  121.         ctx.beginPath();
  122.         ctx.lineWidth = ".3";
  123.         ctx.strokeStyle = "pink";
  124.         ctx.rect(posX, posY, width, height);
  125.         ctx.stroke();
  126.         isDraw = false
  127.     })
  128.  
  129.  
  130.     // Action when Map Are button is clicked
  131.     $('#map_area').click(function() {
  132.             $(this).hide('slow')
  133.             $('#save,#cancel').show('slow')
  134.             $('#fp-canvas').removeClass('d-none')
  135.             cposX = $('#fp-canvas')[0].getBoundingClientRect().x
  136.             cposY = $('#fp-canvas')[0].getBoundingClientRect().y
  137.         })
  138.         // Action when Map Are cancel is clicked
  139.     $('#cancel').click(function() {
  140.             $('#save,#cancel').hide('slow')
  141.             $('#map_area').show('slow')
  142.             $('#fp-canvas').addClass('d-none')
  143.         })
  144.         // Action when Map Are save is clicked
  145.     $('#save').click(function() {
  146.         var cP = px1_perc + ", " + py1_perc + ", " + px2_perc + ", " + py2_perc
  147.         $('#form_modal').find('input[name="coord_perc"]').val(cP)
  148.         $('#form_modal').modal('show')
  149.     })
  150.  
  151.     // Saving the Mapped Area Details on local Storage
  152.     $('#mapped-form').submit(function(e) {
  153.             e.preventDefault();
  154.             var data;
  155.             var id = $(this).find('[name="id"]').val()
  156.             var coord_perc = $(this).find('[name="coord_perc"]').val()
  157.             var description = $(this).find('[name="description"]').val()
  158.  
  159.             if (id == '') {
  160.                 id = AI_seq + 1;
  161.                 localStorage.setItem('seq', id)
  162.             }
  163.             data = { id: id, description: description, coord_perc: coord_perc }
  164.             stored[id] = data;
  165.             localStorage.setItem('mapped', JSON.stringify(stored))
  166.             alert("Mapped Area Successfully saved.")
  167.             location.reload()
  168.         })
  169.         // Edit Mapped Area Details
  170.     $('#edit-area').click(function() {
  171.             $('.modal').modal('hide')
  172.             id = $(this).attr('data-id')
  173.             data = stored[id] || {}
  174.             $('#mapped-form').find('[name="id"]').val(data.id)
  175.             $('#mapped-form').find('[name="coord_perc"]').val(data.coord_perc)
  176.             $('#mapped-form').find('[name="description"]').val(data.description)
  177.             $('#form_modal').modal('show')
  178.         })
  179.         // Delete Mapped Area
  180.     $('#delete-area').click(function() {
  181.         $('.modal').modal('hide')
  182.         id = $(this).attr('data-id')
  183.         data = stored[id] || {}
  184.         var _conf = confirm("Are you sure to delete the selected mapped area?")
  185.         if (_conf === true) {
  186.             if (!!stored[id])
  187.                 delete stored[id];
  188.         }
  189.         localStorage.setItem('mapped', JSON.stringify(stored))
  190.         alert("Selected Mapped Area Successfully Deleted.")
  191.         location.reload()
  192.     })
  193. })

That's it. You can now try to run the application in your browser and see if it works as it was planned to. If you found any errors, kindly review the source code on your end. You can also download the working source code I created for this tutorial. The download button is located below this article.

DEMO VIDEO

That is the end of this tutorial. I hope you will find this tutorial useful for your future web application projects. Explore more on this website for more Tutorials and Free Source Codes.

Happy Coding :)

Add new comment