Showing posts with label TypeScript. Show all posts
Showing posts with label TypeScript. Show all posts

Thursday, August 23, 2018

Decoding paths in here maps

In my panic to move my website off Google Maps, I took some shortcuts. One of those was to continue to use the helper function google.maps.geometry.encoding.decodePath. I primarily use this for decoding the paths of Strava segments, since these are encoded using the same algorithm as Google. But loading up the Google Maps API script for a single function is rather ridiculous, so I’ve converted the path decoder from the rather marvellous (and seemingly abandoned) Strava.NET library.

For your pleasure, here is some TypeScript that will create a here maps LineString from an encoded path. Other map providers are available and adjusting it for them should be fairly straightforward.

function decodePath(polylinechars: string): H.geo.LineString {
  var poly = new H.geo.LineString();

  if (polylinechars == null || polylinechars === "") {
    return poly;
  }

  var index = 0;

  var currentLat = 0;
  var currentLng = 0;

  while (index < polylinechars.length) {
    // calculate next latitude
    var sum = 0;
    var shifter = 0;
    var next5bits;
    do {
      next5bits = polylinechars.charCodeAt(index++) - 63;
      sum |= (next5bits & 31) << shifter;
      shifter += 5;
    } while (next5bits >= 32 && index < polylinechars.length);

    if (index >= polylinechars.length)
      break;

    currentLat += (sum & 1) == 1 ? ~(sum >> 1) : (sum >> 1);

    // calculate next longitude
    sum = 0;
    shifter = 0;
    do {
      next5bits = polylinechars.charCodeAt(index++) - 63;
      sum |= (next5bits & 31) << shifter;
      shifter += 5;
    } while (next5bits >= 32 && index < polylinechars.length);

    if (index >= polylinechars.length && next5bits >= 32)
      break;

    currentLng += (sum & 1) == 1 ? ~(sum >> 1) : (sum >> 1);

    poly.pushPoint(new H.geo.Point(currentLat / 100000.0, currentLng / 100000.0));
  }

  return poly;
}

Thursday, August 02, 2018

Here Maps full screen control

One thing missing from Here Maps is a full screen control, but fortunately it’s possible to add your own custom controls to the map. This isn’t brilliantly documented but a support person from Here Maps was able to help me out with an example. That combined with the fact I’d built something similar in Google Maps before it had its own full screen control. So here’s a TypeScript function that will add a full screen button to a Here Map.

function hereMapsFullScreenControl(ui: H.ui.UI) {
  // add custom UI control
  const myCustomControl = new H.ui.Control();
  myCustomControl.addClass("customControl");
  ui.addControl("myCustomControl", myCustomControl);
  // Set the position of the control in the UI's layout
  myCustomControl.setAlignment(H.ui.LayoutAlignment.TOP_RIGHT);

  const myCustomPanel = new H.ui.base.OverlayPanel();
  myCustomPanel.addClass("customPanel");
  myCustomControl.addChild(myCustomPanel);

  // store original styles
  const mapDiv = ui.getMap().getElement() as HTMLElement;
  let divStyle: CSSStyleDeclaration = mapDiv.style;
  if (mapDiv.runtimeStyle) {
    divStyle = mapDiv.runtimeStyle;
  }
  const originalPos: string = divStyle.position;
  let originalWidth: string = divStyle.width;
  let originalHeight: string = divStyle.height;
  // ie8 hack
  if (originalWidth === "") {
    originalWidth = mapDiv.style.width;
  }
  if (originalHeight === "") {
    originalHeight = mapDiv.style.height;
  }
  const originalTop: string = divStyle.top;
  const originalLeft: string = divStyle.left;
  const originalZIndex: string = divStyle.zIndex;
  let bodyStyle: CSSStyleDeclaration = document.body.style;
  if (document.body.runtimeStyle) {
    bodyStyle = document.body.runtimeStyle;
  }
  const originalOverflow: string = bodyStyle.overflow;

  const myCustomButton = new H.ui.base.PushButton({
    label: "Full screen",
    onStateChange: (evt) => {
      // OK, button state changed... if it's currently down
      if (myCustomButton.getState() === H.ui.base.Button.State.DOWN) {
        // go full screen
        mapDiv.style.position = "fixed";
        mapDiv.style.width = "100%";
        mapDiv.style.height = "100%";
        mapDiv.style.top = "0";
        mapDiv.style.left = "0";
        mapDiv.style.zIndex = "100";
        document.body.style.overflow = "hidden";
      } else {
        // exit full screen
        if (originalPos === "") {
          mapDiv.style.position = "relative";
        } else {
          mapDiv.style.position = originalPos;
        }
        mapDiv.style.width = originalWidth;
        mapDiv.style.height = originalHeight;
        mapDiv.style.top = originalTop;
        mapDiv.style.left = originalLeft;
        mapDiv.style.zIndex = originalZIndex;
        document.body.style.overflow = originalOverflow;
      }
      ui.getMap().getViewPort().resize();
    },
  });

  myCustomPanel.addChild(myCustomButton as any);
  myCustomPanel.setState(H.ui.base.OverlayPanel.State.OPEN);
}

Saturday, March 28, 2015

Loading Google Maps asynchronously

Google’s PageSpeed keeps telling me I should load scripts asynchronously to improve the performance of my website. Now in an ideal world I’d use something like RequireJS to implement this for all my scripts, but frankly that seems like a bit of a big task and more than likely I’d stuff it up and break large chunks of my web site. So I thought I’d start small and just load up Google Maps asynchronously. Google provide an example of how to do this, but I wanted to encapsulate that in a simple reusable function with a callback function parameter to run after the library had loaded. This is what I came up with in TypeScript.

function loadGoogleMaps(libraries: string, callback: () => void) {

  window["initGM"] = () => {
    callback();
  };

  var script = document.createElement("script");
  script.type = "text/javascript";
  var url: string = "http://maps.google.com/maps/api/js?callback=initGM";
  if (libraries != null && libraries !== "") {
    url += "&libraries=" + libraries;
  }
  script.src = url;
  document.body.appendChild(script);
}

Thursday, March 05, 2015

A cross browser XML parser

A post from three years ago detailing how to implement selectSingleNode for XML documents in a cross-browser friendly manner is still getting a good number of hits. Which I guess shows that developers still need to manipulate XML in browsers, even with the increasing popularity of JSON. When we started to rewrite our desktop app on the web, JSON seemed like the obvious choice, but we use XPath in a big way and we wanted our web app to be compatible with our desktop app, so we stuck with XML, which meant having to deal with the different ways XML is supported in different browsers. So we now have a reasonably well featured cross browser XML parser, the source of which you’ll find below.

A couple of things to note, this probably works in IE8 and below but I’ve never tested it since our app needs at least IE9. Also, the code is TypeScript rather than Javascript, since TypeScript is slightly less insane…

// stop TypeScript complaining about stuff we don't have definitions for
interface Window {
  DOMParser;
}
declare var XPathResult;

class XmlWrapper {
  private xmlDoc: any;
  constructor(xml: string) {
    try {
      // try Internet Explorer first. Although later versions have DOMParser, they don't implement evaluate
      this.xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
      this.xmlDoc.async = false;
      this.xmlDoc.loadXML(xml);
      this.xmlDoc.setProperty("SelectionLanguage", "XPath");
    } catch (ex) {
      if (window.DOMParser) {
        var parser = new DOMParser();
        this.xmlDoc = parser.parseFromString(xml, "text/xml");
      } else {
        throw new Error("Can't find an XML parser!");
      }
    }
  }

  public selectSingleElement(xmlNode, elementPath: string): Element {
    return <Element>this.selectSingleNode(xmlNode, elementPath);
  }

  public selectSingleNode(xmlNode, elementPath: string): Node {
    if (xmlNode == null) {
      xmlNode = this.xmlDoc;
    }

    if (this.xmlDoc.evaluate) {
      var doc = xmlNode.ownerDocument;
      if (doc == null) {
        doc = xmlNode;
      }
      var nodes = doc.evaluate(elementPath, xmlNode, null, XPathResult.ANY_TYPE, null);
      var results = nodes.iterateNext();
      return results;
    } else {
      return xmlNode.selectSingleNode(elementPath);
    }
  }

  public selectElements(xmlNode, elementPath: string): Element[] {
    return <Element[]>this.selectNodes(xmlNode, elementPath);
  }

  public selectNodes(xmlNode, elementPath: string): Node[] {
    if (xmlNode == null) {
      xmlNode = this.xmlDoc;
    }

    if (this.xmlDoc.evaluate) {
      var doc = xmlNode.ownerDocument;
      if (doc == null) {
        doc = xmlNode;
      }

      var resultsArray = [];
      var results = doc.evaluate(elementPath, xmlNode, null, XPathResult.ANY_TYPE, null);
      var thisElement = results.iterateNext();
      while (thisElement) {
        resultsArray.push(thisElement);
        thisElement = results.iterateNext();
      }

      return resultsArray;
    } else {
      return xmlNode.selectNodes(elementPath);
    }
  }

  get documentElement(): Element {
    return <Element>(this.xmlDoc.documentElement);
  }

  public xml(): string {
    // for IE
    if (this.xmlDoc.documentElement.xml) {
      return this.xmlDoc.documentElement.xml;
    }

    // Chrome, FireFox
    if (this.xmlDoc.documentElement.outerHTML) {
      return this.xmlDoc.documentElement.outerHTML;
    }

    // Safari
    return (new XMLSerializer()).serializeToString(this.xmlDoc.documentElement);
  }

  public static getNodeText(node: Node): string {
    if (node == null) {
      return "";
    }

    var value: string = node.text;
    if (node.textContent) {
      value = node.textContent;
    }

    return value;
  }

  public static setNodeText(node: Node, value: string): void {
    if (node == null) {
      return;
    }

    if (node.text !== undefined) {
      node.text = value;
    }

    if (node.textContent !== undefined) {
      node.textContent = value;
    }
  }
}