diff --git a/Goui.Plugin.Maps/Goui.Plugin.Maps.csproj b/Goui.Plugin.Maps/Goui.Plugin.Maps.csproj new file mode 100644 index 0000000..f43101e --- /dev/null +++ b/Goui.Plugin.Maps/Goui.Plugin.Maps.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + diff --git a/Goui.Plugin.Maps/Html/GoogleMap.cs b/Goui.Plugin.Maps/Html/GoogleMap.cs new file mode 100644 index 0000000..028affb --- /dev/null +++ b/Goui.Plugin.Maps/Html/GoogleMap.cs @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Goui; +using Goui.Html; + +namespace Goui.Plugin.Maps.Html { + public class GoogleMap : Div { + #region Classes and Enums + /// + /// Supported Map Types by the Google Maps API + /// + public enum MapType { + /// + /// normal, default 2D map + /// + ROADMAP, + /// + /// photographic map + /// + SATELLITE, + /// + /// photographic map + roads and city names + /// + HYBRID, + /// + /// map with mountains, rivers, etc. + /// + TERRAIN, + } + + public class MapMarker : IEquatable { + public double lat; + public double lng; + public string title; + public MapInfoWindow infoWindow; + + public bool Equals(MapMarker other) { + if (lat == other.lat && lng == other.lng && title == other.title && (infoWindow?.content == other.infoWindow?.content)) { + return true; + } + return base.Equals(other); + } + } + + public class Position { + public double latitude; + public double longitude; + } + + public class MapInfoWindow { + public string content; + } + #endregion + + #region Private Globals + private readonly bool _firstMapControlOnPage; + private Position _position; + private MapType _mapType; + private System.Collections.Generic.List Markers { get; } = new System.Collections.Generic.List(); + #endregion + + #region Public Globals + public string APIKey { get; set; } + #endregion + + #region Constructors + public GoogleMap(string apiKey = "", Position startPos = null, MapType mapType = MapType.ROADMAP,/*string mapId = "basic_map",*/ bool firstMapControlOnPage = true) { + //Id = mapId; + _firstMapControlOnPage = firstMapControlOnPage; + APIKey = apiKey; + _position = startPos ?? new Position { + latitude = 51.5073346, + longitude = -0.1276831, + }; + _mapType = mapType; + } + #endregion + + #region Overrides + public override void WriteInnerHtml(XmlWriter w) { + base.WriteInnerHtml(w); + } + + public override void WriteOuterHtml(XmlWriter w) { + base.WriteOuterHtml(w); + + if (_firstMapControlOnPage) { + WriteScriptsOnFirstInstance(w); + } else { + WriteMapScript(w); + } + } + + public override string OuterHtml { + get { + return base.OuterHtml + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + ; + } + } + + public override string Text { + get { + return base.Text; + } + set { + base.Text = value; + } + } + + public override string ToString() { + if (_firstMapControlOnPage) { + return $@"{base.ToString()} + + + +" + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + ; + //"; + } + return $@"{base.ToString()} +" + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + ; + } + + #endregion + + #region Public Methods + public void AddMarker(MapMarker marker) { + try { + Markers.Add(marker); + Send(Message.Event(Id, "addMarker", marker)); + } catch (Exception ex) { + Console.WriteLine(ex.Message); + throw; + } + } + + public void CenterOn(Position position) { + try { + _position = position; + Send(Message.Event(Id, "setCenter", _position)); + } catch (Exception ex) { + Console.WriteLine(ex.Message); + throw; + } + + } + + public async Task GetCenter()//This requires the map to have loaded already + { + try { + Position pos = null; + await Task.Run(() => { + var mre = new ManualResetEvent(false); + CentreReceived += GoogleMap_CentreReceived; + Send(Message.Event(Id, "raiseCenter")); + mre.WaitOne(90000);//90 Second Timeout + + void GoogleMap_CentreReceived(object sender, DOMEventArgs e) { + CentreReceived -= GoogleMap_CentreReceived; + pos = new Position { + latitude = (double)e.Data["latitude"], + longitude = (double)e.Data["longitude"], + }; + mre.Set(); + } + }); + return pos ?? _position; + } catch (Exception e) { + Console.WriteLine(e.Message); + return _position; + } + } + + public void RemoveMarker(MapMarker mapMarker) { + try { + var index = Markers.IndexOf(Markers.First(m => m.Equals(mapMarker))); + Markers.Remove(mapMarker); + Send(Message.Event(Id, "removeMarker", index)); + } catch (Exception ex) { + Console.WriteLine(ex.Message); + throw; + } + } + + public void ClearMarkers() { + try { + Markers.Clear(); + Send(Message.Event(Id, "clearMarkers")); + } catch (Exception ex) { + Console.WriteLine(ex.Message); + throw; + } + } + + public void ChangeMapType(MapType mapType) { + try { + _mapType = mapType; + Send(Message.Event(Id, "changeMapType", mapType.ToString())); + } catch (Exception ex) { + Console.WriteLine(ex.Message); + throw; + } + } + + #endregion + + #region Private Events + private event DOMEventHandler CentreReceived { + add => AddEventListener("centerRaised", value); + remove => RemoveEventListener("centerRaised", value); + } + #endregion + + #region Private Methods + private void WriteScriptsOnFirstInstance(XmlWriter w) { + w.WriteString(@" +"); + w.WriteStartElement("script"); + w.WriteAttributeString("src", "http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js"); + w.WriteString(@" +"); + w.WriteFullEndElement(); + + w.WriteString(@" +"); + w.WriteStartElement("script"); + w.WriteAttributeString("type", "text/javascript"); + w.WriteAttributeString("src", $"http://maps.google.com/maps/api/js?key={APIKey}"); + w.WriteString(@" +"); + w.WriteFullEndElement(); + + w.WriteString(@" +"); + /*w.WriteStartElement("script"); + w.WriteRaw(_gmaps); + w.WriteString(@" +"); + w.WriteFullEndElement();*/ + + WriteMapScript(w); + } + + private void WriteMapScript(XmlWriter w) { + w.WriteString(@" +"); + w.WriteStartElement("script"); + w.WriteRaw(ReplaceTokens(MapsScript)); + w.WriteString(@" +"); + w.WriteFullEndElement(); + } + + private string ReplaceTokens(string tokenizedString) { + return tokenizedString + .Replace("$$Map_ID$$", Id); + } + + private string GetMarkers() { + string markers = string.Empty; + foreach (var marker in Markers) { + markers += $@" + map.addMarker({Newtonsoft.Json.JsonConvert.SerializeObject(marker)}); +"; + } + return markers; + } + #endregion + + #region JavaScript Scripts + private string MapsScript => $@" +$(document).ready(function () {{ + + var mapOwner = $('#$$Map_ID$$'); + mapOwner.height('500px'); + var map = new GMaps({{ + el: '#$$Map_ID$$', + lat: {_position.latitude.ToString().Replace(',', '.')}, + lng: {_position.longitude.ToString().Replace(',', '.')}, + zoom: 12, + zoomControl: true, + zoomControlOpt: {{ + style: 'SMALL', + position: 'TOP_LEFT' + }}, + panControl: false, + mapType: '{_mapType.ToString()}', + }}); + {GetMarkers()} + mapOwner.on('clearMarkers', clearMarkersEventRaised); + function clearMarkersEventRaised(e) {{ + console.log ('Clearing Map markers',map.markers); + var markerLength = map.markers.length; + for (var i = 0, len = markerLength; i < len; i++) {{ + map.removeMarker(map.markers[0]); + }} + console.log ('After clearing map markers',map.markers); + }} + mapOwner.on('changeMapType', changeMapTypeEventRaised); + function changeMapTypeEventRaised(e) {{ + console.log ('Changing map type',e.originalEvent.detail,e.originalEvent,e); + var center = map.getCenter(); + var coords = {{ + latitude: center.lat(), + longitude: center.lng(), + }}; + var arr = map.markers; + map = new GMaps({{ + el: '#$$Map_ID$$', + lat: coords.latitude, + lng: coords.longitude, + zoom: 12, + zoomControl: true, + zoomControlOpt: {{ + style: 'SMALL', + position: 'TOP_LEFT' + }}, + panControl: false, + mapType: e.originalEvent.detail, + }}); + for (var i = 0, len = arr.length; i < len; i++) {{ + map.addMarker(arr[i]); + }} + }} + mapOwner.on('addMarker', addMarkerEventRaised); + function addMarkerEventRaised(e) {{ + console.log ('Adding Map marker',e.originalEvent.detail,e.originalEvent,e); + map.addMarker(e.originalEvent.detail); + }} + mapOwner.on('removeMarker', removeMarkerEventRaised); + function removeMarkerEventRaised(e) {{ + console.log ('Removing Map marker',e.originalEvent.detail,e.originalEvent,e); + map.removeMarker(map.markers[e.originalEvent.detail]); + }} + mapOwner.on('setCenter', setCenterEventRaised); + function setCenterEventRaised(e) {{ + console.log ('Setting Map Center',e.originalEvent.detail,e.originalEvent,e); + map.setCenter(e.originalEvent.detail.latitude, e.originalEvent.detail.longitude); + }} + mapOwner.on('raiseCenter', raiseCenterEventRaised); + function raiseCenterEventRaised(e) {{ + + console.log ('Getting Map Center'); + var center = map.getCenter(); + var coords = {{ + latitude: center.lat(), + longitude: center.lng(), + }}; + console.log ('Current Map Center',center); + var eventMsg = new CustomEvent('centerRaised', {{ detail: coords }}); + //mapOwner.trigger( 'centerRaised', [ coords ] ); + mapOwner[0].dispatchEvent(eventMsg); + }} +}}); +"; + #endregion + } +} diff --git a/Goui.Plugin.Maps/Js/GoogleMaps.js b/Goui.Plugin.Maps/Js/GoogleMaps.js new file mode 100644 index 0000000..2f7dc8c --- /dev/null +++ b/Goui.Plugin.Maps/Js/GoogleMaps.js @@ -0,0 +1,2417 @@ +"use strict"; +(function (root, factory) { + if (typeof exports === 'object') { + module.exports = factory(); + } + else if (typeof define === 'function' && define.amd) { + define(['jquery', 'googlemaps!'], factory); + } + else { + root.GMaps = factory(); + } + + +}(this, function () { + + /*! + * GMaps.js v0.4.25 + * http://hpneo.github.com/gmaps/ + * + * Copyright 2017, Gustavo Leon + * Released under the MIT License. + */ + + var extend_object = function (obj, new_obj) { + var name; + + if (obj === new_obj) { + return obj; + } + + for (name in new_obj) { + if (new_obj[name] !== undefined) { + obj[name] = new_obj[name]; + } + } + + return obj; + }; + + var replace_object = function (obj, replace) { + var name; + + if (obj === replace) { + return obj; + } + + for (name in replace) { + if (obj[name] != undefined) { + obj[name] = replace[name]; + } + } + + return obj; + }; + + var array_map = function (array, callback) { + var original_callback_params = Array.prototype.slice.call(arguments, 2), + array_return = [], + array_length = array.length, + i; + + if (Array.prototype.map && array.map === Array.prototype.map) { + array_return = Array.prototype.map.call(array, function (item) { + var callback_params = original_callback_params.slice(0); + callback_params.splice(0, 0, item); + + return callback.apply(this, callback_params); + }); + } + else { + for (i = 0; i < array_length; i++) { + callback_params = original_callback_params; + callback_params.splice(0, 0, array[i]); + array_return.push(callback.apply(this, callback_params)); + } + } + + return array_return; + }; + + var array_flat = function (array) { + var new_array = [], + i; + + for (i = 0; i < array.length; i++) { + new_array = new_array.concat(array[i]); + } + + return new_array; + }; + + var coordsToLatLngs = function (coords, useGeoJSON) { + var first_coord = coords[0], + second_coord = coords[1]; + + if (useGeoJSON) { + first_coord = coords[1]; + second_coord = coords[0]; + } + + return new google.maps.LatLng(first_coord, second_coord); + }; + + var arrayToLatLng = function (coords, useGeoJSON) { + var i; + + for (i = 0; i < coords.length; i++) { + if (!(coords[i] instanceof google.maps.LatLng)) { + if (coords[i].length > 0 && typeof (coords[i][0]) === "object") { + coords[i] = arrayToLatLng(coords[i], useGeoJSON); + } + else { + coords[i] = coordsToLatLngs(coords[i], useGeoJSON); + } + } + } + + return coords; + }; + + var getElementsByClassName = function (class_name, context) { + var element, + _class = class_name.replace('.', ''); + + if ('jQuery' in this && context) { + element = $("." + _class, context)[0]; + } else { + element = document.getElementsByClassName(_class)[0]; + } + return element; + + }; + + var getElementById = function (id, context) { + var element, + id = id.replace('#', ''); + + if ('jQuery' in window && context) { + element = $('#' + id, context)[0]; + } else { + element = document.getElementById(id); + }; + + return element; + }; + + var findAbsolutePosition = function (obj) { + var curleft = 0, + curtop = 0; + + if (obj.getBoundingClientRect) { + var rect = obj.getBoundingClientRect(); + var sx = -(window.scrollX ? window.scrollX : window.pageXOffset); + var sy = -(window.scrollY ? window.scrollY : window.pageYOffset); + + return [(rect.left - sx), (rect.top - sy)]; + } + + if (obj.offsetParent) { + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + } + + return [curleft, curtop]; + }; + + var GMaps = (function (global) { + "use strict"; + + var doc = document; + /** + * Creates a new GMaps instance, including a Google Maps map. + * @class GMaps + * @constructs + * @param {object} options - `options` accepts all the [MapOptions](https://developers.google.com/maps/documentation/javascript/reference#MapOptions) and [events](https://developers.google.com/maps/documentation/javascript/reference#Map) listed in the Google Maps API. Also accepts: + * * `lat` (number): Latitude of the map's center + * * `lng` (number): Longitude of the map's center + * * `el` (string or HTMLElement): container where the map will be rendered + * * `markerClusterer` (function): A function to create a marker cluster. You can use MarkerClusterer or MarkerClustererPlus. + */ + var GMaps = function (options) { + + if (!(typeof window.google === 'object' && window.google.maps)) { + if (typeof window.console === 'object' && window.console.error) { + console.error('Google Maps API is required. Please register the following JavaScript library https://maps.googleapis.com/maps/api/js.'); + } + + return function () { }; + } + + if (!this) return new GMaps(options); + + options.zoom = options.zoom || 15; + options.mapType = options.mapType || 'roadmap'; + + var valueOrDefault = function (value, defaultValue) { + return value === undefined ? defaultValue : value; + }; + + var self = this, + i, + events_that_hide_context_menu = [ + 'bounds_changed', 'center_changed', 'click', 'dblclick', 'drag', + 'dragend', 'dragstart', 'idle', 'maptypeid_changed', 'projection_changed', + 'resize', 'tilesloaded', 'zoom_changed' + ], + events_that_doesnt_hide_context_menu = ['mousemove', 'mouseout', 'mouseover'], + options_to_be_deleted = ['el', 'lat', 'lng', 'mapType', 'width', 'height', 'markerClusterer', 'enableNewStyle'], + identifier = options.el || options.div, + markerClustererFunction = options.markerClusterer, + mapType = google.maps.MapTypeId[options.mapType.toUpperCase()], + map_center = new google.maps.LatLng(options.lat, options.lng), + zoomControl = valueOrDefault(options.zoomControl, true), + zoomControlOpt = options.zoomControlOpt || { + style: 'DEFAULT', + position: 'TOP_LEFT' + }, + zoomControlStyle = zoomControlOpt.style || 'DEFAULT', + zoomControlPosition = zoomControlOpt.position || 'TOP_LEFT', + panControl = valueOrDefault(options.panControl, true), + mapTypeControl = valueOrDefault(options.mapTypeControl, true), + scaleControl = valueOrDefault(options.scaleControl, true), + streetViewControl = valueOrDefault(options.streetViewControl, true), + overviewMapControl = valueOrDefault(overviewMapControl, true), + map_options = {}, + map_base_options = { + zoom: this.zoom, + center: map_center, + mapTypeId: mapType + }, + map_controls_options = { + panControl: panControl, + zoomControl: zoomControl, + zoomControlOptions: { + style: google.maps.ZoomControlStyle[zoomControlStyle], + position: google.maps.ControlPosition[zoomControlPosition] + }, + mapTypeControl: mapTypeControl, + scaleControl: scaleControl, + streetViewControl: streetViewControl, + overviewMapControl: overviewMapControl + }; + + if (typeof (options.el) === 'string' || typeof (options.div) === 'string') { + if (identifier.indexOf("#") > -1) { + /** + * Container element + * + * @type {HTMLElement} + */ + this.el = getElementById(identifier, options.context); + } else { + this.el = getElementsByClassName.apply(this, [identifier, options.context]); + } + } else { + this.el = identifier; + } + + if (typeof (this.el) === 'undefined' || this.el === null) { + throw 'No element defined.'; + } + + window.context_menu = window.context_menu || {}; + window.context_menu[self.el.id] = {}; + + /** + * Collection of custom controls in the map UI + * + * @type {array} + */ + this.controls = []; + /** + * Collection of map's overlays + * + * @type {array} + */ + this.overlays = []; + /** + * Collection of KML/GeoRSS and FusionTable layers + * + * @type {array} + */ + this.layers = []; + /** + * Collection of data layers (See {@link GMaps#addLayer}) + * + * @type {object} + */ + this.singleLayers = {}; + /** + * Collection of map's markers + * + * @type {array} + */ + this.markers = []; + /** + * Collection of map's lines + * + * @type {array} + */ + this.polylines = []; + /** + * Collection of map's routes requested by {@link GMaps#getRoutes}, {@link GMaps#renderRoute}, {@link GMaps#drawRoute}, {@link GMaps#travelRoute} or {@link GMaps#drawSteppedRoute} + * + * @type {array} + */ + this.routes = []; + /** + * Collection of map's polygons + * + * @type {array} + */ + this.polygons = []; + this.infoWindow = null; + this.overlay_el = null; + /** + * Current map's zoom + * + * @type {number} + */ + this.zoom = options.zoom; + this.registered_events = {}; + + this.el.style.width = options.width || this.el.scrollWidth || this.el.offsetWidth; + this.el.style.height = options.height || this.el.scrollHeight || this.el.offsetHeight; + + google.maps.visualRefresh = options.enableNewStyle; + + for (i = 0; i < options_to_be_deleted.length; i++) { + delete options[options_to_be_deleted[i]]; + } + + if (options.disableDefaultUI != true) { + map_base_options = extend_object(map_base_options, map_controls_options); + } + + map_options = extend_object(map_base_options, options); + + for (i = 0; i < events_that_hide_context_menu.length; i++) { + delete map_options[events_that_hide_context_menu[i]]; + } + + for (i = 0; i < events_that_doesnt_hide_context_menu.length; i++) { + delete map_options[events_that_doesnt_hide_context_menu[i]]; + } + + /** + * Google Maps map instance + * + * @type {google.maps.Map} + */ + this.map = new google.maps.Map(this.el, map_options); + + if (markerClustererFunction) { + /** + * Marker Clusterer instance + * + * @type {object} + */ + this.markerClusterer = markerClustererFunction.apply(this, [this.map]); + } + + var buildContextMenuHTML = function (control, e) { + var html = '', + options = window.context_menu[self.el.id][control]; + + for (var i in options) { + if (options.hasOwnProperty(i)) { + var option = options[i]; + + html += '
  • ' + option.title + '
  • '; + } + } + + if (!getElementById('gmaps_context_menu')) return; + + var context_menu_element = getElementById('gmaps_context_menu'); + + context_menu_element.innerHTML = html; + + var context_menu_items = context_menu_element.getElementsByTagName('a'), + context_menu_items_count = context_menu_items.length, + i; + + for (i = 0; i < context_menu_items_count; i++) { + var context_menu_item = context_menu_items[i]; + + var assign_menu_item_action = function (ev) { + ev.preventDefault(); + + options[this.id.replace(control + '_', '')].action.apply(self, [e]); + self.hideContextMenu(); + }; + + google.maps.event.clearListeners(context_menu_item, 'click'); + google.maps.event.addDomListenerOnce(context_menu_item, 'click', assign_menu_item_action, false); + } + + var position = findAbsolutePosition.apply(this, [self.el]), + left = position[0] + e.pixel.x - 15, + top = position[1] + e.pixel.y - 15; + + context_menu_element.style.left = left + "px"; + context_menu_element.style.top = top + "px"; + + // context_menu_element.style.display = 'block'; + }; + + this.buildContextMenu = function (control, e) { + if (control === 'marker') { + e.pixel = {}; + + var overlay = new google.maps.OverlayView(); + overlay.setMap(self.map); + + overlay.draw = function () { + var projection = overlay.getProjection(), + position = e.marker.getPosition(); + + e.pixel = projection.fromLatLngToContainerPixel(position); + + buildContextMenuHTML(control, e); + }; + } + else { + buildContextMenuHTML(control, e); + } + + var context_menu_element = getElementById('gmaps_context_menu'); + + setTimeout(function () { + context_menu_element.style.display = 'block'; + }, 0); + }; + + /** + * Add a context menu for a map or a marker. + * + * @param {object} options - The `options` object should contain: + * * `control` (string): Kind of control the context menu will be attached. Can be "map" or "marker". + * * `options` (array): A collection of context menu items: + * * `title` (string): Item's title shown in the context menu. + * * `name` (string): Item's identifier. + * * `action` (function): Function triggered after selecting the context menu item. + */ + this.setContextMenu = function (options) { + window.context_menu[self.el.id][options.control] = {}; + + var i, + ul = doc.createElement('ul'); + + for (i in options.options) { + if (options.options.hasOwnProperty(i)) { + var option = options.options[i]; + + window.context_menu[self.el.id][options.control][option.name] = { + title: option.title, + action: option.action + }; + } + } + + ul.id = 'gmaps_context_menu'; + ul.style.display = 'none'; + ul.style.position = 'absolute'; + ul.style.minWidth = '100px'; + ul.style.background = 'white'; + ul.style.listStyle = 'none'; + ul.style.padding = '8px'; + ul.style.boxShadow = '2px 2px 6px #ccc'; + + if (!getElementById('gmaps_context_menu')) { + doc.body.appendChild(ul); + } + + var context_menu_element = getElementById('gmaps_context_menu'); + + google.maps.event.addDomListener(context_menu_element, 'mouseout', function (ev) { + if (!ev.relatedTarget || !this.contains(ev.relatedTarget)) { + window.setTimeout(function () { + context_menu_element.style.display = 'none'; + }, 400); + } + }, false); + }; + + /** + * Hide the current context menu + */ + this.hideContextMenu = function () { + var context_menu_element = getElementById('gmaps_context_menu'); + + if (context_menu_element) { + context_menu_element.style.display = 'none'; + } + }; + + var setupListener = function (object, name) { + google.maps.event.addListener(object, name, function (e) { + if (e == undefined) { + e = this; + } + + options[name].apply(this, [e]); + + self.hideContextMenu(); + }); + }; + + //google.maps.event.addListener(this.map, 'idle', this.hideContextMenu); + google.maps.event.addListener(this.map, 'zoom_changed', this.hideContextMenu); + + for (var ev = 0; ev < events_that_hide_context_menu.length; ev++) { + var name = events_that_hide_context_menu[ev]; + + if (name in options) { + setupListener(this.map, name); + } + } + + for (var ev = 0; ev < events_that_doesnt_hide_context_menu.length; ev++) { + var name = events_that_doesnt_hide_context_menu[ev]; + + if (name in options) { + setupListener(this.map, name); + } + } + + google.maps.event.addListener(this.map, 'rightclick', function (e) { + if (options.rightclick) { + options.rightclick.apply(this, [e]); + } + + if (window.context_menu[self.el.id]['map'] != undefined) { + self.buildContextMenu('map', e); + } + }); + + /** + * Trigger a `resize` event, useful if you need to repaint the current map (for changes in the viewport or display / hide actions). + */ + this.refresh = function () { + google.maps.event.trigger(this.map, 'resize'); + }; + + /** + * Adjust the map zoom to include all the markers added in the map. + */ + this.fitZoom = function () { + var latLngs = [], + markers_length = this.markers.length, + i; + + for (i = 0; i < markers_length; i++) { + if (typeof (this.markers[i].visible) === 'boolean' && this.markers[i].visible) { + latLngs.push(this.markers[i].getPosition()); + } + } + + this.fitLatLngBounds(latLngs); + }; + + /** + * Adjust the map zoom to include all the coordinates in the `latLngs` array. + * + * @param {array} latLngs - Collection of `google.maps.LatLng` objects. + */ + this.fitLatLngBounds = function (latLngs) { + var total = latLngs.length, + bounds = new google.maps.LatLngBounds(), + i; + + for (i = 0; i < total; i++) { + bounds.extend(latLngs[i]); + } + + this.map.fitBounds(bounds); + }; + + /** + * Center the map using the `lat` and `lng` coordinates. + * + * @param {number} lat - Latitude of the coordinate. + * @param {number} lng - Longitude of the coordinate. + * @param {function} [callback] - Callback that will be executed after the map is centered. + */ + this.setCenter = function (lat, lng, callback) { + this.map.panTo(new google.maps.LatLng(lat, lng)); + + if (callback) { + callback(); + } + }; + + /** + * Return the HTML element container of the map. + * + * @returns {HTMLElement} the element container. + */ + this.getElement = function () { + return this.el; + }; + + /** + * Increase the map's zoom. + * + * @param {number} [magnitude] - The number of times the map will be zoomed in. + */ + this.zoomIn = function (value) { + value = value || 1; + + this.zoom = this.map.getZoom() + value; + this.map.setZoom(this.zoom); + }; + + /** + * Decrease the map's zoom. + * + * @param {number} [magnitude] - The number of times the map will be zoomed out. + */ + this.zoomOut = function (value) { + value = value || 1; + + this.zoom = this.map.getZoom() - value; + this.map.setZoom(this.zoom); + }; + + var native_methods = [], + method; + + for (method in this.map) { + if (typeof (this.map[method]) == 'function' && !this[method]) { + native_methods.push(method); + } + } + + for (i = 0; i < native_methods.length; i++) { + (function (gmaps, scope, method_name) { + gmaps[method_name] = function () { + return scope[method_name].apply(scope, arguments); + }; + })(this, this.map, native_methods[i]); + } + }; + + return GMaps; + })(this); + + GMaps.prototype.createControl = function (options) { + var control = document.createElement('div'); + + control.style.cursor = 'pointer'; + + if (options.disableDefaultStyles !== true) { + control.style.fontFamily = 'Roboto, Arial, sans-serif'; + control.style.fontSize = '11px'; + control.style.boxShadow = 'rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px'; + } + + for (var option in options.style) { + control.style[option] = options.style[option]; + } + + if (options.id) { + control.id = options.id; + } + + if (options.title) { + control.title = options.title; + } + + if (options.classes) { + control.className = options.classes; + } + + if (options.content) { + if (typeof options.content === 'string') { + control.innerHTML = options.content; + } + else if (options.content instanceof HTMLElement) { + control.appendChild(options.content); + } + } + + if (options.position) { + control.position = google.maps.ControlPosition[options.position.toUpperCase()]; + } + + for (var ev in options.events) { + (function (object, name) { + google.maps.event.addDomListener(object, name, function () { + options.events[name].apply(this, [this]); + }); + })(control, ev); + } + + control.index = 1; + + return control; + }; + + /** + * Add a custom control to the map UI. + * + * @param {object} options - The `options` object should contain: + * * `style` (object): The keys and values of this object should be valid CSS properties and values. + * * `id` (string): The HTML id for the custom control. + * * `classes` (string): A string containing all the HTML classes for the custom control. + * * `content` (string or HTML element): The content of the custom control. + * * `position` (string): Any valid [`google.maps.ControlPosition`](https://developers.google.com/maps/documentation/javascript/controls#ControlPositioning) value, in lower or upper case. + * * `events` (object): The keys of this object should be valid DOM events. The values should be functions. + * * `disableDefaultStyles` (boolean): If false, removes the default styles for the controls like font (family and size), and box shadow. + * @returns {HTMLElement} + */ + GMaps.prototype.addControl = function (options) { + var control = this.createControl(options); + + this.controls.push(control); + this.map.controls[control.position].push(control); + + return control; + }; + + /** + * Remove a control from the map. `control` should be a control returned by `addControl()`. + * + * @param {HTMLElement} control - One of the controls returned by `addControl()`. + * @returns {HTMLElement} the removed control. + */ + GMaps.prototype.removeControl = function (control) { + var position = null, + i; + + for (i = 0; i < this.controls.length; i++) { + if (this.controls[i] == control) { + position = this.controls[i].position; + this.controls.splice(i, 1); + } + } + + if (position) { + for (i = 0; i < this.map.controls.length; i++) { + var controlsForPosition = this.map.controls[control.position]; + + if (controlsForPosition.getAt(i) == control) { + controlsForPosition.removeAt(i); + + break; + } + } + } + + return control; + }; + + GMaps.prototype.createMarker = function (options) { + if (options.lat == undefined && options.lng == undefined && options.position == undefined) { + throw 'No latitude or longitude defined.'; + } + + var self = this, + details = options.details, + fences = options.fences, + outside = options.outside, + base_options = { + position: new google.maps.LatLng(options.lat, options.lng), + map: null + }, + marker_options = extend_object(base_options, options); + + delete marker_options.lat; + delete marker_options.lng; + delete marker_options.fences; + delete marker_options.outside; + + var marker = new google.maps.Marker(marker_options); + + marker.fences = fences; + + if (options.infoWindow) { + marker.infoWindow = new google.maps.InfoWindow(options.infoWindow); + + var info_window_events = ['closeclick', 'content_changed', 'domready', 'position_changed', 'zindex_changed']; + + for (var ev = 0; ev < info_window_events.length; ev++) { + (function (object, name) { + if (options.infoWindow[name]) { + google.maps.event.addListener(object, name, function (e) { + options.infoWindow[name].apply(this, [e]); + }); + } + })(marker.infoWindow, info_window_events[ev]); + } + } + + var marker_events = ['animation_changed', 'clickable_changed', 'cursor_changed', 'draggable_changed', 'flat_changed', 'icon_changed', 'position_changed', 'shadow_changed', 'shape_changed', 'title_changed', 'visible_changed', 'zindex_changed']; + + var marker_events_with_mouse = ['dblclick', 'drag', 'dragend', 'dragstart', 'mousedown', 'mouseout', 'mouseover', 'mouseup']; + + for (var ev = 0; ev < marker_events.length; ev++) { + (function (object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function () { + options[name].apply(this, [this]); + }); + } + })(marker, marker_events[ev]); + } + + for (var ev = 0; ev < marker_events_with_mouse.length; ev++) { + (function (map, object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function (me) { + if (!me.pixel) { + me.pixel = map.getProjection().fromLatLngToPoint(me.latLng) + } + + options[name].apply(this, [me]); + }); + } + })(this.map, marker, marker_events_with_mouse[ev]); + } + + google.maps.event.addListener(marker, 'click', function () { + this.details = details; + + if (options.click) { + options.click.apply(this, [this]); + } + + if (marker.infoWindow) { + self.hideInfoWindows(); + marker.infoWindow.open(self.map, marker); + } + }); + + google.maps.event.addListener(marker, 'rightclick', function (e) { + e.marker = this; + + if (options.rightclick) { + options.rightclick.apply(this, [e]); + } + + if (window.context_menu[self.el.id]['marker'] != undefined) { + self.buildContextMenu('marker', e); + } + }); + + if (marker.fences) { + google.maps.event.addListener(marker, 'dragend', function () { + self.checkMarkerGeofence(marker, function (m, f) { + outside(m, f); + }); + }); + } + + return marker; + }; + + GMaps.prototype.addMarker = function (options) { + var marker; + if (options.hasOwnProperty('gm_accessors_')) { + // Native google.maps.Marker object + marker = options; + } + else { + if ((options.hasOwnProperty('lat') && options.hasOwnProperty('lng')) || options.position) { + marker = this.createMarker(options); + } + else { + throw 'No latitude or longitude defined.'; + } + } + + marker.setMap(this.map); + + if (this.markerClusterer) { + this.markerClusterer.addMarker(marker); + } + + this.markers.push(marker); + + GMaps.fire('marker_added', marker, this); + + return marker; + }; + + GMaps.prototype.addMarkers = function (array) { + for (var i = 0, marker; marker = array[i]; i++) { + this.addMarker(marker); + } + + return this.markers; + }; + + GMaps.prototype.hideInfoWindows = function () { + for (var i = 0, marker; marker = this.markers[i]; i++) { + if (marker.infoWindow) { + marker.infoWindow.close(); + } + } + }; + + GMaps.prototype.removeMarker = function (marker) { + for (var i = 0; i < this.markers.length; i++) { + if (this.markers[i] === marker) { + this.markers[i].setMap(null); + this.markers.splice(i, 1); + + if (this.markerClusterer) { + this.markerClusterer.removeMarker(marker); + } + + GMaps.fire('marker_removed', marker, this); + + break; + } + } + + return marker; + }; + + GMaps.prototype.removeMarkers = function (collection) { + var new_markers = []; + + if (typeof collection == 'undefined') { + for (var i = 0; i < this.markers.length; i++) { + var marker = this.markers[i]; + marker.setMap(null); + + GMaps.fire('marker_removed', marker, this); + } + + if (this.markerClusterer && this.markerClusterer.clearMarkers) { + this.markerClusterer.clearMarkers(); + } + + this.markers = new_markers; + } + else { + for (var i = 0; i < collection.length; i++) { + var index = this.markers.indexOf(collection[i]); + + if (index > -1) { + var marker = this.markers[index]; + marker.setMap(null); + + if (this.markerClusterer) { + this.markerClusterer.removeMarker(marker); + } + + GMaps.fire('marker_removed', marker, this); + } + } + + for (var i = 0; i < this.markers.length; i++) { + var marker = this.markers[i]; + if (marker.getMap() != null) { + new_markers.push(marker); + } + } + + this.markers = new_markers; + } + }; + + GMaps.prototype.drawOverlay = function (options) { + var overlay = new google.maps.OverlayView(), + auto_show = true; + + overlay.setMap(this.map); + + if (options.auto_show != null) { + auto_show = options.auto_show; + } + + overlay.onAdd = function () { + var el = document.createElement('div'); + + el.style.borderStyle = "none"; + el.style.borderWidth = "0px"; + el.style.position = "absolute"; + el.style.zIndex = 100; + el.innerHTML = options.content; + + overlay.el = el; + + if (!options.layer) { + options.layer = 'overlayLayer'; + } + + var panes = this.getPanes(), + overlayLayer = panes[options.layer], + stop_overlay_events = ['contextmenu', 'DOMMouseScroll', 'dblclick', 'mousedown']; + + overlayLayer.appendChild(el); + + for (var ev = 0; ev < stop_overlay_events.length; ev++) { + (function (object, name) { + google.maps.event.addDomListener(object, name, function (e) { + if (navigator.userAgent.toLowerCase().indexOf('msie') != -1 && document.all) { + e.cancelBubble = true; + e.returnValue = false; + } + else { + e.stopPropagation(); + } + }); + })(el, stop_overlay_events[ev]); + } + + if (options.click) { + panes.overlayMouseTarget.appendChild(overlay.el); + google.maps.event.addDomListener(overlay.el, 'click', function () { + options.click.apply(overlay, [overlay]); + }); + } + + google.maps.event.trigger(this, 'ready'); + }; + + overlay.draw = function () { + var projection = this.getProjection(), + pixel = projection.fromLatLngToDivPixel(new google.maps.LatLng(options.lat, options.lng)); + + options.horizontalOffset = options.horizontalOffset || 0; + options.verticalOffset = options.verticalOffset || 0; + + var el = overlay.el, + content = el.children[0], + content_height = content.clientHeight, + content_width = content.clientWidth; + + switch (options.verticalAlign) { + case 'top': + el.style.top = (pixel.y - content_height + options.verticalOffset) + 'px'; + break; + default: + case 'middle': + el.style.top = (pixel.y - (content_height / 2) + options.verticalOffset) + 'px'; + break; + case 'bottom': + el.style.top = (pixel.y + options.verticalOffset) + 'px'; + break; + } + + switch (options.horizontalAlign) { + case 'left': + el.style.left = (pixel.x - content_width + options.horizontalOffset) + 'px'; + break; + default: + case 'center': + el.style.left = (pixel.x - (content_width / 2) + options.horizontalOffset) + 'px'; + break; + case 'right': + el.style.left = (pixel.x + options.horizontalOffset) + 'px'; + break; + } + + el.style.display = auto_show ? 'block' : 'none'; + + if (!auto_show) { + options.show.apply(this, [el]); + } + }; + + overlay.onRemove = function () { + var el = overlay.el; + + if (options.remove) { + options.remove.apply(this, [el]); + } + else { + overlay.el.parentNode.removeChild(overlay.el); + overlay.el = null; + } + }; + + this.overlays.push(overlay); + return overlay; + }; + + GMaps.prototype.removeOverlay = function (overlay) { + for (var i = 0; i < this.overlays.length; i++) { + if (this.overlays[i] === overlay) { + this.overlays[i].setMap(null); + this.overlays.splice(i, 1); + + break; + } + } + }; + + GMaps.prototype.removeOverlays = function () { + for (var i = 0, item; item = this.overlays[i]; i++) { + item.setMap(null); + } + + this.overlays = []; + }; + + GMaps.prototype.drawPolyline = function (options) { + var path = [], + points = options.path; + + if (points.length) { + if (points[0][0] === undefined) { + path = points; + } + else { + for (var i = 0, latlng; latlng = points[i]; i++) { + path.push(new google.maps.LatLng(latlng[0], latlng[1])); + } + } + } + + var polyline_options = { + map: this.map, + path: path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight, + geodesic: options.geodesic, + clickable: true, + editable: false, + visible: true + }; + + if (options.hasOwnProperty("clickable")) { + polyline_options.clickable = options.clickable; + } + + if (options.hasOwnProperty("editable")) { + polyline_options.editable = options.editable; + } + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + if (options.hasOwnProperty("zIndex")) { + polyline_options.zIndex = options.zIndex; + } + + var polyline = new google.maps.Polyline(polyline_options); + + var polyline_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polyline_events.length; ev++) { + (function (object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function (e) { + options[name].apply(this, [e]); + }); + } + })(polyline, polyline_events[ev]); + } + + this.polylines.push(polyline); + + GMaps.fire('polyline_added', polyline, this); + + return polyline; + }; + + GMaps.prototype.removePolyline = function (polyline) { + for (var i = 0; i < this.polylines.length; i++) { + if (this.polylines[i] === polyline) { + this.polylines[i].setMap(null); + this.polylines.splice(i, 1); + + GMaps.fire('polyline_removed', polyline, this); + + break; + } + } + }; + + GMaps.prototype.removePolylines = function () { + for (var i = 0, item; item = this.polylines[i]; i++) { + item.setMap(null); + } + + this.polylines = []; + }; + + GMaps.prototype.drawCircle = function (options) { + options = extend_object({ + map: this.map, + center: new google.maps.LatLng(options.lat, options.lng) + }, options); + + delete options.lat; + delete options.lng; + + var polygon = new google.maps.Circle(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function (object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function (e) { + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + return polygon; + }; + + GMaps.prototype.drawRectangle = function (options) { + options = extend_object({ + map: this.map + }, options); + + var latLngBounds = new google.maps.LatLngBounds( + new google.maps.LatLng(options.bounds[0][0], options.bounds[0][1]), + new google.maps.LatLng(options.bounds[1][0], options.bounds[1][1]) + ); + + options.bounds = latLngBounds; + + var polygon = new google.maps.Rectangle(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function (object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function (e) { + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + return polygon; + }; + + GMaps.prototype.drawPolygon = function (options) { + var useGeoJSON = false; + + if (options.hasOwnProperty("useGeoJSON")) { + useGeoJSON = options.useGeoJSON; + } + + delete options.useGeoJSON; + + options = extend_object({ + map: this.map + }, options); + + if (useGeoJSON == false) { + options.paths = [options.paths.slice(0)]; + } + + if (options.paths.length > 0) { + if (options.paths[0].length > 0) { + options.paths = array_flat(array_map(options.paths, arrayToLatLng, useGeoJSON)); + } + } + + var polygon = new google.maps.Polygon(options), + polygon_events = ['click', 'dblclick', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'rightclick']; + + for (var ev = 0; ev < polygon_events.length; ev++) { + (function (object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function (e) { + options[name].apply(this, [e]); + }); + } + })(polygon, polygon_events[ev]); + } + + this.polygons.push(polygon); + + GMaps.fire('polygon_added', polygon, this); + + return polygon; + }; + + GMaps.prototype.removePolygon = function (polygon) { + for (var i = 0; i < this.polygons.length; i++) { + if (this.polygons[i] === polygon) { + this.polygons[i].setMap(null); + this.polygons.splice(i, 1); + + GMaps.fire('polygon_removed', polygon, this); + + break; + } + } + }; + + GMaps.prototype.removePolygons = function () { + for (var i = 0, item; item = this.polygons[i]; i++) { + item.setMap(null); + } + + this.polygons = []; + }; + + GMaps.prototype.getFromFusionTables = function (options) { + var events = options.events; + + delete options.events; + + var fusion_tables_options = options, + layer = new google.maps.FusionTablesLayer(fusion_tables_options); + + for (var ev in events) { + (function (object, name) { + google.maps.event.addListener(object, name, function (e) { + events[name].apply(this, [e]); + }); + })(layer, ev); + } + + this.layers.push(layer); + + return layer; + }; + + GMaps.prototype.loadFromFusionTables = function (options) { + var layer = this.getFromFusionTables(options); + layer.setMap(this.map); + + return layer; + }; + + GMaps.prototype.getFromKML = function (options) { + var url = options.url, + events = options.events; + + delete options.url; + delete options.events; + + var kml_options = options, + layer = new google.maps.KmlLayer(url, kml_options); + + for (var ev in events) { + (function (object, name) { + google.maps.event.addListener(object, name, function (e) { + events[name].apply(this, [e]); + }); + })(layer, ev); + } + + this.layers.push(layer); + + return layer; + }; + + GMaps.prototype.loadFromKML = function (options) { + var layer = this.getFromKML(options); + layer.setMap(this.map); + + return layer; + }; + + GMaps.prototype.addLayer = function (layerName, options) { + //var default_layers = ['weather', 'clouds', 'traffic', 'transit', 'bicycling', 'panoramio', 'places']; + options = options || {}; + var layer; + + switch (layerName) { + case 'weather': this.singleLayers.weather = layer = new google.maps.weather.WeatherLayer(); + break; + case 'clouds': this.singleLayers.clouds = layer = new google.maps.weather.CloudLayer(); + break; + case 'traffic': this.singleLayers.traffic = layer = new google.maps.TrafficLayer(); + break; + case 'transit': this.singleLayers.transit = layer = new google.maps.TransitLayer(); + break; + case 'bicycling': this.singleLayers.bicycling = layer = new google.maps.BicyclingLayer(); + break; + case 'panoramio': + this.singleLayers.panoramio = layer = new google.maps.panoramio.PanoramioLayer(); + layer.setTag(options.filter); + delete options.filter; + + //click event + if (options.click) { + google.maps.event.addListener(layer, 'click', function (event) { + options.click(event); + delete options.click; + }); + } + break; + case 'places': + this.singleLayers.places = layer = new google.maps.places.PlacesService(this.map); + + //search, nearbySearch, radarSearch callback, Both are the same + if (options.search || options.nearbySearch || options.radarSearch) { + var placeSearchRequest = { + bounds: options.bounds || null, + keyword: options.keyword || null, + location: options.location || null, + name: options.name || null, + radius: options.radius || null, + rankBy: options.rankBy || null, + types: options.types || null + }; + + if (options.radarSearch) { + layer.radarSearch(placeSearchRequest, options.radarSearch); + } + + if (options.search) { + layer.search(placeSearchRequest, options.search); + } + + if (options.nearbySearch) { + layer.nearbySearch(placeSearchRequest, options.nearbySearch); + } + } + + //textSearch callback + if (options.textSearch) { + var textSearchRequest = { + bounds: options.bounds || null, + location: options.location || null, + query: options.query || null, + radius: options.radius || null + }; + + layer.textSearch(textSearchRequest, options.textSearch); + } + break; + } + + if (layer !== undefined) { + if (typeof layer.setOptions == 'function') { + layer.setOptions(options); + } + if (typeof layer.setMap == 'function') { + layer.setMap(this.map); + } + + return layer; + } + }; + + GMaps.prototype.removeLayer = function (layer) { + if (typeof (layer) == "string" && this.singleLayers[layer] !== undefined) { + this.singleLayers[layer].setMap(null); + + delete this.singleLayers[layer]; + } + else { + for (var i = 0; i < this.layers.length; i++) { + if (this.layers[i] === layer) { + this.layers[i].setMap(null); + this.layers.splice(i, 1); + + break; + } + } + } + }; + + var travelMode, unitSystem; + + GMaps.prototype.getRoutes = function (options) { + switch (options.travelMode) { + case 'bicycling': + travelMode = google.maps.TravelMode.BICYCLING; + break; + case 'transit': + travelMode = google.maps.TravelMode.TRANSIT; + break; + case 'driving': + travelMode = google.maps.TravelMode.DRIVING; + break; + default: + travelMode = google.maps.TravelMode.WALKING; + break; + } + + if (options.unitSystem === 'imperial') { + unitSystem = google.maps.UnitSystem.IMPERIAL; + } + else { + unitSystem = google.maps.UnitSystem.METRIC; + } + + var base_options = { + avoidHighways: false, + avoidTolls: false, + optimizeWaypoints: false, + waypoints: [] + }, + request_options = extend_object(base_options, options); + + request_options.origin = /string/.test(typeof options.origin) ? options.origin : new google.maps.LatLng(options.origin[0], options.origin[1]); + request_options.destination = /string/.test(typeof options.destination) ? options.destination : new google.maps.LatLng(options.destination[0], options.destination[1]); + request_options.travelMode = travelMode; + request_options.unitSystem = unitSystem; + + delete request_options.callback; + delete request_options.error; + + var self = this, + routes = [], + service = new google.maps.DirectionsService(); + + service.route(request_options, function (result, status) { + if (status === google.maps.DirectionsStatus.OK) { + for (var r in result.routes) { + if (result.routes.hasOwnProperty(r)) { + routes.push(result.routes[r]); + } + } + + if (options.callback) { + options.callback(routes, result, status); + } + } + else { + if (options.error) { + options.error(result, status); + } + } + }); + }; + + GMaps.prototype.removeRoutes = function () { + this.routes.length = 0; + }; + + GMaps.prototype.getElevations = function (options) { + options = extend_object({ + locations: [], + path: false, + samples: 256 + }, options); + + if (options.locations.length > 0) { + if (options.locations[0].length > 0) { + options.locations = array_flat(array_map([options.locations], arrayToLatLng, false)); + } + } + + var callback = options.callback; + delete options.callback; + + var service = new google.maps.ElevationService(); + + //location request + if (!options.path) { + delete options.path; + delete options.samples; + + service.getElevationForLocations(options, function (result, status) { + if (callback && typeof (callback) === "function") { + callback(result, status); + } + }); + //path request + } else { + var pathRequest = { + path: options.locations, + samples: options.samples + }; + + service.getElevationAlongPath(pathRequest, function (result, status) { + if (callback && typeof (callback) === "function") { + callback(result, status); + } + }); + } + }; + + GMaps.prototype.cleanRoute = GMaps.prototype.removePolylines; + + GMaps.prototype.renderRoute = function (options, renderOptions) { + var self = this, + panel = ((typeof renderOptions.panel === 'string') ? document.getElementById(renderOptions.panel.replace('#', '')) : renderOptions.panel), + display; + + renderOptions.panel = panel; + renderOptions = extend_object({ + map: this.map + }, renderOptions); + display = new google.maps.DirectionsRenderer(renderOptions); + + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + avoidHighways: options.avoidHighways, + avoidTolls: options.avoidTolls, + optimizeWaypoints: options.optimizeWaypoints, + callback: function (routes, response, status) { + if (status === google.maps.DirectionsStatus.OK) { + display.setDirections(response); + } + } + }); + }; + + GMaps.prototype.drawRoute = function (options) { + var self = this; + + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + avoidHighways: options.avoidHighways, + avoidTolls: options.avoidTolls, + optimizeWaypoints: options.optimizeWaypoints, + callback: function (routes) { + if (routes.length > 0) { + var polyline_options = { + path: routes[routes.length - 1].overview_path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + + if (options.callback) { + options.callback(routes[routes.length - 1]); + } + } + } + }); + }; + + GMaps.prototype.travelRoute = function (options) { + if (options.origin && options.destination) { + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + unitSystem: options.unitSystem, + error: options.error, + callback: function (e) { + //start callback + if (e.length > 0 && options.start) { + options.start(e[e.length - 1]); + } + + //step callback + if (e.length > 0 && options.step) { + var route = e[e.length - 1]; + if (route.legs.length > 0) { + var steps = route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + options.step(step, (route.legs[0].steps.length - 1)); + } + } + } + + //end callback + if (e.length > 0 && options.end) { + options.end(e[e.length - 1]); + } + } + }); + } + else if (options.route) { + if (options.route.legs.length > 0) { + var steps = options.route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + options.step(step); + } + } + } + }; + + GMaps.prototype.drawSteppedRoute = function (options) { + var self = this; + + if (options.origin && options.destination) { + this.getRoutes({ + origin: options.origin, + destination: options.destination, + travelMode: options.travelMode, + waypoints: options.waypoints, + error: options.error, + callback: function (e) { + //start callback + if (e.length > 0 && options.start) { + options.start(e[e.length - 1]); + } + + //step callback + if (e.length > 0 && options.step) { + var route = e[e.length - 1]; + if (route.legs.length > 0) { + var steps = route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + var polyline_options = { + path: step.path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + options.step(step, (route.legs[0].steps.length - 1)); + } + } + } + + //end callback + if (e.length > 0 && options.end) { + options.end(e[e.length - 1]); + } + } + }); + } + else if (options.route) { + if (options.route.legs.length > 0) { + var steps = options.route.legs[0].steps; + for (var i = 0, step; step = steps[i]; i++) { + step.step_number = i; + var polyline_options = { + path: step.path, + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + self.drawPolyline(polyline_options); + options.step(step); + } + } + } + }; + + GMaps.Route = function (options) { + this.origin = options.origin; + this.destination = options.destination; + this.waypoints = options.waypoints; + + this.map = options.map; + this.route = options.route; + this.step_count = 0; + this.steps = this.route.legs[0].steps; + this.steps_length = this.steps.length; + + var polyline_options = { + path: new google.maps.MVCArray(), + strokeColor: options.strokeColor, + strokeOpacity: options.strokeOpacity, + strokeWeight: options.strokeWeight + }; + + if (options.hasOwnProperty("icons")) { + polyline_options.icons = options.icons; + } + + this.polyline = this.map.drawPolyline(polyline_options).getPath(); + }; + + GMaps.Route.prototype.getRoute = function (options) { + var self = this; + + this.map.getRoutes({ + origin: this.origin, + destination: this.destination, + travelMode: options.travelMode, + waypoints: this.waypoints || [], + error: options.error, + callback: function () { + self.route = e[0]; + + if (options.callback) { + options.callback.call(self); + } + } + }); + }; + + GMaps.Route.prototype.back = function () { + if (this.step_count > 0) { + this.step_count--; + var path = this.route.legs[0].steps[this.step_count].path; + + for (var p in path) { + if (path.hasOwnProperty(p)) { + this.polyline.pop(); + } + } + } + }; + + GMaps.Route.prototype.forward = function () { + if (this.step_count < this.steps_length) { + var path = this.route.legs[0].steps[this.step_count].path; + + for (var p in path) { + if (path.hasOwnProperty(p)) { + this.polyline.push(path[p]); + } + } + this.step_count++; + } + }; + + GMaps.prototype.checkGeofence = function (lat, lng, fence) { + return fence.containsLatLng(new google.maps.LatLng(lat, lng)); + }; + + GMaps.prototype.checkMarkerGeofence = function (marker, outside_callback) { + if (marker.fences) { + for (var i = 0, fence; fence = marker.fences[i]; i++) { + var pos = marker.getPosition(); + if (!this.checkGeofence(pos.lat(), pos.lng(), fence)) { + outside_callback(marker, fence); + } + } + } + }; + + GMaps.prototype.toImage = function (options) { + var options = options || {}, + static_map_options = {}; + + static_map_options['size'] = options['size'] || [this.el.clientWidth, this.el.clientHeight]; + static_map_options['lat'] = this.getCenter().lat(); + static_map_options['lng'] = this.getCenter().lng(); + + if (this.markers.length > 0) { + static_map_options['markers'] = []; + + for (var i = 0; i < this.markers.length; i++) { + static_map_options['markers'].push({ + lat: this.markers[i].getPosition().lat(), + lng: this.markers[i].getPosition().lng() + }); + } + } + + if (this.polylines.length > 0) { + var polyline = this.polylines[0]; + + static_map_options['polyline'] = {}; + static_map_options['polyline']['path'] = google.maps.geometry.encoding.encodePath(polyline.getPath()); + static_map_options['polyline']['strokeColor'] = polyline.strokeColor + static_map_options['polyline']['strokeOpacity'] = polyline.strokeOpacity + static_map_options['polyline']['strokeWeight'] = polyline.strokeWeight + } + + return GMaps.staticMapURL(static_map_options); + }; + + GMaps.staticMapURL = function (options) { + var parameters = [], + data, + static_root = (location.protocol === 'file:' ? 'http:' : location.protocol) + '//maps.googleapis.com/maps/api/staticmap'; + + if (options.url) { + static_root = options.url; + delete options.url; + } + + static_root += '?'; + + var markers = options.markers; + + delete options.markers; + + if (!markers && options.marker) { + markers = [options.marker]; + delete options.marker; + } + + var styles = options.styles; + + delete options.styles; + + var polyline = options.polyline; + delete options.polyline; + + /** Map options **/ + if (options.center) { + parameters.push('center=' + options.center); + delete options.center; + } + else if (options.address) { + parameters.push('center=' + options.address); + delete options.address; + } + else if (options.lat) { + parameters.push(['center=', options.lat, ',', options.lng].join('')); + delete options.lat; + delete options.lng; + } + else if (options.visible) { + var visible = encodeURI(options.visible.join('|')); + parameters.push('visible=' + visible); + } + + var size = options.size; + if (size) { + if (size.join) { + size = size.join('x'); + } + delete options.size; + } + else { + size = '630x300'; + } + parameters.push('size=' + size); + + if (!options.zoom && options.zoom !== false) { + options.zoom = 15; + } + + var sensor = options.hasOwnProperty('sensor') ? !!options.sensor : true; + delete options.sensor; + parameters.push('sensor=' + sensor); + + for (var param in options) { + if (options.hasOwnProperty(param)) { + parameters.push(param + '=' + options[param]); + } + } + + /** Markers **/ + if (markers) { + var marker, loc; + + for (var i = 0; data = markers[i]; i++) { + marker = []; + + if (data.size && data.size !== 'normal') { + marker.push('size:' + data.size); + delete data.size; + } + else if (data.icon) { + marker.push('icon:' + encodeURI(data.icon)); + delete data.icon; + } + + if (data.color) { + marker.push('color:' + data.color.replace('#', '0x')); + delete data.color; + } + + if (data.label) { + marker.push('label:' + data.label[0].toUpperCase()); + delete data.label; + } + + loc = (data.address ? data.address : data.lat + ',' + data.lng); + delete data.address; + delete data.lat; + delete data.lng; + + for (var param in data) { + if (data.hasOwnProperty(param)) { + marker.push(param + ':' + data[param]); + } + } + + if (marker.length || i === 0) { + marker.push(loc); + marker = marker.join('|'); + parameters.push('markers=' + encodeURI(marker)); + } + // New marker without styles + else { + marker = parameters.pop() + encodeURI('|' + loc); + parameters.push(marker); + } + } + } + + /** Map Styles **/ + if (styles) { + for (var i = 0; i < styles.length; i++) { + var styleRule = []; + if (styles[i].featureType) { + styleRule.push('feature:' + styles[i].featureType.toLowerCase()); + } + + if (styles[i].elementType) { + styleRule.push('element:' + styles[i].elementType.toLowerCase()); + } + + for (var j = 0; j < styles[i].stylers.length; j++) { + for (var p in styles[i].stylers[j]) { + var ruleArg = styles[i].stylers[j][p]; + if (p == 'hue' || p == 'color') { + ruleArg = '0x' + ruleArg.substring(1); + } + styleRule.push(p + ':' + ruleArg); + } + } + + var rule = styleRule.join('|'); + if (rule != '') { + parameters.push('style=' + rule); + } + } + } + + /** Polylines **/ + function parseColor(color, opacity) { + if (color[0] === '#') { + color = color.replace('#', '0x'); + + if (opacity) { + opacity = parseFloat(opacity); + opacity = Math.min(1, Math.max(opacity, 0)); + if (opacity === 0) { + return '0x00000000'; + } + opacity = (opacity * 255).toString(16); + if (opacity.length === 1) { + opacity += opacity; + } + + color = color.slice(0, 8) + opacity; + } + } + return color; + } + + if (polyline) { + data = polyline; + polyline = []; + + if (data.strokeWeight) { + polyline.push('weight:' + parseInt(data.strokeWeight, 10)); + } + + if (data.strokeColor) { + var color = parseColor(data.strokeColor, data.strokeOpacity); + polyline.push('color:' + color); + } + + if (data.fillColor) { + var fillcolor = parseColor(data.fillColor, data.fillOpacity); + polyline.push('fillcolor:' + fillcolor); + } + + var path = data.path; + if (path.join) { + for (var j = 0, pos; pos = path[j]; j++) { + polyline.push(pos.join(',')); + } + } + else { + polyline.push('enc:' + path); + } + + polyline = polyline.join('|'); + parameters.push('path=' + encodeURI(polyline)); + } + + /** Retina support **/ + var dpi = window.devicePixelRatio || 1; + parameters.push('scale=' + dpi); + + parameters = parameters.join('&'); + return static_root + parameters; + }; + + GMaps.prototype.addMapType = function (mapTypeId, options) { + if (options.hasOwnProperty("getTileUrl") && typeof (options["getTileUrl"]) == "function") { + options.tileSize = options.tileSize || new google.maps.Size(256, 256); + + var mapType = new google.maps.ImageMapType(options); + + this.map.mapTypes.set(mapTypeId, mapType); + } + else { + throw "'getTileUrl' function required."; + } + }; + + GMaps.prototype.addOverlayMapType = function (options) { + if (options.hasOwnProperty("getTile") && typeof (options["getTile"]) == "function") { + var overlayMapTypeIndex = options.index; + + delete options.index; + + this.map.overlayMapTypes.insertAt(overlayMapTypeIndex, options); + } + else { + throw "'getTile' function required."; + } + }; + + GMaps.prototype.removeOverlayMapType = function (overlayMapTypeIndex) { + this.map.overlayMapTypes.removeAt(overlayMapTypeIndex); + }; + + GMaps.prototype.addStyle = function (options) { + var styledMapType = new google.maps.StyledMapType(options.styles, { name: options.styledMapName }); + + this.map.mapTypes.set(options.mapTypeId, styledMapType); + }; + + GMaps.prototype.setStyle = function (mapTypeId) { + this.map.setMapTypeId(mapTypeId); + }; + + GMaps.prototype.createPanorama = function (streetview_options) { + if (!streetview_options.hasOwnProperty('lat') || !streetview_options.hasOwnProperty('lng')) { + streetview_options.lat = this.getCenter().lat(); + streetview_options.lng = this.getCenter().lng(); + } + + this.panorama = GMaps.createPanorama(streetview_options); + + this.map.setStreetView(this.panorama); + + return this.panorama; + }; + + GMaps.createPanorama = function (options) { + var el = getElementById(options.el, options.context); + + options.position = new google.maps.LatLng(options.lat, options.lng); + + delete options.el; + delete options.context; + delete options.lat; + delete options.lng; + + var streetview_events = ['closeclick', 'links_changed', 'pano_changed', 'position_changed', 'pov_changed', 'resize', 'visible_changed'], + streetview_options = extend_object({ visible: true }, options); + + for (var i = 0; i < streetview_events.length; i++) { + delete streetview_options[streetview_events[i]]; + } + + var panorama = new google.maps.StreetViewPanorama(el, streetview_options); + + for (var i = 0; i < streetview_events.length; i++) { + (function (object, name) { + if (options[name]) { + google.maps.event.addListener(object, name, function () { + options[name].apply(this); + }); + } + })(panorama, streetview_events[i]); + } + + return panorama; + }; + + GMaps.prototype.on = function (event_name, handler) { + return GMaps.on(event_name, this, handler); + }; + + GMaps.prototype.off = function (event_name) { + GMaps.off(event_name, this); + }; + + GMaps.prototype.once = function (event_name, handler) { + return GMaps.once(event_name, this, handler); + }; + + GMaps.custom_events = ['marker_added', 'marker_removed', 'polyline_added', 'polyline_removed', 'polygon_added', 'polygon_removed', 'geolocated', 'geolocation_failed']; + + GMaps.on = function (event_name, object, handler) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if (object instanceof GMaps) object = object.map; + return google.maps.event.addListener(object, event_name, handler); + } + else { + var registered_event = { + handler: handler, + eventName: event_name + }; + + object.registered_events[event_name] = object.registered_events[event_name] || []; + object.registered_events[event_name].push(registered_event); + + return registered_event; + } + }; + + GMaps.off = function (event_name, object) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if (object instanceof GMaps) object = object.map; + google.maps.event.clearListeners(object, event_name); + } + else { + object.registered_events[event_name] = []; + } + }; + + GMaps.once = function (event_name, object, handler) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + if (object instanceof GMaps) object = object.map; + return google.maps.event.addListenerOnce(object, event_name, handler); + } + }; + + GMaps.fire = function (event_name, object, scope) { + if (GMaps.custom_events.indexOf(event_name) == -1) { + google.maps.event.trigger(object, event_name, Array.prototype.slice.apply(arguments).slice(2)); + } + else { + if (event_name in scope.registered_events) { + var firing_events = scope.registered_events[event_name]; + + for (var i = 0; i < firing_events.length; i++) { + (function (handler, scope, object) { + handler.apply(scope, [object]); + })(firing_events[i]['handler'], scope, object); + } + } + } + }; + + GMaps.geolocate = function (options) { + var complete_callback = options.always || options.complete; + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function (position) { + options.success(position); + + if (complete_callback) { + complete_callback(); + } + }, function (error) { + options.error(error); + + if (complete_callback) { + complete_callback(); + } + }, options.options); + } + else { + options.not_supported(); + + if (complete_callback) { + complete_callback(); + } + } + }; + + GMaps.geocode = function (options) { + this.geocoder = new google.maps.Geocoder(); + var callback = options.callback; + if (options.hasOwnProperty('lat') && options.hasOwnProperty('lng')) { + options.latLng = new google.maps.LatLng(options.lat, options.lng); + } + + delete options.lat; + delete options.lng; + delete options.callback; + + this.geocoder.geocode(options, function (results, status) { + callback(results, status); + }); + }; + + if (typeof window.google === 'object' && window.google.maps) { + //========================== + // Polygon containsLatLng + // https://github.com/tparkin/Google-Maps-Point-in-Polygon + // Poygon getBounds extension - google-maps-extensions + // http://code.google.com/p/google-maps-extensions/source/browse/google.maps.Polygon.getBounds.js + if (!google.maps.Polygon.prototype.getBounds) { + google.maps.Polygon.prototype.getBounds = function (latLng) { + var bounds = new google.maps.LatLngBounds(); + var paths = this.getPaths(); + var path; + + for (var p = 0; p < paths.getLength(); p++) { + path = paths.getAt(p); + for (var i = 0; i < path.getLength(); i++) { + bounds.extend(path.getAt(i)); + } + } + + return bounds; + }; + } + + if (!google.maps.Polygon.prototype.containsLatLng) { + // Polygon containsLatLng - method to determine if a latLng is within a polygon + google.maps.Polygon.prototype.containsLatLng = function (latLng) { + // Exclude points outside of bounds as there is no way they are in the poly + var bounds = this.getBounds(); + + if (bounds !== null && !bounds.contains(latLng)) { + return false; + } + + // Raycast point in polygon method + var inPoly = false; + + var numPaths = this.getPaths().getLength(); + for (var p = 0; p < numPaths; p++) { + var path = this.getPaths().getAt(p); + var numPoints = path.getLength(); + var j = numPoints - 1; + + for (var i = 0; i < numPoints; i++) { + var vertex1 = path.getAt(i); + var vertex2 = path.getAt(j); + + if (vertex1.lng() < latLng.lng() && vertex2.lng() >= latLng.lng() || vertex2.lng() < latLng.lng() && vertex1.lng() >= latLng.lng()) { + if (vertex1.lat() + (latLng.lng() - vertex1.lng()) / (vertex2.lng() - vertex1.lng()) * (vertex2.lat() - vertex1.lat()) < latLng.lat()) { + inPoly = !inPoly; + } + } + + j = i; + } + } + + return inPoly; + }; + } + + if (!google.maps.Circle.prototype.containsLatLng) { + google.maps.Circle.prototype.containsLatLng = function (latLng) { + if (google.maps.geometry) { + return google.maps.geometry.spherical.computeDistanceBetween(this.getCenter(), latLng) <= this.getRadius(); + } + else { + return true; + } + }; + } + + google.maps.Rectangle.prototype.containsLatLng = function (latLng) { + return this.getBounds().contains(latLng); + }; + + google.maps.LatLngBounds.prototype.containsLatLng = function (latLng) { + return this.contains(latLng); + }; + + google.maps.Marker.prototype.setFences = function (fences) { + this.fences = fences; + }; + + google.maps.Marker.prototype.addFence = function (fence) { + this.fences.push(fence); + }; + + google.maps.Marker.prototype.getId = function () { + return this['__gm_id']; + }; + } + + //========================== + // Array indexOf + // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf + if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } + } + + return GMaps; +})); diff --git a/Goui.Plugin.Maps/MapsPlugin.cs b/Goui.Plugin.Maps/MapsPlugin.cs new file mode 100644 index 0000000..45f7efb --- /dev/null +++ b/Goui.Plugin.Maps/MapsPlugin.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; +using System.Text; +using Goui.Html; + +namespace Goui.Plugin.Maps { + public class MapsPlugin : IGouiPlugin { + + + private static Dictionary HostedFiles = new Dictionary(); + static MapsPlugin() { + var asm = typeof(MapsPlugin).Assembly; + HostedFiles.Add("/GoogleMaps.js", HostedFile.LoadFromResource(asm, "Goui.Plugin.Maps.Js.GoogleMaps.js")); + } + + public HostedFile GetHostedFile(string path) { + return HostedFiles.ContainsKey(path) ? HostedFiles[path] : null; + } + + public bool OnHttpRequest(HttpListenerContext listenerContext) { + return true; + } + + public void OnNodeCreated(Node node) { + } + + public void OnPublish(string path, Element element) { + } + + public bool OnRenderTemplate(TextWriter writer, string webSocketPath, string title, string initialHtml) { + return true; + } + + public void OnUIStart() { + } + + public void OnUIStop() { + } + } +} diff --git a/Goui.sln b/Goui.sln index 541aa90..acae468 100644 --- a/Goui.sln +++ b/Goui.sln @@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmFormsApp", "PlatformSam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{43187C08-A1BC-431C-ABE6-BAE270E71BBE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{FAF96103-D422-4319-BE6B-F34B6C9322EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Goui.Plugin.Maps", "Goui.Plugin.Maps\Goui.Plugin.Maps.csproj", "{074FFDB5-5603-4796-BA63-B8515B568DF7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -139,6 +143,18 @@ Global {43187C08-A1BC-431C-ABE6-BAE270E71BBE}.Release|x64.Build.0 = Release|Any CPU {43187C08-A1BC-431C-ABE6-BAE270E71BBE}.Release|x86.ActiveCfg = Release|Any CPU {43187C08-A1BC-431C-ABE6-BAE270E71BBE}.Release|x86.Build.0 = Release|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Debug|x64.ActiveCfg = Debug|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Debug|x64.Build.0 = Debug|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Debug|x86.ActiveCfg = Debug|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Debug|x86.Build.0 = Debug|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Release|Any CPU.Build.0 = Release|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Release|x64.ActiveCfg = Release|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Release|x64.Build.0 = Release|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Release|x86.ActiveCfg = Release|Any CPU + {074FFDB5-5603-4796-BA63-B8515B568DF7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,6 +162,7 @@ Global GlobalSection(NestedProjects) = preSolution {7C6D477C-3378-4A86-9C31-AAD51204120B} = {12ADF328-BBA8-48FC-9AF1-F11B7921D9EA} {ABA00F85-03DC-4FBE-8858-C47601A2102A} = {12ADF328-BBA8-48FC-9AF1-F11B7921D9EA} + {074FFDB5-5603-4796-BA63-B8515B568DF7} = {FAF96103-D422-4319-BE6B-F34B6C9322EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9370DD6D-816D-4E0F-8356-835F65DCF3AA} diff --git a/Goui/Client.js b/Goui/Client.js index fa67a19..82cd0f2 100644 --- a/Goui/Client.js +++ b/Goui/Client.js @@ -8,13 +8,13 @@ const hasText = {}; let socket = null; let wasmSession = null; -function send (json) { - if (debug) console.log ("Send", json); +function send(json) { + if (debug) console.log("Send", json); if (socket != null) { - socket.send (json); + socket.send(json); } else if (wasmSession != null) { - WebAssemblyApp.receiveMessagesJson (wasmSession, json); + WebAssemblyApp.receiveMessagesJson(wasmSession, json); } } @@ -37,47 +37,33 @@ const inputEvents = { keyup: true, }; -function getSize () { +function getSize() { return { height: window.innerHeight, width: window.innerWidth }; } -function setCookie (name, value, days) { +function setCookie(name, value, days) { var expires = ""; if (days) { - var date = new Date (); - date.setTime(date.getTime () + (days*24*60*60*1000)); + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } - document.cookie = name + "=" + (value || "") + expires + "; path=/"; + document.cookie = name + "=" + (value || "") + expires + "; path=/"; } -function saveSize (s) { - setCookie ("GouiWindowWidth", s.width, 7); - setCookie ("GouiWindowHeight", s.height, 7); -} - -function initializeNavigation() { - monitorHashChanged(); - const em = { - m: "event", - id: "window", - k: "hashchange", - v: window.location - }; - saveSize(em.v); - const ems = JSON.stringify(em); - send(ems); - if (debug) console.log("Event", em); +function saveSize(s) { + setCookie("GouiWindowWidth", s.width, 7); + setCookie("GouiWindowHeight", s.height, 7); } // Main entrypoint -function Goui (rootElementPath) { +function Goui(rootElementPath) { - var initialSize = getSize (); - saveSize (initialSize); + var initialSize = getSize(); + saveSize(initialSize); var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; @@ -87,72 +73,51 @@ function Goui (rootElementPath) { proto = "wss"; } - socket = new WebSocket (proto + "://" + document.location.host + rootElementPath + wsArgs, "Goui"); + socket = new WebSocket(proto + "://" + document.location.host + rootElementPath + wsArgs, "Goui"); - socket.addEventListener ("open", function (event) { - console.log ("Web socket opened"); - initializeNavigation(); + socket.addEventListener("open", function (event) { + console.log("Web socket opened"); }); - socket.addEventListener ("error", function (event) { - console.error ("Web socket error", event); + socket.addEventListener("error", function (event) { + console.error("Web socket error", event); }); - socket.addEventListener ("close", function (event) { - console.error ("Web socket close", event); + socket.addEventListener("close", function (event) { + console.error("Web socket close", event); }); socket.addEventListener("message", function (event) { - const messages = JSON.parse (event.data); + const messages = JSON.parse(event.data); if (debug) console.log("Messages", messages); - if (Array.isArray (messages)) { - messages.forEach (function (m) { + if (Array.isArray(messages)) { + messages.forEach(function (m) { // console.log('Raw value from server', m.v); - m.v = fixupValue (m.v); - processMessage (m); + m.v = fixupValue(m.v); + processMessage(m); }); } }); console.log("Web socket created"); - monitorSizeChanges (1000/10); + monitorSizeChanges(1000 / 10); } -function GouiWasm (mainAsmName, mainNamespace, mainClassName, mainMethodName, assemblies) -{ +function GouiWasm(mainAsmName, mainNamespace, mainClassName, mainMethodName, assemblies) { Module.entryPoint = { "a": mainAsmName, "n": mainNamespace, "t": mainClassName, "m": mainMethodName }; Module.assemblies = assemblies; - initializeNavigation(); - monitorSizeChanges (1000/30); -} - -function monitorHashChanged() { - function hashChangeHandler() { - const em = { - m: "event", - id: "window", - k: "hashchange", - v: window.location - }; - saveSize(em.v); - const ems = JSON.stringify(em); - send(ems); - if (debug) console.log("Event", em); - } - - window.addEventListener("hashchange", hashChangeHandler, false); + monitorSizeChanges(1000 / 30); } -function monitorSizeChanges (millis) -{ +function monitorSizeChanges(millis) { var resizeTimeout; function resizeThrottler() { if (!resizeTimeout) { - resizeTimeout = setTimeout(function() { + resizeTimeout = setTimeout(function () { resizeTimeout = null; - resizeHandler(); + resizeHandler(); }, millis); } } @@ -162,55 +127,55 @@ function monitorSizeChanges (millis) m: "event", id: "window", k: "resize", - v: getSize (), + v: getSize(), }; - saveSize (em.v); - const ems = JSON.stringify (em); - send (ems); - if (debug) console.log ("Event", em); + saveSize(em.v); + const ems = JSON.stringify(em); + send(ems); + if (debug) console.log("Event", em); } window.addEventListener("resize", resizeThrottler, false); } -function getNode (id) { +function getNode(id) { switch (id) { case "window": return window; case "document": return document; case "document.body": - const bodyNode = document.getElementById ("Goui-body"); + const bodyNode = document.getElementById("Goui-body"); return bodyNode || document.body; default: return nodes[id]; } } -function getOrCreateElement (id, tagName) { - var e = document.getElementById (id); +function getOrCreateElement(id, tagName) { + var e = document.getElementById(id); if (e) { if (e.firstChild && e.firstChild.nodeType == Node.TEXT_NODE) hasText[e.id] = true; return e; } - return document.createElement (tagName); + return document.createElement(tagName); } -function msgCreate (m) { +function msgCreate(m) { const id = m.id; const tagName = m.k; const node = tagName === "#text" ? - document.createTextNode ("") : - getOrCreateElement (id, tagName); + document.createTextNode("") : + getOrCreateElement(id, tagName); if (tagName !== "#text") node.id = id; nodes[id] = node; - if (debug) console.log ("Created node", node); + if (debug) console.log("Created node", node); } -function msgSet (m) { +function msgSet(m) { const id = m.id; - const node = getNode (id); + const node = getNode(id); if (!node) { - console.error ("Unknown node id", m); + console.error("Unknown node id", m); return; } const parts = m.k.split("."); @@ -221,73 +186,77 @@ function msgSet (m) { const lastPart = parts[parts.length - 1]; const value = lastPart === "htmlFor" ? m.v.id : m.v; o[lastPart] = value; - if (debug) console.log ("Set", node, parts, value); + if (debug) console.log("Set", node, parts, value); } -function msgSetAttr (m) { +function msgSetAttr(m) { const id = m.id; - const node = getNode (id); + const node = getNode(id); if (!node) { - console.error ("Unknown node id", m); + console.error("Unknown node id", m); return; } node.setAttribute(m.k, m.v); - if (debug) console.log ("SetAttr", node, m.k, m.v); + if (debug) console.log("SetAttr", node, m.k, m.v); } -function msgRemAttr (m) { +function msgRemAttr(m) { const id = m.id; - const node = getNode (id); + const node = getNode(id); if (!node) { - console.error ("Unknown node id", m); + console.error("Unknown node id", m); return; } node.removeAttribute(m.k); - if (debug) console.log ("RemAttr", node, m.k); + if (debug) console.log("RemAttr", node, m.k); } -function getCallerProperty(target, accessorStr) { - const arr = accessorStr.split('.'); - var caller = target; - var property = target; - arr.forEach(function (v) { - caller = property; - property = caller[v]; - }); - return [caller, property]; +function msgEvent(m) { + const id = m.id; + const node = getNode(id); + if (!node) { + console.error("Unknown node id", m); + return; + } + const target = node; + const eventName = m.k; + if (debug) console.log("Event", node, eventName, m.v); + + var eventMsg = new CustomEvent(eventName, { detail: m.v }); + target.dispatchEvent(eventMsg); } -function msgCall (m) { +function msgCall(m) { const id = m.id; - const node = getNode (id); + const node = getNode(id); if (!node) { - console.error ("Unknown node id", m); + console.error("Unknown node id", m); return; } const target = node; if (m.k === "insertBefore" && m.v[0].nodeType == Node.TEXT_NODE && m.v[1] == null && hasText[id]) { // Text is already set so it clear it first if (target.firstChild) - target.removeChild (target.firstChild); + target.removeChild(target.firstChild); delete hasText[id]; } - //const f = target[m.k]; - const f = getCallerProperty(target, m.k); - if (debug) console.log ("Call", node, f, m.v); - const r = f[1].apply (f[0], m.v); + const f = target[m.k]; + if (debug) console.log("Call", node, f, m.v); + const r = f.apply(target, m.v); if (typeof m.rid === 'string' || m.rid instanceof String) { nodes[m.rid] = r; } } -function msgListen (m) { - const node = getNode (m.id); +function msgListen(m) { + const node = getNode(m.id); if (!node) { - console.error ("Unknown node id", m); + console.error("Unknown node id", m); return; } - if (debug) console.log ("Listen", node, m.k); + if (debug) console.log("Listen", node, m.k); node.addEventListener(m.k, function (e) { + if (debug) console.log("Event Caught", node, m.k); const em = { m: "event", id: m.id, @@ -304,53 +273,59 @@ function msgListen (m) { offsetY: e.offsetY, }; } - const ems = JSON.stringify (em); - send (ems); - if (debug) console.log ("Event", em); + else if (e.detail) { + em.v = e.detail; + } + const ems = JSON.stringify(em); + send(ems); + if (debug) console.log("Event", em); if (em.k === "submit") - e.preventDefault (); + e.preventDefault(); }); } -function processMessage (m) { +function processMessage(m) { switch (m.m) { case "nop": break; case "create": - msgCreate (m); + msgCreate(m); break; case "set": - msgSet (m); + msgSet(m); break; case "setAttr": - msgSetAttr (m); + msgSetAttr(m); break; case "remAttr": - msgRemAttr (m); + msgRemAttr(m); break; case "call": - msgCall (m); + msgCall(m); break; case "listen": - msgListen (m); + msgListen(m); + break; + case "event": + msgEvent(m); break; default: - console.error ("Unknown message type", m.m, m); + console.error("Unknown message type", m.m, m); } } -function fixupValue (v) { +function fixupValue(v) { var x, n; - if (Array.isArray (v)) { + if (Array.isArray(v)) { for (x in v) { - v[x] = fixupValue (v[x]); + v[x] = fixupValue(v[x]); } return v; } else if (typeof v === 'string' || v instanceof String) { if ((v.length > 1) && (v[0] === "\u2999")) { // console.log("V", v); - return getNode (v); + return getNode(v); } } else if (!!v && v.hasOwnProperty("id") && v.hasOwnProperty("k")) { @@ -361,94 +336,93 @@ function fixupValue (v) { // == WASM Support == -window["__GouiReceiveMessages"] = function (sessionId, messages) -{ - if (debug) console.log ("WebAssembly Receive", messages); +window["__GouiReceiveMessages"] = function (sessionId, messages) { + if (debug) console.log("WebAssembly Receive", messages); if (wasmSession != null) { - messages.forEach (function (m) { + messages.forEach(function (m) { // console.log ('Raw value from server', m.v); - m.v = fixupValue (m.v); - processMessage (m); + m.v = fixupValue(m.v); + processMessage(m); }); } }; var Module = { onRuntimeInitialized: function () { - if (debug) console.log ("Done with WASM module instantiation."); + if (debug) console.log("Done with WASM module instantiation."); - Module.FS_createPath ("/", "managed", true, true); + Module.FS_createPath("/", "managed", true, true); var pending = 0; var mangled_ext_re = new RegExp("\\.bin$"); - this.assemblies.forEach (function(asm_mangled_name) { - var asm_name = asm_mangled_name.replace (mangled_ext_re, ".dll"); - if (debug) console.log ("Loading", asm_name); + this.assemblies.forEach(function (asm_mangled_name) { + var asm_name = asm_mangled_name.replace(mangled_ext_re, ".dll"); + if (debug) console.log("Loading", asm_name); ++pending; - fetch ("managed/" + asm_mangled_name, { credentials: 'same-origin' }).then (function (response) { + fetch("managed/" + asm_mangled_name, { credentials: 'same-origin' }).then(function (response) { if (!response.ok) throw "failed to load Assembly '" + asm_name + "'"; return response['arrayBuffer'](); - }).then (function (blob) { - var asm = new Uint8Array (blob); - Module.FS_createDataFile ("managed/" + asm_name, null, asm, true, true, true); + }).then(function (blob) { + var asm = new Uint8Array(blob); + Module.FS_createDataFile("managed/" + asm_name, null, asm, true, true, true); --pending; if (pending == 0) - Module.bclLoadingDone (); + Module.bclLoadingDone(); }); }); }, bclLoadingDone: function () { - if (debug) console.log ("Done loading the BCL."); - MonoRuntime.init (); + if (debug) console.log("Done loading the BCL."); + MonoRuntime.init(); } }; var MonoRuntime = { init: function () { - this.load_runtime = Module.cwrap ('mono_wasm_load_runtime', null, ['string', 'number']); - this.assembly_load = Module.cwrap ('mono_wasm_assembly_load', 'number', ['string']); - this.find_class = Module.cwrap ('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']); - this.find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']); - this.invoke_method = Module.cwrap ('mono_wasm_invoke_method', 'number', ['number', 'number', 'number']); - this.mono_string_get_utf8 = Module.cwrap ('mono_wasm_string_get_utf8', 'number', ['number']); - this.mono_string = Module.cwrap ('mono_wasm_string_from_js', 'number', ['string']); + this.load_runtime = Module.cwrap('mono_wasm_load_runtime', null, ['string', 'number']); + this.assembly_load = Module.cwrap('mono_wasm_assembly_load', 'number', ['string']); + this.find_class = Module.cwrap('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']); + this.find_method = Module.cwrap('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']); + this.invoke_method = Module.cwrap('mono_wasm_invoke_method', 'number', ['number', 'number', 'number']); + this.mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']); + this.mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']); - this.load_runtime ("managed", 1); + this.load_runtime("managed", 1); - if (debug) console.log ("Done initializing the runtime."); + if (debug) console.log("Done initializing the runtime."); - WebAssemblyApp.init (); + WebAssemblyApp.init(); }, conv_string: function (mono_obj) { if (mono_obj == 0) return null; - var raw = this.mono_string_get_utf8 (mono_obj); - var res = Module.UTF8ToString (raw); - Module._free (raw); + var raw = this.mono_string_get_utf8(mono_obj); + var res = Module.UTF8ToString(raw); + Module._free(raw); return res; }, call_method: function (method, this_arg, args) { - var args_mem = Module._malloc (args.length * 4); - var eh_throw = Module._malloc (4); + var args_mem = Module._malloc(args.length * 4); + var eh_throw = Module._malloc(4); for (var i = 0; i < args.length; ++i) - Module.setValue (args_mem + i * 4, args [i], "i32"); - Module.setValue (eh_throw, 0, "i32"); + Module.setValue(args_mem + i * 4, args[i], "i32"); + Module.setValue(eh_throw, 0, "i32"); - var res = this.invoke_method (method, this_arg, args_mem, eh_throw); + var res = this.invoke_method(method, this_arg, args_mem, eh_throw); - var eh_res = Module.getValue (eh_throw, "i32"); + var eh_res = Module.getValue(eh_throw, "i32"); - Module._free (args_mem); - Module._free (eh_throw); + Module._free(args_mem); + Module._free(eh_throw); if (eh_res != 0) { - var msg = this.conv_string (res); - throw new Error (msg); + var msg = this.conv_string(res); + throw new Error(msg); } return res; @@ -457,11 +431,11 @@ var MonoRuntime = { var WebAssemblyApp = { init: function () { - this.loading = document.getElementById ("loading"); + this.loading = document.getElementById("loading"); - this.findMethods (); + this.findMethods(); - this.runApp ("1", "2"); + this.runApp("1", "2"); this.loading.hidden = true; }, @@ -470,13 +444,13 @@ var WebAssemblyApp = { try { var sessionId = "main"; if (!!this.Goui_DisableServer_method) { - MonoRuntime.call_method (this.Goui_DisableServer_method, null, []); + MonoRuntime.call_method(this.Goui_DisableServer_method, null, []); } - MonoRuntime.call_method (this.main_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); + MonoRuntime.call_method(this.main_method, null, [MonoRuntime.mono_string(a), MonoRuntime.mono_string(b)]); wasmSession = sessionId; if (!!this.Goui_StartWebAssemblySession_method) { - var initialSize = getSize (); - MonoRuntime.call_method (this.Goui_StartWebAssemblySession_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string ("/"), MonoRuntime.mono_string (Math.round(initialSize.width) + " " + Math.round(initialSize.height))]); + var initialSize = getSize(); + MonoRuntime.call_method(this.Goui_StartWebAssemblySession_method, null, [MonoRuntime.mono_string(sessionId), MonoRuntime.mono_string("/"), MonoRuntime.mono_string(Math.round(initialSize.width) + " " + Math.round(initialSize.height))]); } } catch (e) { console.error(e); @@ -485,42 +459,41 @@ var WebAssemblyApp = { receiveMessagesJson: function (sessionId, json) { if (!!this.Goui_ReceiveWebAssemblySessionMessageJson_method) { - MonoRuntime.call_method (this.Goui_ReceiveWebAssemblySessionMessageJson_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string (json)]); + MonoRuntime.call_method(this.Goui_ReceiveWebAssemblySessionMessageJson_method, null, [MonoRuntime.mono_string(sessionId), MonoRuntime.mono_string(json)]); } }, findMethods: function () { - this.main_module = MonoRuntime.assembly_load (Module.entryPoint.a); + this.main_module = MonoRuntime.assembly_load(Module.entryPoint.a); if (!this.main_module) throw "Could not find Main Module " + Module.entryPoint.a + ".dll"; - this.main_class = MonoRuntime.find_class (this.main_module, Module.entryPoint.n, Module.entryPoint.t) + this.main_class = MonoRuntime.find_class(this.main_module, Module.entryPoint.n, Module.entryPoint.t) if (!this.main_class) throw "Could not find Program class in main module"; - this.main_method = MonoRuntime.find_method (this.main_class, Module.entryPoint.m, -1) + this.main_method = MonoRuntime.find_method(this.main_class, Module.entryPoint.m, -1) if (!this.main_method) throw "Could not find Main method"; - this.Goui_module = MonoRuntime.assembly_load ("Goui"); + this.Goui_module = MonoRuntime.assembly_load("Goui"); if (!!this.Goui_module) { - this.Goui_class = MonoRuntime.find_class (this.Goui_module, "Goui", "UI"); + this.Goui_class = MonoRuntime.find_class(this.Goui_module, "Goui", "UI"); if (!this.Goui_class) throw "Could not find UI class in Goui module"; - this.Goui_DisableServer_method = MonoRuntime.find_method (this.Goui_class, "DisableServer", -1); + this.Goui_DisableServer_method = MonoRuntime.find_method(this.Goui_class, "DisableServer", -1); if (!this.Goui_DisableServer_method) throw "Could not find DisableServer method"; - this.Goui_StartWebAssemblySession_method = MonoRuntime.find_method (this.Goui_class, "StartWebAssemblySession", -1); + this.Goui_StartWebAssemblySession_method = MonoRuntime.find_method(this.Goui_class, "StartWebAssemblySession", -1); if (!this.Goui_StartWebAssemblySession_method) throw "Could not find StartWebAssemblySession method"; - this.Goui_ReceiveWebAssemblySessionMessageJson_method = MonoRuntime.find_method (this.Goui_class, "ReceiveWebAssemblySessionMessageJson", -1); + this.Goui_ReceiveWebAssemblySessionMessageJson_method = MonoRuntime.find_method(this.Goui_class, "ReceiveWebAssemblySessionMessageJson", -1); if (!this.Goui_ReceiveWebAssemblySessionMessageJson_method) throw "Could not find ReceiveWebAssemblySessionMessageJson method"; } }, }; - diff --git a/Goui/EventTarget.cs b/Goui/EventTarget.cs index 8548d14..49e5a5d 100644 --- a/Goui/EventTarget.cs +++ b/Goui/EventTarget.cs @@ -4,17 +4,18 @@ using System.Reflection; using System.ComponentModel; -namespace Goui -{ - [Newtonsoft.Json.JsonConverter (typeof (EventTargetJsonConverter))] - public abstract class EventTarget : INotifyPropertyChanged - { - readonly List stateMessages = new List (); +namespace Goui { + [Newtonsoft.Json.JsonConverter(typeof(EventTargetJsonConverter))] + public abstract class EventTarget : INotifyPropertyChanged { + readonly List stateMessages = new List(); - readonly Dictionary> eventListeners = - new Dictionary> (); + readonly Dictionary> targetEventListeners = + new Dictionary>(); - public string Id { get; protected set; } = GenerateId (); + readonly Dictionary> domEventListeners = + new Dictionary>(); + + public string Id { get; protected set; } = GenerateId(); public string TagName { get; private set; } @@ -25,16 +26,15 @@ public abstract class EventTarget : INotifyPropertyChanged public IReadOnlyList StateMessages { get { lock (stateMessages) { - return new ReadOnlyList (stateMessages); + return new ReadOnlyList(stateMessages); } } } - protected EventTarget (string tagName) - { + protected EventTarget(string tagName) { TagName = tagName; - Send (new Message { + Send(new Message { MessageType = MessageType.Create, TargetId = Id, Key = TagName, @@ -43,91 +43,118 @@ protected EventTarget (string tagName) public override string ToString() => $"<{TagName} id=\"{Id}\" />"; - public virtual EventTarget GetElementById (string id) - { + public virtual EventTarget GetElementById(string id) { if (id == Id) return this; return null; } - public void AddEventListener (string eventType, TargetEventHandler handler) - { + public void AddEventListener(string eventType, TargetEventHandler handler) { if (eventType == null) return; if (handler == null) return; var sendListen = false; List handlers; - lock (eventListeners) { - if (!eventListeners.TryGetValue (eventType, out handlers)) { - handlers = new List (); - eventListeners[eventType] = handlers; + lock (targetEventListeners) { + if (!targetEventListeners.TryGetValue(eventType, out handlers)) { + handlers = new List(); + targetEventListeners[eventType] = handlers; sendListen = true; } - handlers.Add (handler); + handlers.Add(handler); } if (sendListen) - Send (new Message { + Send(new Message { MessageType = MessageType.Listen, TargetId = Id, Key = eventType, }); } - public void RemoveEventListener (string eventType, TargetEventHandler handler) - { + public void RemoveEventListener(string eventType, TargetEventHandler handler) { if (eventType == null) return; if (handler == null) return; List handlers; - lock (eventListeners) { - if (eventListeners.TryGetValue (eventType, out handlers)) { - handlers.Remove (handler); + lock (targetEventListeners) { + if (targetEventListeners.TryGetValue(eventType, out handlers)) { + handlers.Remove(handler); + } + } + } + + public void AddEventListener(string eventType, DOMEventHandler handler) { + if (eventType == null) return; + if (handler == null) return; + + var sendListen = false; + + List handlers; + lock (domEventListeners) { + if (!domEventListeners.TryGetValue(eventType, out handlers)) { + handlers = new List(); + domEventListeners[eventType] = handlers; + sendListen = true; + } + handlers.Add(handler); + } + + if (sendListen) + Send(new Message { + MessageType = MessageType.Listen, + TargetId = Id, + Key = eventType, + }); + } + + public void RemoveEventListener(string eventType, DOMEventHandler handler) { + if (eventType == null) return; + if (handler == null) return; + + List handlers; + lock (domEventListeners) { + if (domEventListeners.TryGetValue(eventType, out handlers)) { + handlers.Remove(handler); } } } - protected bool SetProperty (ref T backingStore, T newValue, string jsPropertyName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") - { - if (EqualityComparer.Default.Equals (backingStore, newValue)) + protected bool SetProperty(ref T backingStore, T newValue, string jsPropertyName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { + if (EqualityComparer.Default.Equals(backingStore, newValue)) return false; backingStore = newValue; - SendSet (jsPropertyName, newValue); - OnPropertyChanged (propertyName); + SendSet(jsPropertyName, newValue); + OnPropertyChanged(propertyName); return true; } - protected virtual void OnPropertyChanged (string propertyName) - { - PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName)); + protected virtual void OnPropertyChanged(string propertyName) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public const char IdPrefix = '\u2999'; static long idCounter = 0; - static string GenerateId () - { - var id = System.Threading.Interlocked.Increment (ref idCounter); + static string GenerateId() { + var id = System.Threading.Interlocked.Increment(ref idCounter); return $"{IdPrefix}{id}"; } - public void Send (Message message) - { + public void Send(Message message) { if (message == null) return; if (message.TargetId == Id) - SaveStateMessageIfNeeded (message); - MessageSent?.Invoke (message); + SaveStateMessageIfNeeded(message); + MessageSent?.Invoke(message); } - public void Call (string methodName, params object[] args) - { - Send (Message.Call (Id, methodName, args)); + public void Call(string methodName, params object[] args) { + Send(Message.Call(Id, methodName, args)); } - protected void SendSet (string jsPropertyName, object value) - { - Send (new Message { + protected void SendSet(string jsPropertyName, object value) { + Send(new Message { MessageType = MessageType.Set, TargetId = Id, Key = jsPropertyName, @@ -135,127 +162,137 @@ protected void SendSet (string jsPropertyName, object value) }); } - public void Receive (Message message) - { + public void Receive(Message message) { if (message == null) return; - SaveStateMessageIfNeeded (message); - TriggerEventFromMessage (message); + SaveStateMessageIfNeeded(message); + TriggerEventFromMessage(message); } - protected void AddStateMessage (Message message) - { - lock (stateMessages) stateMessages.Add (message); + protected void AddStateMessage(Message message) { + lock (stateMessages) stateMessages.Add(message); } - protected void UpdateStateMessages (Action> updater) - { - lock (stateMessages) updater (stateMessages); + protected void UpdateStateMessages(Action> updater) { + lock (stateMessages) updater(stateMessages); } - protected virtual bool SaveStateMessageIfNeeded (Message message) - { + protected virtual bool SaveStateMessageIfNeeded(Message message) { if (message.TargetId != Id) return false; switch (message.MessageType) { case MessageType.Create: - AddStateMessage (message); + AddStateMessage(message); break; case MessageType.Set: - UpdateStateMessages (state => { - state.RemoveAll (x => x.MessageType == MessageType.Set && x.Key == message.Key); - state.Add (message); + UpdateStateMessages(state => { + state.RemoveAll(x => x.MessageType == MessageType.Set && x.Key == message.Key); + state.Add(message); }); break; case MessageType.SetAttribute: - UpdateStateMessages (state => { - state.RemoveAll (x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key); - state.Add (message); + UpdateStateMessages(state => { + state.RemoveAll(x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key); + state.Add(message); }); break; case MessageType.RemoveAttribute: - this.UpdateStateMessages (state => { - state.RemoveAll (x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key); + this.UpdateStateMessages(state => { + state.RemoveAll(x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key); }); return true; case MessageType.Listen: - AddStateMessage (message); + AddStateMessage(message); break; } return true; } - protected virtual bool TriggerEvent (string name) - { + protected virtual bool TriggerEvent(string name) { List handlers = null; - lock (eventListeners) { + lock (targetEventListeners) { List hs; - if (eventListeners.TryGetValue (name, out hs)) { - handlers = new List (hs); + if (targetEventListeners.TryGetValue(name, out hs)) { + handlers = new List(hs); } } if (handlers != null) { - var args = new TargetEventArgs (); + var args = new TargetEventArgs(); foreach (var h in handlers) { - h.Invoke (this, args); + h.Invoke(this, args); } } return true; } - protected virtual bool TriggerEventFromMessage (Message message) - { + protected virtual bool TriggerEventFromMessage(Message message) { if (message.TargetId != Id) return false; - List handlers = null; - lock (eventListeners) { + List handlers = new List(); + lock (targetEventListeners) { List hs; - if (eventListeners.TryGetValue (message.Key, out hs)) { - handlers = new List (hs); + if (targetEventListeners.TryGetValue(message.Key, out hs)) { + handlers.AddRange(hs); } } - if (handlers != null) { - var args = new TargetEventArgs (); + lock (domEventListeners) { + List hs; + if (domEventListeners.TryGetValue(message.Key, out hs)) { + handlers.AddRange(hs); + } + } + if (handlers != null && handlers.Count > 0) { + var tArgs = new TargetEventArgs(); + var domArgs = new DOMEventArgs(); if (message.Value is Newtonsoft.Json.Linq.JObject o) { - args.OffsetX = (double)o["offsetX"]; - args.OffsetY = (double)o["offsetY"]; + domArgs.Data = o; + try { + tArgs.OffsetX = (double)o["offsetX"]; + tArgs.OffsetY = (double)o["offsetY"]; + } catch { } } foreach (var h in handlers) { - h.Invoke (this, args); + if (h is TargetEventHandler targetEventHandler) { + targetEventHandler.Invoke(this, tArgs); + } else if (h is DOMEventHandler domEventHandler) { + domEventHandler.Invoke(this, domArgs); + } else { + h.DynamicInvoke(this, message.Value); + } } } return true; } } - class EventTargetJsonConverter : Newtonsoft.Json.JsonConverter - { + class EventTargetJsonConverter : Newtonsoft.Json.JsonConverter { public override bool CanRead => false; - public override void WriteJson (Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) - { - writer.WriteValue (((EventTarget)value).Id); + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) { + writer.WriteValue(((EventTarget)value).Id); } - public override object ReadJson (Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) - { - throw new NotSupportedException (); + public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) { + throw new NotSupportedException(); } - public override bool CanConvert (Type objectType) - { - return typeof (EventTarget).GetTypeInfo ().IsAssignableFrom (objectType.GetTypeInfo ()); + public override bool CanConvert(Type objectType) { + return typeof(EventTarget).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); } } - public delegate void TargetEventHandler (object sender, TargetEventArgs e); + public delegate void TargetEventHandler(object sender, TargetEventArgs e); + public delegate void DOMEventHandler(object sender, DOMEventArgs e); - public class TargetEventArgs : EventArgs - { + public class TargetEventArgs : EventArgs { public double OffsetX { get; set; } public double OffsetY { get; set; } } + + public class DOMEventArgs : EventArgs { + public Newtonsoft.Json.Linq.JObject Data { get; set; } + } } diff --git a/Goui/HostedFile.cs b/Goui/HostedFile.cs new file mode 100644 index 0000000..376d3b5 --- /dev/null +++ b/Goui/HostedFile.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; +using System.Text; + +namespace Goui { + public class HostedFile { + public byte[] Data { get; private set; } + public string Etag { get; private set; } + public string ContentType { get; private set; } + public HostedFile(byte[] data, string contentType = "application/javascript") { + Data = data; + Etag = "\"" + Utilities.Hash(data) + "\""; + ContentType = contentType; + } + + public void Respond(HttpListenerContext listenerContext) { + var response = listenerContext.Response; + var inm = listenerContext.Request.Headers.Get("If-None-Match"); + if (string.IsNullOrEmpty(inm) || inm != Etag) { + response.StatusCode = 200; + response.ContentLength64 = Data.LongLength; + response.ContentType = ContentType; + response.ContentEncoding = Encoding.UTF8; + response.AddHeader("Cache-Control", "public, max-age=60"); + response.AddHeader("Etag", Etag); + using (var s = response.OutputStream) { + s.Write(Data, 0, Data.Length); + } + response.Close(); + } else { + response.StatusCode = 304; + response.Close(); + } + } + + public static HostedFile LoadFromResource(Assembly asm, string resource, string contentType = "application/javascript") { + using (var s = asm.GetManifestResourceStream(resource)) { + if (s == null) + throw new Exception("Missing " + resource); + using (var r = new StreamReader(s)) { + var data = Encoding.UTF8.GetBytes(r.ReadToEnd()); + return new HostedFile(data, contentType); + } + } + } + } +} diff --git a/Goui/IGouiPlugin.cs b/Goui/IGouiPlugin.cs index aabe90e..422036a 100644 --- a/Goui/IGouiPlugin.cs +++ b/Goui/IGouiPlugin.cs @@ -6,6 +6,7 @@ namespace Goui { public interface IGouiPlugin { + HostedFile GetHostedFile(string path); void OnNodeCreated(Node node); void OnUIStart(); void OnUIStop(); diff --git a/Goui/UI.cs b/Goui/UI.cs index 82e59e3..21b0737 100644 --- a/Goui/UI.cs +++ b/Goui/UI.cs @@ -315,6 +315,16 @@ static void ProcessRequest (HttpListenerContext listenerContext, CancellationTok var response = listenerContext.Response; + // If a plugin is hosting this file + foreach(var plugin in Config.Plugins) { + var hf = plugin.GetHostedFile(path); + if(hf != null) { + hf.Respond(listenerContext); + return; + } + } + + if (path == "/Goui.js") { var inm = listenerContext.Request.Headers.Get ("If-None-Match"); if (string.IsNullOrEmpty (inm) || inm != clientJsEtag) { diff --git a/Samples/MapSample.cs b/Samples/MapSample.cs new file mode 100644 index 0000000..d1cc64e --- /dev/null +++ b/Samples/MapSample.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Goui; +using Goui.Html; +using Goui.Plugin.Maps.Html; + +namespace Samples { + public class MapSample : ISample { + public string Title => "Map Sample"; + + GoogleMap MakeMap() { + var map = new GoogleMap(mapType: GoogleMap.MapType.HYBRID, firstMapControlOnPage: true) { + APIKey = "YOUR_GOOGLE_MAPS_API_KEY_HERE", + }; + + + //Add Marker from code on creating of view + map.AddMarker(new GoogleMap.MapMarker() { + lat = 51.511884, + lng = -0.195421, + title = "Testing Markers on Load", + infoWindow = new GoogleMap.MapInfoWindow() { + content = "This is the first pin placement (I think it is at Kensington Palace)" + } + }); + + ThreadPool.QueueUserWorkItem((state) => { + //Add Marker from code after view has been loaded + Thread.Sleep(5000); + map.AddMarker(new GoogleMap.MapMarker { + lat = 51.5073346, + lng = -0.1276831, + title = "Testing Markers", + infoWindow = new GoogleMap.MapInfoWindow { + content = "This is a random pin placement" + } + }); + }); + ThreadPool.QueueUserWorkItem((state) => { + //Change map center location after ten seconds and add a marker there + Thread.Sleep(10000); + + var pos = new GoogleMap.Position { + latitude = 40.6892, + longitude = 74.0445, + }; + map.CenterOn(pos); + map.AddMarker(new GoogleMap.MapMarker { + lat = pos.latitude, + lng = pos.longitude, + title = "Kyrgyzstan", + infoWindow = new GoogleMap.MapInfoWindow { + content = "This should be in Kyrgyzstan" + } + }); + ThreadPool.QueueUserWorkItem(async (state2) => { + await Task.Delay(5000); + + var pos2 = await map.GetCenter(); + }); + }); + return map; + } + + public void Publish() { + var b = MakeMap(); + + UI.Publish("/shared-map", b); + UI.Publish("/map", MakeMap); + } + + public Element CreateElement() { + return MakeMap(); + } + } +} + diff --git a/Samples/Program.cs b/Samples/Program.cs index e72ad5c..ca0205e 100644 --- a/Samples/Program.cs +++ b/Samples/Program.cs @@ -1,5 +1,6 @@ using System; using Goui; +using Goui.Plugin.Maps; namespace Samples { @@ -11,9 +12,10 @@ static void Main (string[] args) UI.Config = new UIConfig() { Plugins = new IGouiPlugin[] { + new MapsPlugin() } }; - UI.Port = 8081; + UI.Port = 8080; for (var i = 0; i < args.Length; i++) { var a = args[i]; switch (args[i]) { @@ -50,6 +52,7 @@ static void Main (string[] args) new XuzzleSample().Publish(); new WebViewSample().Publish(); new PickerSample().Publish(); + new MapSample().Publish(); UI.Present ("/display-alert"); diff --git a/Samples/Samples.csproj b/Samples/Samples.csproj index dc4301f..f7e1311 100644 --- a/Samples/Samples.csproj +++ b/Samples/Samples.csproj @@ -5,6 +5,7 @@ +