Skip to content

SMap Architecture

Lindsey Jacks edited this page Jun 30, 2017 · 1 revision

Javascript Environment

Organization

Purpose specific: If a JS folder serves a similar purpose to other JS files, they should be grouped together in a subfolder. All form validation files should go inside /parsley. Any additions to dataTables should go in the /dataTable folder.

Vendor Folders: Anything that comes from a third party library. Didn't build it yourself? Copied and pasted from a GitHub repo? Put it in the vendor folder.

There is a vendor folder inside each JS subfolder. If you're working on a specific project, it should have it's own folder, and each third party file you add should go in the vendor file located inside that.

SMap

index.js : Initiates the map, the editor, the router, and the SMap. Contains one event listener that triggers only once when the map has fully loaded.

/dist: this is where the min files end up after they're compiled by Grunt. These should not be changed manually.

/edit: Everything for adding/editing geometries in leaflet go here. It uses L.Editable.

  • controls.js : add functionality to the buttons that appear on the left hand side.
  • editor.js : All of the logic that makes adding/editing geometries possible. Augments L.Editable, and includes custom event handlers which can be triggered. Notes inline about what actions trigger what functions and what they do.
  • styles.js : Where all geometry styles are established. If you want to adjust a polygon coloration, this is the place.
  • tooltip.js : Logic/language for the tooltip that appears when you click on one of the toolbar buttons.
  • /vendor :
    • LatLngUtils.js : use for simple replacement/saving of geometry coordinates.
    • Leaflet.Editable.js : main tool for editing geometries. Replaced Leaflet.Draw
    • leaflet.toolbar.js : main repo for the toolbar that appears during editing.
    • path.drag.js : Makes geometries draggable.

/map: Everything else needed to make the map work that isn't editing/adding.

  • map.js : Contains the SMap() function. The geojsonTileLayer is initiated here, project extents are established, spatial resources are rendered, and any leaflet augmentations need to be initiated here.
  • lazytiles.js : Used to check if a parent tile has already been loaded, in which case, don't load the child tile.
  • /vendor:
    • catiline.js : the web worker used to make tile loading non blocking. Not totally sure this works….
    • L.TileLayer.GeoJSON.js : All of the logic for loading vector tiles. Could use some improvements (i.e. not loading tiles that have already been zoomed out of, moved past)

/router: All of the logic to update the side panel and modal in the project overview.

  • CreateRoutes.js : Each django view/template, needs it's own url hash, and it's own route. CreateRoutes constructs a dictionary object with each window.location.hash corresponding with a specific set of actions.

    function route(path, el, controller, eventHook) {
        routes[path] = {
            el: el,
            controller: controller,
            eventHook: eventHook
        };
    }
    
  • RouterMixins.js : Everything but the kitchen sink. State updates, DOM manipulation, Event Hooks, Ajax form submissions, all of that goes here. There are detailed notes about updatePage and updateState inline in RouterMixins.js

    • DOM MANIPULATION : Anything that affects the physical appears of the page

    • UPDATING THE STATE : Things that affect how the router is able to function.

    • ADDING EVENT HOOKS : Because the html doesn't exist when the JS files initially run, if there are event hooks involved (i.e. $('#delete-location').on('click', function (e) {})), you have to rerun the JS functions after the router is called.

    • INTERCEPTING FORM SUBMISSIONS : Hooks into any form in a router page and allows them to be submitted via AJAX.

      • Your submit button needs to have a submit-btn class, and a formaction="{{ submit_url }}"
      • Your cancel link needs to be <a hrf="{{ cancel_url }}">

      submit_url should be the async url that handles the form submission

      cancel_url is the hash where you would like to redirect to if a user cancels the form.

  • SimpleRouter.js : What happens when the hash changes. Parses and formats that hash to be figure out which router to call in the dictionary created by CreateRoutes, then sends an async request for the django view and the template. Handles any errors/permissions/redirects as well.

  • /vendor:

    • L.Hash.js : adds coordinates to the end of the hash so users can share specific locations.

HTML

When you load the page, you're loading the ProjectMap view (project_map.html). Think of this as the skeleton that holds SMap together. Any css/script files you need for ANY of the views need to be loaded here OR loaded dynamically in smap/router/RouterMixins.js.

If there are django context variables that you need to use in the js files, you'll need to create a global JS variable in project_map.html. For any other router html file (i.e. location_detail.html) Django context variables will still be translated, but any JS variables will be loaded as TEXT, not JS, so they won't work. smap.min.js needs to be loaded after any of these global variable are defined.

<div id="mapid"> contains the leaflet map.

<div id='project-detail'> : this is the white detail box to the left of the map. It's empty because it's dynamically updated if the hash changes. If you'd like to add a new html page for this detail page, it should only contain the html necessary to fill this box. No wrappers, no extensions, just the barebones html required.

The final new html feature is the additional-modals div, which is located in project_wrapper.html. This has the same concept as the project-detail but it's used for any modal form that appears (i.e. party_add.html, resource_add.html, location_delete.html, etc);

When you're creating a new view, you'll by asked to specify whether it's a "detail" or "modal". This is what it's referring to: Do you want it in the project-detail div, or the additional-modals div?

Router Walkthrough:

A user arrives at their project page. This calls the ProjectMap view, which calls project_map.html. An event listener in index.js is established for the window.location.hash. The project overview is called by default if there is no hash.

The user then clicks on a location. The geometry has a 'url' property that contains the hash for that location (if it is a newly added location, the url will be set in setCurrentLocationFeature in router/RouterMixin.js) The window.location.hash is set to this url property: #/records/location/<location_id>/

Original: /organizations/<org-id>/projects/<proj-id>/records/location/<location-id>/#/records/location/<location_id>/?coords=x/y/z/

Hash: #/records/location/<location_id>/?coords=x/y/z/

The event listener picks up on this, and calls router() in router/SimpleRouter.js.

The hash is then parsed to remove any coordinates that were appended automatically (these are not needed for routing).

New Hash: /records/location/<location_id>/

Once the hash is cleaned up, the async url is created by combining async/ + location.path + new_hash.

Async: async/organizations/<org-id>/projects/<proj-id>/records/location/<location-id>/

router then checks if this newly formatted hash exists in the routes dictionary created in router/CreateRoutes.js . Since routes are generic, if the hash contains an id, it'll return undefined. If the resulting route variable is null, we then remove the ID.

New New Hash: /records/location/

The resulting route will have four properties:

route['/records/location/'] = {
  el: 'detail',
  controller: function () {
            rm.updatePage({
                'page_title': options.trans.location_detail,
                'display_detail_panel': true,
                'display_modal': false,
            });
        },
  eventHandler: function () {
            rm.updateState({
                'current_location': window.location.hash,
                'datatable': true,
                'detach_forms': true,
                'active_tab': 'overview',
            });

            rm.locationDetailHooks();
            map.locationEditor.fire('route:location:detail');
        });
}

An AJAX call is made to the newly formed async url, which returns an html snippet. route.el tells it which DOM element to update (in this case, it's the detail panel). If there are no permission errors, the route controller is called to make sure all of the appropriate DOM elements are visible (or hidden in the case of the modal). Then the innerHTML is set to the AJAX response.

Once the HTML is updated, the route eventHandler is called. The eventHandler contains all of the actions that aren't possible until the HTML snippet is accessible. From here we can set the current location and establish a link to the geometry for easier navigation, call any javascript files necessary (in this case, call dataTables files, and add eventListeners to all of the resource detach buttons). In this case, the location detail page has its own set of unique hooks that are called after upateState. This will enable xlang.js, and add functionality to the tabs for "overview", "relationships" and resources".

If a user then decides to add a relationship, the hash will change to #/records/location/<location-id>/relationships/new/, which after formatting will return this:

route('/records/location/relationships/new/', 'modal',
        function () {
            rm.updatePage({
                'page_title': options.trans.rel_add,
                'display_detail_panel': true,
                'display_modal': true,
            });

        },
        function () {
            rm.updateState({
                'current_location': window.location.hash,
                'active_tab': 'relationships',
                'form': {
                    'type': 'modal',
                    'success_url': 'location',
                    'tab': 'relationships',
                    'callback': rm.relationshipAddHooks,
                },
            });
            rm.relationshipAddHooks();
        });

The only added feature is the form property in the eventHandler. This tells any route with a form attached where the form is located (in this case, in the 'modal'), where to redirect to after a successful submission (in this case, the 'location' detail page), and which tab should be active (if applicable, and in this case 'relationships'). The 'callback' property should be the same as the eventHook called after updateState. If a form submission fails, the HTML is updated, thus removing any eventHooks, so they need to be called again.

When a form is submitted, it's intercepted. The forms formation contains the async url (in this case /async/organizations/<org-id>/projects/<proj-id>/records/location/<location-id>/relationships/new/ which is the url the AJAX function calls. If the form fails, the DOM element contain the form's innerHTML is replaced with the response, and the callback is called. If the form is successful, the hash is updated with the success_url: #/records/location/<location-id>/?tab=relationships/.

Grunt

Grunt is currently setup to run whenever you run ./runserver . It will minify files (run concat and uglify), and watch. It's currently only watching changes inside of SMap, but anytime you change a file it will automatically run concat and uglify again, so all you have to do is refresh the page.

Commands:

grunt : Minifies files and runs JSHint.

grunt runserver : Minifies files and watches for changes.

grunt production : Only minifies files.

Clone this wiki locally