diff --git a/README.md b/README.md index 8ee45a1..aa671b7 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Widgets for [CMV](http://cmv.io/), the Configurable Map Viewer created by [Tim M - [Geoprocessor](https://github.com/tmcgee/cmv-widgets#geoprocessor) - [Layer Labels](https://github.com/tmcgee/cmv-widgets#layer-labels) - [Layer Toggle](https://github.com/tmcgee/cmv-widgets#layer-toggle) +- [Mapillary](https://github.com/tmcgee/cmv-widgets#mapillary) - [MessageBox](https://github.com/tmcgee/cmv-widgets#messagebox) - [Open External Map](https://github.com/tmcgee/cmv-widgets#open-external-map) - [Print Plus](https://github.com/tmcgee/cmv-widgets#print-plus) @@ -81,6 +82,13 @@ A simple widget to toggle the visibility of a set of layers. Only a single layer ##### Documentation - in the works ##### [Widget](https://github.com/tmcgee/cmv-widgets/tree/master/widgets/LayerToggle.js) +--- +### Mapillary +A replacement for the CMV Google StreetView widget that display street level imagery from [Mapillary](https://www.mapillary.com/) using [MapillaryJS](https://github.com/mapillary/mapillary-js). +##### [Documentation](https://github.com/tmcgee/cmv-widgets/tree/master/widgets/Mapillary/README.md) +##### [Demo](http://tmcgee.github.io/cmv-widgets/demo.html?config=mapillary) +![Screenshot](https://tmcgee.github.io/cmv-widgets/images/mapillary1.jpg) + --- ### MessageBox Show an Alert or Confirmation modal dialog box. Intended to be called from other widgets. diff --git a/config/mapillary.js b/config/mapillary.js new file mode 100644 index 0000000..0840b6c --- /dev/null +++ b/config/mapillary.js @@ -0,0 +1,51 @@ +define({ + isDebug: true, + + mapOptions: { + basemap: 'topo', + center: [-122.435, 37.775], + zoom: 13, + sliderStyle: 'small' + }, + + titles: { + header: 'CMV Mapillary Widget', + subHeader: 'This is an example of displaying street level imagery from Mapillary', + pageTitle: 'CMV Mapillary Widget' + }, + + collapseButtonsPane: 'center', //center or outer + + operationalLayers: [], + + widgets: { + mapillary: { + include: true, + type: 'titlePane', + title: 'Mapillary', + iconClass: 'fa-location-arrow fa-rotate-90', + open: true, + position: 0, + path: 'widgets/Mapillary', + canFloat: true, + paneOptions: { + resizable: true, + resizeOptions: { + minSize: { + w: 250, + h: 250 + } + } + }, + options: { + map: true, + mapillaryOptions: { + // this is for demo purposes only + // get your own clientID at mapillary.com + clientID: 'cjJ1SUtVOEMtdy11b21JM0tyYTZIQTpiNjQ0MTgzNTIzZGM2Mjhl', + photoID: null + } + } + } + } +}); diff --git a/images/mapillary1.jpg b/images/mapillary1.jpg new file mode 100644 index 0000000..7d28ecd Binary files /dev/null and b/images/mapillary1.jpg differ diff --git a/index.html b/index.html index e4b1601..6908e58 100644 --- a/index.html +++ b/index.html @@ -159,6 +159,22 @@

Layer Toggle

+
+
+

Mapillary

+

A replacement for the CMV Google StreetView widget that display street level imagery from Mapillary using MapillaryJS.

+

+ Demo +

+

+ +

Screenshot
+ +

+

+
+
+

MessageBox

diff --git a/widgets/Mapillary.js b/widgets/Mapillary.js new file mode 100644 index 0000000..db74494 --- /dev/null +++ b/widgets/Mapillary.js @@ -0,0 +1,414 @@ +define([ + 'dojo/_base/declare', + 'dijit/_WidgetBase', + 'dijit/_TemplatedMixin', + 'dijit/_WidgetsInTemplateMixin', + + 'dojo/_base/lang', + 'dojo/topic', + 'dojo/on', + 'dojo/aspect', + 'dojo/dom', + 'dojo/dom-class', + 'dojo/dom-style', + 'dojo/dom-geometry', + + 'esri/Color', + 'esri/geometry/Point', + 'esri/geometry/webMercatorUtils', + 'esri/SpatialReference', + + 'esri/graphic', + 'esri/InfoTemplate', + + 'esri/layers/GraphicsLayer', + 'esri/layers/VectorTileLayer', + 'esri/symbols/PictureMarkerSymbol', + 'esri/renderers/SimpleRenderer', + + 'dijit/MenuItem', + + 'proj4js/proj4', + + 'dojo/text!./Mapillary/templates/Mapillary.html', + 'dojo/i18n!./Mapillary/nls/Mapillary', + + 'https://unpkg.com/mapillary-js@2.5.2/dist/mapillary.min.js', + 'xstyle/css!https://unpkg.com/mapillary-js@2.5.2/dist/mapillary.min.css', + + 'xstyle/css!./Mapillary/css/Mapillary.css', + + 'dijit/form/ToggleButton', + 'dijit/form/CheckBox' + +], function ( + declare, + _WidgetBase, + _TemplatedMixin, + _WidgetsInTemplateMixin, + + lang, + topic, + on, + aspect, + dom, + domClass, + domStyle, + domGeom, + + Color, + Point, + webMercatorUtils, + SpatialReference, + + Graphic, + InfoTemplate, + + GraphicsLayer, + VectorTileLayer, + PictureMarkerSymbol, + SimpleRenderer, + + MenuItem, + proj4, + + template, + i18n, + + Mapillary + +) { + return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], { + widgetsInTemplate: true, + templateString: template, + i18n: i18n, + baseClass: 'cmvMapillaryWidget', + + mapClickMode: null, + + // in case this changes some day + proj4BaseURL: 'https://epsg.io/', + + // options are ESRI, EPSG and SR-ORG + // See http://sepsg.io/ for more information + proj4Catalog: 'EPSG', + + // if desired, you can load a projection file from your server + // instead of using one from epsg.io + // i.e., http://server/projections/102642.js + proj4CustomURL: null, + + domID: 'mapillary-node', + + mapillaryOptions: {}, + defaultMapillaryOptions: { + clientID: null, + photoID: null, + options: { + baseImageSize: Mapillary.ImageSize.Size320, + basePanoramaSize: Mapillary.ImageSize.Size1024, + component: { + cache: false, + cover: false, + direction: true + }, + maxImageSize: Mapillary.ImageSize.Size1024, + renderMode: Mapillary.RenderMode.Letterbox + } + }, + + layerOptions: {}, + defaultLayerOptions: { + url: require.toUrl('./widgets/Mapillary/mapillary-style.js'), + id: 'mapillary', + opacity: 0.6, + visible: false + }, + + visibleOnFirstOpen: true, + + startup: function () { + this.inherited(arguments); + + this.mapillaryOptions = this.mixinDeep(this.defaultMapillaryOptions, this.mapillaryOptions); + this.layerOptions = this.mixinDeep(this.defaultLayerOptions, this.layerOptions); + + this.createMapillary(); + + this.createGraphicsLayer(); + this.createVectorLayer(); + + this.own(topic.subscribe('mapClickMode/currentSet', lang.hitch(this, 'setMapClickMode'))); + + this.map.on('click', lang.hitch(this, 'getMapillary')); + + if (this.parentWidget) { + if (this.parentWidget.toggleable) { + if (this.parentWidget.get('open') && this.visibleOnFirstOpen) { + this.layer.setVisibility(true); + } + this.own(aspect.after(this.parentWidget, 'toggle', lang.hitch(this, function () { + this.onLayoutChange(this.parentWidget.open); + }))); + } + this.own(aspect.after(this.parentWidget, 'resize', lang.hitch(this, 'resize'))); + this.own(topic.subscribe(this.parentWidget.id + '/resize/resize', lang.hitch(this, 'resize'))); + } + + if (!window.proj4) { + window.proj4 = proj4; + } + + if (this.mapRightClickMenu) { + this.addRightClickMenu(); + } + + }, + + createMapillary: function () { + this.mapillary = new Mapillary.Viewer( + this.domID, + this.mapillaryOptions.clientID, + this.mapillaryOptions.photoID, + this.mapillaryOptions.options + ); + this.mapillary.on(Mapillary.Viewer.nodechanged, lang.hitch(this, 'onNodeChanged')); + this.mapillary.on(Mapillary.Viewer.bearingchanged, lang.hitch(this, 'onBearingChanged')); + }, + + createVectorLayer: function () { + this.layer = new VectorTileLayer(this.layerOptions.url, this.layerOptions); + this.map.addLayer(this.layer); + + this.layer.on('visibility-change', lang.hitch(this, function () { + this.checkLayerVisible.set('checked', this.layer.visible); + this.visibleOnFirstOpen = false; + })); + + this.checkLayerVisible.set('checked', this.layer.visible); + on(this.checkLayerVisible, 'change', lang.hitch(this, function () { + this.layer.setVisibility(this.checkLayerVisible.get('checked')); + })); + }, + + createGraphicsLayer: function () { + this.pointGraphics = new GraphicsLayer({ + id: 'mapillary_graphics', + title: 'Mapillary' + }); + this.pointSymbol = new PictureMarkerSymbol(require.toUrl('widgets/Mapillary/images/blueArrow.png'), 24, 24); + this.pointRenderer = new SimpleRenderer(this.pointSymbol); + this.pointRenderer.label = 'Mapillary'; + this.pointRenderer.description = 'Mapillary'; + this.pointGraphics.setRenderer(this.pointRenderer); + this.map.addLayer(this.pointGraphics); + }, + + addRightClickMenu: function () { + this.map.on('MouseDown', lang.hitch(this, function (evt) { + this.mapRightClickPoint = evt.mapPoint; + })); + this.mapRightClickMenu.addChild(new MenuItem({ + label: this.i18n.rightClickMenuItem.label, + onClick: lang.hitch(this, 'mapillaryFromMapRightClick') + })); + }, + + resize: function (options) { + if (options && options.h) { + domGeom.setContentSize(this.containerNode, { + h: (options.h - 2) + }); + } + this.mapillary.resize(); + }, + + getMapillary: function (evt, overRide) { + if (this.mapClickMode === 'mapillary' || overRide) { + var mapPoint = evt.mapPoint; + if (!mapPoint) { + return; + } + + if (this.parentWidget && !this.parentWidget.open) { + this.parentWidget.toggle(); + } + + this.disableMapClick(); + + // convert the map point's coordinate system into lat/long + var geometry = null, + wkid = mapPoint.spatialReference.wkid; + if (wkid === 102100) { + wkid = 3857; + } + var key = this.proj4Catalog + ':' + String(wkid); + if (!proj4.defs[key]) { + var url = this.proj4CustomURL || this.proj4BaseURL + String(wkid) + '.js'; + require([url], lang.hitch(this, 'getMapillary', evt, true)); + return; + } + // only need one projection as we are + // converting to WGS84 lat/long + var projPoint = proj4(proj4.defs[key]).inverse([mapPoint.x, mapPoint.y]); + if (projPoint) { + geometry = { + x: projPoint[0], + y: projPoint[1] + }; + } + + domStyle.set(this.mapillaryInstructions, 'display', 'none'); + if (geometry) { + domStyle.set(this.noMapillaryResults, 'display', 'none'); + domStyle.set(this.mapillaryNode, 'display', 'block'); + this.getMapillaryLocation(geometry); + } else { + this.clearGraphics(); + domStyle.set(this.noMapillaryResults, 'display', 'block'); + domStyle.set(this.mapillaryNode, 'display', 'none'); + } + } + }, + + getMapillaryLocation: function (geometry) { + var promise = this.mapillary.moveCloseTo(geometry.y, geometry.x); + promise.catch(lang.hitch(this, function () { + this.clearGraphics(); + domStyle.set(this.noMapillaryResults, 'display', 'block'); + domStyle.set(this.mapillaryNode, 'display', 'none'); + })); + }, + + mapillaryFromMapRightClick: function () { + var evt = { + mapPoint: this.mapRightClickPoint + }; + this.getMapillary(evt, true); + }, + + onNodeChanged: function (node) { + var lng = node.latLon.lon; + var lat = node.latLon.lat; + + this.setPlaceMarkerPosition(lat, lng); + this.mapillary.resize(); + }, + + onBearingChanged: function (bearing) { + if (this.placeMarker) { + this.pointSymbol.setAngle(bearing); + this.pointGraphics.refresh(); + } + }, + + onOpen: function () { + this.pointGraphics.show(); + if (this.visibleOnFirstOpen) { + this.layer.setVisibility(true); + } + }, + + onClose: function () { + // end mapillary on close of title pane + this.pointGraphics.hide(); + this.layer.setVisibility(false); + if (this.mapClickMode === 'mapillary') { + this.connectMapClick(); + } + }, + onLayoutChange: function (open) { + if (open) { + this.onOpen(); + } else { + this.onClose(); + } + }, + + placePoint: function () { + if (this.mapillaryButtonDijit.get('checked')) { + this.enableMapClick(); + } else { + this.disableMapClick(); + } + }, + + enableMapClick: function () { + this.mapillaryButtonDijit.set('checked', true); + this.map.setMapCursor('crosshair'); + this.layer.setVisibility(true); + topic.publish('mapClickMode/setCurrent', 'mapillary'); + }, + + disableMapClick: function () { + this.mapillaryButtonDijit.set('checked', false); + this.map.setMapCursor('auto'); + topic.publish('mapClickMode/setDefault'); + }, + + clearGraphics: function () { + this.pointGraphics.clear(); + domStyle.set(this.noMapillaryResults, 'display', 'block'); + }, + + setPlaceMarkerPosition: function (lat, lng) { + if (!this.placeMarker || this.pointGraphics.graphics.length === 0) { + this.placeMarker = new Graphic(); + // Add graphic to the map + this.pointGraphics.add(this.placeMarker); + } + // get the new lat/long from streetview + // Make sure they are numbers + if (!isNaN(lat) && !isNaN(lng)) { + // convert the resulting lat/long to the map's spatial reference + var xy = null, + wkid = this.map.spatialReference.wkid; + if (wkid === 102100) { + wkid = 3857; + } + var key = this.proj4Catalog + ':' + String(wkid); + if (!proj4.defs[key]) { + var url = this.proj4CustomURL || this.proj4BaseURL + String(wkid) + '.js'; + require([url], lang.hitch(this, 'setPlaceMarkerPosition')); + return; + } + // only need the one projection as we are + // converting from WGS84 lat/long + xy = proj4(proj4.defs[key]).forward([lng, lat]); + if (xy) { + var point = new Point(xy, new SpatialReference({ + wkid: wkid + })); + + // change point position on the map + this.placeMarker.setGeometry(point); + } + } + }, + + setMapClickMode: function (mode) { + this.mapClickMode = mode; + }, + + mixinDeep: function (dest, source) { + //Recursively mix the properties of two objects + var empty = {}; + for (var name in source) { + if (!(name in dest) || (dest[name] !== source[name] && (!(name in empty) || empty[name] !== source[name]))) { + try { + if (source[name].constructor === Object) { + dest[name] = this.mixinDeep(dest[name], source[name]); + } else { + dest[name] = source[name]; + } + } catch (e) { + // Property in destination object not set. Create it and set its value. + dest[name] = source[name]; + } + } + } + return dest; + } + + }); +}); \ No newline at end of file diff --git a/widgets/Mapillary/README.md b/widgets/Mapillary/README.md new file mode 100644 index 0000000..63f4593 --- /dev/null +++ b/widgets/Mapillary/README.md @@ -0,0 +1,46 @@ +# Mapillary Widget for CMV +A replacement for the CMV Google StreetView widget that display street level imagery from [Mapillary](https://www.mapillary.com/) using [MapillaryJS](https://github.com/mapillary/mapillary-js). + +To use MapillaryJS you must [create an account](https://www.mapillary.com/signup) and [obtain a Client ID](https://www.mapillary.com/app/settings/developers). + +--- +## Configurable Options + +| Parameter | Type | Description | +| :----: | :--: | ----------- | +| `mapillaryOptions` | Object | Options supported by the [Mapillary Viewer](https://github.com/mapillary/mapillary-js) | +| `layerOptions` | Object | Options for the VectorTiles layer displaying the Mapillary coverage | + +--- +## Example Configuration: +``` javascript +mapillary: { + include: true, + type: 'titlePane', + title: 'Mapillary', + iconClass: 'fa-location-arrow fa-rotate-90', + open: true, + position: 0, + path: 'widgets/Mapillary', + canFloat: true, + paneOptions: { + resizable: true, + resizeOptions: { + minSize: { + w: 250, + h: 250 + } + } + }, + options: { + map: true, + mapillaryOptions: { + clientID: 'insert-your-own-client-id', + photoID: null + } + } +} +``` +## Screenshot: +![Screenshot](https://tmcgee.github.io/cmv-widgets/images/mapillary1.jpg) + diff --git a/widgets/Mapillary/css/Mapillary.css b/widgets/Mapillary/css/Mapillary.css new file mode 100644 index 0000000..0bdecda --- /dev/null +++ b/widgets/Mapillary/css/Mapillary.css @@ -0,0 +1,72 @@ +.cmvMapillaryWidget { + min-height: 275px; + position: relative; + width: 100%; +} + +.cmvMapillaryWidget .mapillary-button { + left: 4px; + position: absolute; + top: 4px; + z-index: 9; +} + +.cmvMapillaryWidget .mapillary-button .dijitButtonNode { + color: #36AF6D; + font-size: 1.5em; + padding: 6px 6px 6px 6px; +} + +.cmvMapillaryWidget .mapillary-container { + background-color: #202020; + height: calc(100% - 40px); + position: absolute; + top: 0; + width: 100%; +} + +.floatingWidget .cmvMapillaryWidget .mapillary-container { + height: 275px; + width: 400px; +} + +.cmvMapillaryWidget .mapillary-checkbox { + bottom: 0; + height: 20px; + margin: 10px; + position: absolute; +} + +.cmvMapillaryWidget .mapillary-instructions, .cmvMapillaryWidget .mapillary-no-results { + background-color: #202020; + height: 100%; + position: absolute; + width: 100%; + z-index: 2; +} + +.cmvMapillaryWidget .mapillary-no-results { + display: none; +} + +.cmvMapillaryWidget .mapillary-instructions-text, .cmvMapillaryWidget .mapillary-no-results-text { + color: white; + padding: 10px; + position: absolute; + top: 50px; + z-index: 2; +} + +.cmvMapillaryWidget .mapillary-js { + height: 100%; + position: absolute; + width: 100%; +} + +.cmvMapillaryWidget .mapillary-js canvas { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; +} diff --git a/widgets/Mapillary/images/blueArrow.png b/widgets/Mapillary/images/blueArrow.png new file mode 100644 index 0000000..bf84313 Binary files /dev/null and b/widgets/Mapillary/images/blueArrow.png differ diff --git a/widgets/Mapillary/mapillary-style.js b/widgets/Mapillary/mapillary-style.js new file mode 100644 index 0000000..203d6ce --- /dev/null +++ b/widgets/Mapillary/mapillary-style.js @@ -0,0 +1,54 @@ +{ + "version": 8, + "sources": { + "mapillary-source": { + "tiles": [ + "http://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt" + ], + "type": "vector", + "minzoom": 0, + "maxzoom": 18 + } + }, + "layers": [ + { + "id": "mapillary-lines", + "type": "line", + "source": "mapillary-source", + "source-layer": "mapillary-sequences", + "minzoom": 6, + "layout": { + "line-join": "round", + "line-cap": "round" + }, + "paint": { + "line-color": "#39AF64", + "line-width": 2 + } + }, + { + "id": "mapillary-overview", + "type": "symbol", + "source": "mapillary-source", + "source-layer": "mapillary-sequence-overview", + "maxzoom": 6, + "layout": { + "icon-image": "Road/Rectangle green yellow (Alt)/2", + "icon-allow-overlap": true, + "symbol-avoid-edges": true + } + }, + { + "id": "mapillary-dots", + "type": "symbol", + "source": "mapillary-source", + "source-layer": "mapillary-images", + "minzoom": 14, + "layout": { + "icon-image": "Road/Rectangle green yellow (Alt)/2", + "icon-allow-overlap": true, + "symbol-avoid-edges": true + } + } + ] +} \ No newline at end of file diff --git a/widgets/Mapillary/nls/Mapillary.js b/widgets/Mapillary/nls/Mapillary.js new file mode 100644 index 0000000..c19689a --- /dev/null +++ b/widgets/Mapillary/nls/Mapillary.js @@ -0,0 +1,17 @@ +// https://dojotoolkit.org/reference-guide/1.10/dojo/i18n.html +define({ + root: { + messages: { + instructions: 'Click the Mapillary button then click the map at your desired location.', + notAvailable: 'Unfortunately, Mapillary imagery is not yet available at that location.' + }, + rightClickMenuItem: { + label: 'Mapillary imagery here' + } + }, + 'es': true, + 'fr': true, + 'pt-br': true, + 'pt-pt': true +}); + diff --git a/widgets/Mapillary/nls/es/Mapillary.js b/widgets/Mapillary/nls/es/Mapillary.js new file mode 100644 index 0000000..11aa1c7 --- /dev/null +++ b/widgets/Mapillary/nls/es/Mapillary.js @@ -0,0 +1,9 @@ +define ({ + messages: { + instructions: 'Haga clic en el botón de Mapillary a continuación, haga clic en el mapa en su posición deseada.', + notAvailable: 'Desafortunadamente, Mapillary todavía no está disponible en ese lugar.' + }, + rightClickMenuItem: { + label: 'Mapillary aquí' + } +}); \ No newline at end of file diff --git a/widgets/Mapillary/nls/fr/Mapillary.js b/widgets/Mapillary/nls/fr/Mapillary.js new file mode 100644 index 0000000..1b56b6c --- /dev/null +++ b/widgets/Mapillary/nls/fr/Mapillary.js @@ -0,0 +1,9 @@ +define ({ + messages: { + instructions: 'Cliquez sur le bouton Mapillary puis cliquez sur la carte à l\'endroit désiré.', + notAvailable: 'Malheureusement, les images de Mapillary ne sont pas encore disponible à cet endroit.' + }, + rightClickMenuItem: { + label: 'Ouvrir Mapillary à cet endroit' + } +}); \ No newline at end of file diff --git a/widgets/Mapillary/nls/pt-br/Mapillary.js b/widgets/Mapillary/nls/pt-br/Mapillary.js new file mode 100644 index 0000000..53594cc --- /dev/null +++ b/widgets/Mapillary/nls/pt-br/Mapillary.js @@ -0,0 +1,10 @@ +// http://dojotoolkit.org/reference-guide/1.10/dojo/i18n.html +define({ + messages: { + instructions: 'Clique no botão do Mapillary e depois clique na localização desejada no mapa.', + notAvailable: 'Infelizmente, o Mapillary não está disponível nesta localização.' + }, + rightClickMenuItem: { + label: 'Mapillary aqui' + } +}); \ No newline at end of file diff --git a/widgets/Mapillary/nls/pt-pt/Mapillary.js b/widgets/Mapillary/nls/pt-pt/Mapillary.js new file mode 100644 index 0000000..53594cc --- /dev/null +++ b/widgets/Mapillary/nls/pt-pt/Mapillary.js @@ -0,0 +1,10 @@ +// http://dojotoolkit.org/reference-guide/1.10/dojo/i18n.html +define({ + messages: { + instructions: 'Clique no botão do Mapillary e depois clique na localização desejada no mapa.', + notAvailable: 'Infelizmente, o Mapillary não está disponível nesta localização.' + }, + rightClickMenuItem: { + label: 'Mapillary aqui' + } +}); \ No newline at end of file diff --git a/widgets/Mapillary/templates/Mapillary.html b/widgets/Mapillary/templates/Mapillary.html new file mode 100644 index 0000000..4375c65 --- /dev/null +++ b/widgets/Mapillary/templates/Mapillary.html @@ -0,0 +1,20 @@ +
+
+ + +
+ ${i18n.messages.instructions} +
+
+ +
+ ${i18n.messages.notAvailable} +
+
+ +
+
+ + +
+
\ No newline at end of file