diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6510134 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "editor.tabSize": 4, + "editor.insertSpaces": true, + "files.eol": "\n", + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/node_modules/**": true, + ".tmp": true + }, + "files.exclude": { + ".tmp": true, + "coverage": true + }, + "search.exclude": { + ".tmp": true, + "dist": true, + "coverage": true + }, + "json.schemas": [ + { + "fileMatch": [ + "/pbiviz.json" + ], + "url": "./.api/v1.6.0/schema.pbiviz.json" + }, + { + "fileMatch": [ + "/capabilities.json" + ], + "url": "./.api/v1.6.0/schema.capabilities.json" + } + ] +} diff --git a/bower.json b/bower.json index 3a4ee9b..5dd20ad 100644 --- a/bower.json +++ b/bower.json @@ -11,8 +11,7 @@ "**/.*", "node_modules", "bower_components", - "test", - "tests" + "test" ], "dependencies": { "webgl-heatmap": "*" diff --git a/package.json b/package.json index d93e510..36b8f81 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { "name": "powerbi-visuals-globemap", "description": "GlobeMap", - "version": "1.4.3", + "version": "1.4.7", "author": { "name": "Microsoft", "email": "pbicvsupport@microsoft.com" }, "scripts": { - "postinstall": "bower install && pbiviz update 1.5.0", - "typings": "typings", + "postinstall": "bower install && pbiviz update 1.6.0", "pbiviz": "pbiviz", "start": "pbiviz start", "package": "pbiviz package", - "lint": "node node_modules/tslint/bin/tslint -r \"node_modules/tslint-microsoft-contrib\" \"+(src|test)/**/*.ts\"", + "lint": "tslint -r \"node_modules/tslint-microsoft-contrib\" \"+(src|test)/**/*.ts\"", "pretest": "pbiviz package --resources --no-minify --no-pbiviz", "test": "karma start" }, @@ -22,36 +21,35 @@ "url": "git+https://github.com/Microsoft/PowerBI-visuals-globemap.git" }, "dependencies": { - "globalize": "0.1.0-a2", - "three": "0.75.0", - "lodash": "4.16.2", - "moment": "2.15.1", - "jquery": "3.1.1", "d3": "3.5.5", - "powerbi-visuals-utils-chartutils": "0.2.1", - "powerbi-visuals-utils-colorutils": "0.2.1", - "powerbi-visuals-utils-dataviewutils": "1.0.1" + "globalize": "0.1.0-a2", + "jquery": "3.1.1", + "lodash": "4.17.4", + "powerbi-visuals-utils-colorutils": "0.2.2", + "powerbi-visuals-utils-dataviewutils": "1.1.1", + "powerbi-visuals-utils-formattingutils": "1.0.0", + "powerbi-visuals-utils-interactivityutils": "0.3.1", + "powerbi-visuals-utils-typeutils": "1.0.0", + "three": "0.75.0" }, "devDependencies": { "@types/d3": "3.5.36", - "@types/jasmine": "2.5.37", - "@types/jasmine-jquery": "1.5.28", + "@types/jasmine": "2.5.47", + "@types/jasmine-jquery": "1.5.30", "@types/jquery": "2.0.41", - "@types/lodash": "4.14.43", + "@types/lodash": "4.14.55", "@types/three": "0.0.19", - "bingmaps": "1.0.12", "bower": "1.8.0", - "jasmine": "2.5.2", + "jasmine": "2.6.0", "jasmine-jquery": "2.1.1", - "karma": "1.3.0", + "karma": "1.6.0", "karma-chrome-launcher": "2.0.0", - "karma-jasmine": "1.0.2", - "karma-typescript-preprocessor": "0.3.0", - "powerbi-visuals-tools": "1.5.0", - "powerbi-visuals-utils-testutils": "0.2.2", - "powerbi-visuals-utils-typeutils": "^0.2.1", - "tslint": "^4.4.2", - "tslint-microsoft-contrib": "^4.0.0", + "karma-jasmine": "1.1.0", + "karma-typescript-preprocessor": "0.3.1", + "powerbi-visuals-tools": "1.6.3", + "powerbi-visuals-utils-testutils": "1.0.0", + "tslint": "4.5.1", + "tslint-microsoft-contrib": "^4.0.1", "typescript": "2.1.4" } } diff --git a/pbiviz.json b/pbiviz.json index fe765a0..8cdda02 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -4,12 +4,12 @@ "displayName": "GlobeMap", "guid": "GlobeMap1447669447624", "visualClassName": "GlobeMap", - "version": "1.4.5", + "version": "1.4.7", "description": "A 3D visual using WebGL for plotting locations, with category values displayed as bar heights and heat maps.\n\nShift+Click on bar to change center point. \nSlicing data points will animate to average location.\n\nAttributions:\nthree.js - https://github.com/mrdoob/three.js/\nwebgl-heatmap - https://github.com/pyalot/webgl-heatmap", "supportUrl": "http://community.powerbi.com", "gitHubUrl": "https://github.com/Microsoft/powerbi-visuals-globemap" }, - "apiVersion": "1.5.0", + "apiVersion": "1.6.0", "author": { "name": "Microsoft", "email": "pbicvsupport@microsoft.com" @@ -20,7 +20,7 @@ "externalJS": [ "node_modules/jquery/dist/jquery.min.js", "node_modules/lodash/lodash.min.js", - "node_modules/d3/d3.js", + "node_modules/d3/d3.min.js", "node_modules/three/three.js", "src/lib/OrbitControls.js", "node_modules/powerbi-visuals-utils-typeutils/lib/index.js", @@ -30,7 +30,6 @@ "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js", "node_modules/powerbi-visuals-utils-formattingutils/lib/index.js", "node_modules/powerbi-visuals-utils-interactivityutils/lib/index.js", - "node_modules/powerbi-visuals-utils-chartutils/lib/index.js", "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.js", "node_modules/powerbi-visuals-utils-colorutils/lib/index.js", "bower_components/webgl-heatmap/webgl-heatmap.js" diff --git a/src/UrlUtils/UrlUtils.ts b/src/UrlUtils/UrlUtils.ts index a12b730..a095f3e 100644 --- a/src/UrlUtils/UrlUtils.ts +++ b/src/UrlUtils/UrlUtils.ts @@ -25,165 +25,7 @@ */ namespace powerbi.extensibility.utils { - import RegExpExtensions = powerbi.extensibility.utils.type.RegExpExtensions; - export module Deprecated { - export const escape: (s: string) => string = window['escape']; - export const unescape: (s: string) => string = window['unescape']; - } - - export interface TextMatch { - start: number; - end: number; - text: string; - } - export module UrlUtils { - const urlRegex = /http[s]?:\/\/(\S)+/gi; - - export function isValidUrl(value: string): boolean { - if (_.isEmpty(value)) { - return false; - } - - let match: RegExpExecArray = RegExpExtensions.run(urlRegex, value); - if (!!match && match.index === 0) { - return true; - } - - return false; - } - - /* Tests whether a URL is valid. - * @param url The url to be tested. - * @returns Whether the provided url is valid. - **/ - export function isValidImageUrl(url: string): boolean { - // For now, passes for any valid Url - return isValidUrl(url); - } - - export function findAllValidUrls(text: string): TextMatch[] { - if (_.isEmpty(text)) { - return []; - } - - // Find all urls in the text. - // TODO: This could potentially be expensive, maybe include a cap here for text with many urls? - let urlRanges: TextMatch[] = []; - let matches: RegExpExecArray; - let start: number = 0; - while ((matches = RegExpExtensions.run(urlRegex, text, start)) !== null) { - let url: any = matches[0]; - let end: number = matches.index + url.length; - urlRanges.push({ - start: matches.index, - end: end, - text: url, - }); - start = end; - } - - return urlRanges; - } - - export function isDataUri(uri: string): boolean { - return uri && uri.indexOf('data:') === 0; - } - - export function getBase64ContentFromDataUri(uri: string): string { - if (!isDataUri(uri)) { - throw new Error("Expected data uri"); - } - - // Locate the base 64 content from the URL (e.g. "data:image/png;base64,xxxxx=") - const base64Token = ";base64,"; - let indexBase64TokenStart: number = uri.indexOf(base64Token); - if (indexBase64TokenStart < 0) { - throw new Error("Expected base 64 content in data url"); - } - - let indexBase64Start: number = indexBase64TokenStart + base64Token.length; - return uri.substr(indexBase64Start, uri.length - indexBase64Start); - } - - /** - * Create a base64 data URI for a string with a UTF-8 character encoding. - * @param rawText {string} The text string to be encapsulated. It is the raw Javascript string - */ - export function makeUTF8EncodedBase64DataUri(contentType: string, rawText: string): string { - return "data:" + contentType + ";base64," + UrlUtils.utoa(rawText); - } - - export function makeJsonDataUri(rawJson: string): string { - return makeUTF8EncodedBase64DataUri("application/json", rawJson); - } - - // btoa does not work for char codes > 0xff. for these we have to UTF-8 encode it - // first. cleverly combining the deprecated functions unescape/escape with - // encode/decodeURIComponent gets the browser to do all the work. in case - // unescape/escape are not present, use slower Javascript implementations. - // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa#Unicode_strings - // http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html - // http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%201st%20edition,%20June%201997.pdf#sec-15.1.2.4 - - // exported for testing - export function escapeSlow(s: string): string { - if (!s) { - return s; - } - - return s.replace(/[^*+\-./0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz]/g, unescaped => { - let escaped: any = unescaped.charCodeAt(0).toString(16).toUpperCase(); - switch (escaped.length) { - case 1: return '%0' + escaped; - case 2: return '%' + escaped; - case 3: return '%u0' + escaped; - default: return '%u' + escaped; - } - }); - } - - // exported for testing - export function unescapeSlow(s: string): string { - if (!s) { - return s; - } - - return s.replace(/%([0-9a-fA-F]{2})|%u([0-9a-fA-F]{4})/g, (_, short, long) => { - return String.fromCharCode(parseInt(short || long, 16)); - }); - } - - const unescape: (s: string) => string = Deprecated.unescape || unescapeSlow; - const escape: (s: string) => string = Deprecated.escape || escapeSlow; - - export function encodeUTF8(s: string): string { - return unescape(encodeURIComponent(s)); - } - - export function decodeUTF8(s: string): string { - return decodeURIComponent(escape(s)); - } - - export function utoa(s: string): string { - return btoa(encodeUTF8(s)); - } - - export function atou(s: string): string { - return decodeUTF8(atob(s)); - } - - /** Returns the set of query parameters in a URL */ - export function getQueryParameters(url: string): _.Dictionary { - const query = getQueryString(url); - - if (!query) { - return; - } - - return parseQueryString(query); - } - /** * Given a URL, set the provided query string parameters * @param url The URL to modify @@ -202,11 +44,11 @@ namespace powerbi.extensibility.utils { return result; } - result += '?' + _.chain(parameters) + result += "?" + _.chain(parameters) .toPairs() - .map(pair => pair.join('=')) + .map(pair => pair.join("=")) .value() - .join('&'); + .join("&"); return result; } @@ -222,57 +64,8 @@ namespace powerbi.extensibility.utils { }; } - export interface ParsedUrl { - scheme: string; - host: string; - path: string; - query: string; - fragment: string; - } - - export function parseUrl(url: string): ParsedUrl { - // see http://www.ietf.org/rfc/rfc3986.txt, Appendix B (around page 50) - // ^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))? - // 1 2 3 4 5 - // http://www.ics.uci.edu/pub/ietf/uri/#Related - // scheme = $1 = http - // host = $2 = www.ics.uci.edu - // path = $3 = /pub/ietf/uri/ - // query = $4 = - // fragment = $5 = Related - let matches: RegExpMatchArray = url.match(/^(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/); - if (matches) { - return { - scheme: matches[1], - host: matches[2], - path: matches[3], - query: matches[4], - fragment: matches[5] - }; - } - } - - export function getHost(url: string): string { - let parsed: ParsedUrl = parseUrl(url); - return parsed && parsed.host; - } - - const HostnameRegex: any = /https?:\/\/[^\/]+/i; - - /** - * Returns everything in a URL after the hostname. Per RFC 3986, this is known as the absolute path reference. - * @example for "https://foo.bar/hello/world", return "/hello/world". - */ - export function getAbsolutePath(url: string): string { - if (!url) { - return url; - } - - return url.replace(HostnameRegex, ''); - } - function getQueryString(url: string): string { - let elem: HTMLAnchorElement = document.createElement('a'); + let elem: HTMLAnchorElement = document.createElement("a"); elem.href = url; return elem.search; @@ -284,7 +77,7 @@ namespace powerbi.extensibility.utils { return null; } - if (_.startsWith(queryString, '?')) { + if (_.startsWith(queryString, "?")) { queryString = queryString.substring(1); } diff --git a/src/columns.ts b/src/columns.ts index feeba7a..6d4ff30 100644 --- a/src/columns.ts +++ b/src/columns.ts @@ -29,44 +29,12 @@ module powerbi.extensibility.visual { import DataViewValueColumns = powerbi.DataViewValueColumns; import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn; import DataViewValueColumn = powerbi.DataViewValueColumn; + + // powerbi.extensibility.utils.dataview import converterHelper = powerbi.extensibility.utils.dataview.converterHelper; export class GlobeMapColumns { - public static getColumnSources(dataView: DataView): GlobeMapColumns { - return this.getColumnSourcesT(dataView); - } - - public static getTableValues(dataView: DataView): GlobeMapColumns | DataViewTable { - let table: DataViewTable = dataView && dataView.table; - let columns: GlobeMapColumns = this.getColumnSourcesT(dataView); - return columns && table && _.mapValues( - columns, (n: DataViewMetadataColumn, i) => n && table.rows.map(row => row[n.index])); - } - - public static getTableRows(dataView: DataView): GlobeMapColumns | DataViewTable | GlobeMapColumns[] { - let table: DataViewTable = dataView && dataView.table; - let columns: GlobeMapColumns = this.getColumnSourcesT(dataView); - return columns && table && table.rows.map(row => - _.mapValues(columns, (n: DataViewMetadataColumn, i) => n && row[n.index])); - } - - public static getCategoricalValues(dataView: DataView): DataViewCategorical | GlobeMapColumns { - let categorical: DataViewCategorical = dataView && dataView.categorical; - let categories: DataViewCategoryColumn[] = categorical && categorical.categories || []; - let values: DataViewValueColumns = categorical && categorical.values || []; - let series: DataViewCategorical = categorical && values.source && this.getSeriesValues(dataView); - return categorical && _.mapValues(new this(), (n, i) => - (_.toArray(categories)).concat(_.toArray(values)) - .filter(x => x.source.roles && x.source.roles[i]).map(x => x.values)[0] - || values.source && values.source.roles && values.source.roles[i] && series); - } - - public static getSeriesValues(dataView: DataView) { - return dataView && dataView.categorical && dataView.categorical.values - && dataView.categorical.values.map(x => converterHelper.getSeriesName(x.source)); - } - public static getCategoricalColumns(dataView: DataView): DataViewCategorical | any { let categorical = dataView && dataView.categorical; let categories = categorical && categorical.categories || []; @@ -87,12 +55,6 @@ module powerbi.extensibility.visual { (n, i) => g.values.filter(v => v.source.roles[i])[0])); } - private static getColumnSourcesT(dataView: DataView): any { - let columns: any = dataView && dataView.metadata && dataView.metadata.columns; - return columns && _.mapValues( - new this(), (n, i) => columns.filter(x => x.roles && x.roles[i])[0]); - } - public Category: T = null; public Series: T = null; public X: T = null; diff --git a/src/dataInterfaces.ts b/src/dataInterfaces.ts index 9eda459..f75547d 100644 --- a/src/dataInterfaces.ts +++ b/src/dataInterfaces.ts @@ -25,7 +25,10 @@ */ module powerbi.extensibility.visual { + // powerbi.extensibility.utils.interactivity import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint; + + // powerbi.extensibility.geocoder import ILocation = powerbi.extensibility.geocoder.ILocation; export interface GlobeMapData { @@ -52,6 +55,32 @@ module powerbi.extensibility.visual { color: string; category?: string; } + + export interface BingMetadata { + resourceSets: ResourceSet[]; + statusCode: string; + statusDescription: string; + } + + export interface ResourceSet { + resources: BingResourceMetadata[]; + } + + export interface BingResourceMetadata { + imageHeight: number; + imageWidth: number; + imageUrl: string; + imageUrlSubdomains: string[]; + } + + export interface TileMap { + [quadKey: string]: string; + } + + export interface CanvasCoordinate { + x: number; + y: number; + } } diff --git a/src/geocoder/geocoder.ts b/src/geocoder/geocoder.ts index 6a70dbe..ffbbc47 100644 --- a/src/geocoder/geocoder.ts +++ b/src/geocoder/geocoder.ts @@ -700,7 +700,6 @@ module powerbi.extensibility.geocoder { } this.activeEntries.push(entry); - entry.request = $.ajax({ url: url, dataType: 'jsonp', diff --git a/src/geocoder/geocoderInterfaces.ts b/src/geocoder/geocoderInterfaces.ts index fcf38e8..7d6218e 100644 --- a/src/geocoder/geocoderInterfaces.ts +++ b/src/geocoder/geocoderInterfaces.ts @@ -1,13 +1,18 @@ module powerbi.extensibility.geocoder { import IPromise = powerbi.IPromise; - import IRect = powerbi.extensibility.utils.svg.IRect; - /** Defines geocoding services. */ export interface GeocodeOptions { /** promise that should abort the request when resolved */ timeout?: IPromise; } + export interface IRect { + left: number; + top: number; + width: number; + height: number; + } + export interface IGeocoder { geocode(query: string, category?: string, options?: GeocodeOptions): IPromise; geocodeBoundary(latitude: number, longitude: number, category: string, levelOfDetail?: number, maxGeoData?: number, options?: GeocodeOptions): IPromise; diff --git a/src/geocoder/geocodingCache.ts b/src/geocoder/geocodingCache.ts index 1b078ee..9c5288b 100644 --- a/src/geocoder/geocodingCache.ts +++ b/src/geocoder/geocodingCache.ts @@ -25,8 +25,9 @@ */ module powerbi.extensibility.geocoder { - + // powerbi.extensibility.utils.formatting import IStorageService = powerbi.extensibility.utils.formatting.IStorageService; + import LocalStorageService = powerbi.extensibility.utils.formatting.LocalStorageService; interface GeocodeCacheEntry { coordinate: IGeocodeCoordinate; @@ -41,7 +42,7 @@ module powerbi.extensibility.geocoder { export function createGeocodingCache(maxCacheSize: number, maxCacheSizeOverflow: number, localStorageService?: IStorageService): IGeocodingCache { if (!localStorageService) { - localStorageService = new powerbi.extensibility.utils.formatting.LocalStorageService(); + localStorageService = new LocalStorageService(); } return new GeocodingCache(maxCacheSize, maxCacheSizeOverflow, localStorageService); } diff --git a/src/globemap.ts b/src/globemap.ts index 01e70ad..81c2d52 100644 --- a/src/globemap.ts +++ b/src/globemap.ts @@ -1,4 +1,4 @@ -/* +/* * Power BI Visualizations * * Copyright (c) Microsoft Corporation @@ -24,51 +24,44 @@ * THE SOFTWARE. */ -let WebGLHeatmap: any = window['createWebGLHeatmap']; -let GlobeMapCanvasLayers: JQuery[]; +let WebGLHeatmap: any = window["createWebGLHeatmap"]; module powerbi.extensibility.visual { + // powerbi.extensibility.geocoder import IGeocoder = powerbi.extensibility.geocoder.IGeocoder; import IGeocodeCoordinate = powerbi.extensibility.geocoder.IGeocodeCoordinate; - import IPromise = powerbi.IPromise; - import Rectangle = powerbi.extensibility.utils.svg.touch.Rectangle; import ILocation = powerbi.extensibility.geocoder.ILocation; + + // powerbi.extensibility.utils.dataview import converterHelper = powerbi.extensibility.utils.dataview.converterHelper; + + // powerbi.extensibility.utils.color import ColorHelper = powerbi.extensibility.utils.color.ColorHelper; - import ClassAndSelector = powerbi.extensibility.utils.svg.CssConstants.ClassAndSelector; - import createClassAndSelector = powerbi.extensibility.utils.svg.CssConstants.createClassAndSelector; - import DataViewPropertyValue = powerbi.DataViewPropertyValue; - import SelectableDataPoint = powerbi.extensibility.utils.interactivity.SelectableDataPoint; + // powerbi.visuals + import ISelectionId = powerbi.visuals.ISelectionId; + + // powerbi.extensibility.utils.formatting import IValueFormatter = powerbi.extensibility.utils.formatting.IValueFormatter; - import IInteractivityService = powerbi.extensibility.utils.interactivity.IInteractivityService; - import IMargin = powerbi.extensibility.utils.chart.axis.IMargin; - import IInteractiveBehavior = powerbi.extensibility.utils.interactivity.IInteractiveBehavior; - import ISelectionHandler = powerbi.extensibility.utils.interactivity.ISelectionHandler; - import appendClearCatcher = powerbi.extensibility.utils.interactivity.appendClearCatcher; - import createInteractivityService = powerbi.extensibility.utils.interactivity.createInteractivityService; import valueFormatter = powerbi.extensibility.utils.formatting.valueFormatter; - import IAxisProperties = powerbi.extensibility.utils.chart.axis.IAxisProperties; - import IVisualHost = powerbi.extensibility.visual.IVisualHost; - import svg = powerbi.extensibility.utils.svg; - import axis = powerbi.extensibility.utils.chart.axis; - import textMeasurementService = powerbi.extensibility.utils.formatting.textMeasurementService; - import ValueType = utils.type.ValueType; - import DataViewObjectsParser = utils.dataview.DataViewObjectsParser; - import IColorPalette = powerbi.extensibility.IColorPalette; + import LocalStorageService = powerbi.extensibility.utils.formatting.LocalStorageService; + import IStorageService = powerbi.extensibility.utils.formatting.IStorageService; + + // powerbi.extensibility.utils.type + import ValueType = powerbi.extensibility.utils.type.ValueType; interface ExtendedPromise extends IPromise { always(value: any): void; } - export class GlobeMap implements IVisual { + private localStorageService: IStorageService; public static MercartorSphere: any; private static GlobeSettings = { autoRotate: false, earthRadius: 30, cameraRadius: 100, earthSegments: 100, - heatmapSize: 1000, + heatmapSize: 1024, heatPointSize: 7, heatIntensity: 10, heatmapScaleOnZoom: 0.95, @@ -79,7 +72,13 @@ module powerbi.extensibility.visual { cameraAnimDuration: 1000, // ms clickInterval: 200 // ms }; - + private static ChangeDataType: number = 2; + private static ChangeAllType: number = 62; + private static DataPointFillProperty: DataViewObjectPropertyIdentifier = { + objectName: "dataPoint", + propertyName: "fill" + }; + private static CountTilesPerSegment: number = 4; private layout: VisualLayout; private root: JQuery; private rendererContainer: JQuery; @@ -111,7 +110,6 @@ module powerbi.extensibility.visual { private hoveredBar: any; private averageBarVector: THREE.Vector3; private zoomContainer: d3.Selection; - private zoomControl: d3.Selection; public colors: IColorPalette; private animationFrameId: number; private cameraAnimationFrameId: number; @@ -126,15 +124,13 @@ module powerbi.extensibility.visual { || (_.isEmpty(categorical.Height) && _.isEmpty(categorical.Heat))) { return null; } - const properties: GlobeMapSettings = GlobeMapSettings.getDefault() as GlobeMapSettings; const settings: GlobeMapSettings = GlobeMap.parseSettings(dataView); const groupedColumns: GlobeMapColumns[] | any = GlobeMapColumns.getGroupedValueColumns(dataView); const dataPoints: any = []; let seriesDataPoints: any = []; let locations: any = []; - const colorHelper: ColorHelper = new ColorHelper(colors, null, properties.dataPoint.fill); - + const colorHelper: ColorHelper = new ColorHelper(colors, GlobeMap.DataPointFillProperty); let locationType: any; let heights: any; let heightsBySeries: any; @@ -158,9 +154,9 @@ module powerbi.extensibility.visual { // creating a matrix for drawing values by series later. for (let i: number = 0; i < groupedColumns.length; i++) { const values: any = groupedColumns[i].Height.values; + seriesDataPoints[i] = GlobeMap.createDataPointForEnumeration( dataView, groupedColumns[i].Height.source, i, null, colorHelper, colors, visualHost); - seriesDataPoints[i].color = settings.dataPoint.fill; for (let j: number = 0; j < values.length; j++) { if (!heights[j]) { heights[j] = 0; @@ -192,13 +188,11 @@ module powerbi.extensibility.visual { heightsBySeries = []; seriesDataPoints[0] = GlobeMap.createDataPointForEnumeration( dataView, groupedColumns[0].Height.source, 0, dataView.metadata, colorHelper, colors, visualHost); - seriesDataPoints[0].color = settings.dataPoint.fill; } } else { heightsBySeries = []; heights = []; } - if (!_.isEmpty(categorical.Heat)) { if (groupedColumns.length > 1) { heats = []; @@ -262,7 +256,6 @@ module powerbi.extensibility.visual { dataPoints.push(renderDatum); } } - return { dataView: dataView, dataPoints: dataPoints, @@ -296,19 +289,20 @@ module powerbi.extensibility.visual { const label: string = valueFormatter.format(nameForFormat, valueFormatter.getFormatString(sourceForFormat, null)); + let measureValues = values[0]; const categoryColumn: DataViewCategoryColumn = { - source: values[seriesIndex].source, + source: measureValues.source, values: null, - identity: [values[seriesIndex].identity] + identity: [measureValues.identity] }; const identity: ISelectionId = visualHost.createSelectionIdBuilder() .withCategory(categoryColumn, 0) - .withMeasure(values[seriesIndex].source.queryName) + .withMeasure(measureValues.source.queryName) .createSelectionId(); const category: any = converterHelper.getSeriesName(source); - const objects: any = columns.objects; + const objects: any = columns.objects || source.objects; const color: string = objects && objects.dataPoint ? objects.dataPoint.fill.solid.color : metaData && metaData.objects ? colorHelper.getColorForMeasure(metaData.objects, "") : colors.getColor(seriesIndex).value; @@ -321,16 +315,47 @@ module powerbi.extensibility.visual { }; } + private addAnInstanceToEnumeration( + instanceEnumeration: VisualObjectInstanceEnumeration, + instance: VisualObjectInstance): void { + + if ((instanceEnumeration as VisualObjectInstanceEnumerationObject).instances) { + (instanceEnumeration as VisualObjectInstanceEnumerationObject) + .instances + .push(instance); + } else { + (instanceEnumeration as VisualObjectInstance[]).push(instance); + } + } + public enumerateObjectInstances(options: EnumerateVisualObjectInstancesOptions): VisualObjectInstance[] | VisualObjectInstanceEnumerationObject { - return GlobeMapSettings.enumerateObjectInstances(this.settings || GlobeMapSettings.getDefault(), options); + let instances: VisualObjectInstanceEnumeration = GlobeMapSettings.enumerateObjectInstances(this.settings || GlobeMapSettings.getDefault(), options); + switch (options.objectName) { + case "dataPoint": if (this.data && this.data.seriesDataPoints) { + for (let i: number = 0; i < this.data.seriesDataPoints.length; i++) { + let dataPoint: GlobeMapSeriesDataPoint = this.data.seriesDataPoints[i]; + this.addAnInstanceToEnumeration(instances, { + objectName: "dataPoint", + displayName: dataPoint.label, + selector: ColorHelper.normalizeSelector((dataPoint.identity as ISelectionId).getSelector()), + properties: { + fill: { solid: { color: dataPoint.color } } + } + }); + } + } + break; + } + return instances; } constructor(options: VisualConstructorOptions) { - + this.currentLanguage = options.host.locale; + this.localStorageService = new LocalStorageService(); this.root = $("
").appendTo(options.element) - .attr('drag-resize-disabled', "true") + .attr("drag-resize-disabled", "true") .css({ - 'position': "absolute" + "position": "absolute" }); this.visualHost = options.host; @@ -351,12 +376,16 @@ module powerbi.extensibility.visual { } private setup(): void { - this.initTextures(); - this.initMercartorSphere(); - this.initZoomControl(); this.initScene(); + this.initMercartorSphere(); + this.initTextures().then( + () => { + this.earth = this.createEarth(); + this.scene.add(this.earth); + this.readyToRender = true; + }); + this.initZoomControl(); this.initHeatmap(); - this.readyToRender = true; this.initRayCaster(); } private static cameraFov: number = 35; @@ -366,6 +395,26 @@ module powerbi.extensibility.visual { private static ambientLight: number = 0x000000; private static directionalLight: number = 0xffffff; private static directionalLightIntensity: number = 0.4; + private static tileSize: number = 256; + private static maxResolutionLevel: number = 4; + private static metadataUrl: string = `https://dev.virtualearth.net/REST/V1/Imagery/Metadata/Road?output=json&uriScheme=https&key=${powerbi.extensibility.geocoder.Settings.BingKey}`; + private static reserveBindMapsMetadata: BingResourceMetadata = { + imageUrl: "https://{subdomain}.tiles.virtualearth.net/tiles/r{quadkey}.jpeg?g=0&mkt={culture}", + imageUrlSubdomains: [ + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7" + ], + imageHeight: 256, + imageWidth: 256 + }; + private currentLanguage: string = "en-GB"; + private static TILE_STORAGE_KEY = "GLOBEMAP_TILES_STORAGE"; + private static TILE_LANGUAGE_CULTURE = "GLOBEMAP_TILE_LANGUAGE_CULTURE"; private initScene(): void { this.renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); this.rendererContainer = $("
").appendTo(this.root).addClass("globeMapView"); @@ -390,12 +439,10 @@ module powerbi.extensibility.visual { const ambientLight: THREE.AmbientLight = new THREE.AmbientLight(GlobeMap.ambientLight); const light1: THREE.DirectionalLight = new THREE.DirectionalLight(GlobeMap.directionalLight, GlobeMap.directionalLightIntensity); const light2: THREE.DirectionalLight = new THREE.DirectionalLight(GlobeMap.directionalLight, GlobeMap.directionalLightIntensity); - const earth: THREE.Mesh = this.earth = this.createEarth(); this.scene.add(ambientLight); this.scene.add(light1); this.scene.add(light2); - this.scene.add(earth); light1.position.set(20, 20, 20); light2.position.set(0, 0, -20); @@ -447,7 +494,7 @@ module powerbi.extensibility.visual { } private static dollyX: number = 0.95; - public zoomClicked(zoomDirection: any): void { + public zoomClicked(zoomDirection: number): void { if (this.orbitControls.enabled === false) { return; } @@ -462,7 +509,7 @@ module powerbi.extensibility.visual { this.animateCamera(this.camera.position); } - public rotateCam(deltaX: number, deltaY: number) { + public rotateCam(deltaX: number, deltaY: number): void { if (!this.orbitControls.enabled) { return; } @@ -472,33 +519,145 @@ module powerbi.extensibility.visual { this.animateCamera(this.camera.position); } - private initTextures() { - if (!GlobeMapCanvasLayers) { + private initTextures(): JQueryPromise<{}> { + this.mapTextures = []; + const tileCulture: string = this.localStorageService.getData(GlobeMap.TILE_LANGUAGE_CULTURE); + let tileCache: TileMap[] = this.localStorageService.getData(GlobeMap.TILE_STORAGE_KEY); + if (!tileCache || tileCulture !== this.currentLanguage) { // Initialize once, since this is a CPU + Network heavy operation. - GlobeMapCanvasLayers = []; + return this.getBingMapsServerMetadata() + .then((metadata: BingResourceMetadata) => { + tileCache = []; + let urlTemplate = metadata.imageUrl.replace("{culture}", this.currentLanguage); + for (let level: number = 1; level <= GlobeMap.maxResolutionLevel; ++level) { + let levelTiles = this.generateQuadsByLevel(level, urlTemplate, metadata.imageUrlSubdomains); + this.mapTextures.push(this.createTexture(level, levelTiles)); + tileCache.push(levelTiles); + } + this.localStorageService.setData(GlobeMap.TILE_STORAGE_KEY, tileCache); + this.localStorageService.setData(GlobeMap.TILE_LANGUAGE_CULTURE, this.currentLanguage); + return tileCache; + }); + } else { + for (let level: number = 1; level <= GlobeMap.maxResolutionLevel; ++level) { + this.mapTextures.push(this.createTexture(level, tileCache[level - 1])); + } + return jQuery.when(tileCache); + } + } - for (let level: number = 2; level <= 5; ++level) { - const canvas: JQuery = this.getBingMapCanvas(level); - GlobeMapCanvasLayers.push(canvas); + private getBingMapsServerMetadata(): JQueryPromise { + return $.ajax(GlobeMap.metadataUrl) + .then((data: BingMetadata) => { + if (data.resourceSets.length) { + let resourceSet = data.resourceSets[0]; + if (resourceSet && resourceSet.resources.length) { + return resourceSet.resources[0]; + } + } + throw "Bing Maps API response was changed. Please update code for new version"; + }) + .fail((error: any) => { + console.error(JSON.stringify(error)); + return GlobeMap.reserveBindMapsMetadata; + }); + } + + /** + * Generate Bing tile object by map level + * @see https://msdn.microsoft.com/en-us/library/bb259689.aspx + * @private + * @param {number} level map lavel + * @param {string} urlTemplate url template + * @example https://ecn.{subdomain}.tiles.virtualearth.net/tiles/r{quadkey}.jpeg?g=5691&mkt={culture}&shading=hill + * @param {string[]} subdomains list of subdomauns + * @returns {{ [quadKey: string]: string }} Object : + * @memberOf GlobeMap + */ + private generateQuadsByLevel(level: number, urlTemplate: string, subdomains: string[]): TileMap { + const result: TileMap = {}; + let currentSubDomainNumber: number = 0; + const generateQuard = (currentLevel: number = 0, quadKey: string = ""): void => { + if (currentLevel < level) { + for (let i = 0; i < GlobeMap.CountTilesPerSegment; i++) { + generateQuard(currentLevel + 1, `${quadKey}${i}`); + } + } else if (currentLevel === level) { + result[quadKey] = urlTemplate.replace("{subdomain}", subdomains[currentSubDomainNumber]).replace("{quadkey}", quadKey); + currentSubDomainNumber++; + currentSubDomainNumber = currentSubDomainNumber < subdomains.length ? currentSubDomainNumber : 0; + } + }; + generateQuard(); + return result; + } + + private createTexture(level: number, tiles: TileMap): THREE.Texture { + const numSegments: number = Math.pow(2, level); + const canvasSize: number = GlobeMap.tileSize * numSegments; + const canvas: HTMLCanvasElement = document.createElement("canvas"); + canvas.width = canvasSize; + canvas.height = canvasSize; + const texture: THREE.Texture = new THREE.Texture(canvas); + texture.needsUpdate = true; + this.loadTiles(canvas, tiles, () => { + texture.needsUpdate = true; + this.needsRender = true; + }); + return texture; + } + + /** + * Load tiles of Bing Maps + * @param jCanvas jQuery convas object + * @param tiles map of tiles + * @param successCallback call this function when all tiles of the map are successfully loaded + */ + private loadTiles(canvasEl: HTMLCanvasElement, tiles: TileMap, successCallback: Function): void { + let tilesLoaded: number = 0; + const countTiles: number = Object.keys(tiles).length; + const canvasContext: CanvasRenderingContext2D = canvasEl.getContext("2d"); + for (let quadKey in tiles) { + if (tiles.hasOwnProperty(quadKey)) { + const coords: any = this.getCoordByQuadKey(quadKey); + const tile: HTMLImageElement = new Image(); + tile.onload = (event: Event) => { + tilesLoaded++; + canvasContext.drawImage(tile, coords.x * GlobeMap.tileSize, coords.y * GlobeMap.tileSize, GlobeMap.tileSize, GlobeMap.tileSize); + if (tilesLoaded === countTiles) { + successCallback(); + } + }; + // So the canvas doesn't get tainted + tile.crossOrigin = ""; + tile.src = tiles[quadKey]; + } + } + } + + /** + * Get coordinates by Bing Maps quard name + * @private + * @param {string} quard Bing Maps quard name + * @returns {CanvasCoordinate} image coordinate + * @memberOf GlobeMap + */ + private getCoordByQuadKey(quard: string): CanvasCoordinate { + const last: number = quard.length - 1; + let x: number = 0; + let y: number = 0; + + for (let i: number = last; i >= 0; i--) { + const chr: string = quard.charAt(i); + const pow: number = Math.pow(2, last - i); + switch (chr) { + case "1": x += pow; break; + case "2": y += pow; break; + case "3": x += pow; y += pow; break; } } - // Can't execute in for loop because variable assignement gets overwritten - const createTexture: (canvas: JQuery) => THREE.Texture = (canvas: JQuery) => { - const texture: THREE.Texture = new THREE.Texture(canvas.get(0)); - texture.needsUpdate = true; - canvas.on("ready", () => { - texture.needsUpdate = true; - this.needsRender = true; - }); - return texture; - - }; - - this.mapTextures = []; - for (let i: number = 0; i < GlobeMapCanvasLayers.length; ++i) { - this.mapTextures.push(createTexture(GlobeMapCanvasLayers[i])); - } + return { x: x, y: y }; } private initHeatmap() { @@ -531,19 +690,12 @@ module powerbi.extensibility.visual { } const maxDistance: number = GlobeMap.GlobeSettings.cameraRadius - GlobeMap.GlobeSettings.earthRadius; const distance: number = (this.camera.position.length() - GlobeMap.GlobeSettings.earthRadius) / maxDistance; - let texture: THREE.Texture; - const oneOfFive: number = 1 / 5; - const twoOfFive: number = 2 / 5; - const threeOfFive: number = 3 / 5; - - if (distance <= oneOfFive) { - texture = this.mapTextures[3]; - } else if (distance <= twoOfFive) { - texture = this.mapTextures[2]; - } else if (distance <= threeOfFive) { - texture = this.mapTextures[1]; - } else { - texture = this.mapTextures[0]; + let texture: THREE.Texture = this.mapTextures[0]; + for (let divider = 1; divider <= GlobeMap.maxResolutionLevel; divider++) { + if (distance <= divider / GlobeMap.maxResolutionLevel) { + texture = this.mapTextures[GlobeMap.maxResolutionLevel - divider]; + break; + } } if ((this.earth.material).map !== texture) { @@ -558,18 +710,15 @@ module powerbi.extensibility.visual { } public update(options: VisualUpdateOptions): void { - if (options.dataViews === undefined || options.dataViews === null) { return; } this.layout.viewport = options.viewport; this.root.css(this.layout.viewportIn); - const sixPointsToAdd: number = 6; this.zoomContainer.style({ - 'padding-left': (this.layout.viewportIn.width - parseFloat(this.zoomControl.attr("width")) + sixPointsToAdd) + "px", // Fix for chrome - 'display': this.layout.viewportIn.height > $(this.zoomContainer.node()).height() - && this.layout.viewportIn.width > $(this.zoomContainer.node()).width() - ? null : 'none' + "display": this.layout.viewportIn.height > GlobeMap.ZoomControlSettings.height + && this.layout.viewportIn.width > GlobeMap.ZoomControlSettings.width + ? null : "none" }); if (this.layout.viewportChanged) { @@ -581,7 +730,7 @@ module powerbi.extensibility.visual { } } - if (options.type === VisualUpdateType.Data || options.type === VisualUpdateType.All) { + if (options.type === GlobeMap.ChangeDataType || options.type === GlobeMap.ChangeAllType) { this.cleanHeatAndBar(); const data: GlobeMapData = GlobeMap.converter(options.dataViews[0], this.colors, this.visualHost); if (data) { @@ -603,9 +752,9 @@ module powerbi.extensibility.visual { if (!this.data) { return; } - this.data.dataPoints.forEach(d => this.geocodeRenderDatum(d)); + this.data.dataPoints.forEach(d => this.geocodeRenderDatum(d)); // all coordinates (latitude/longitude) will be gained here this.data.dataPoints.forEach((d) => { - return d.location = d.location || this.globeMapLocationCache[d.placeKey]; + return d.location = this.globeMapLocationCache[d.placeKey] || d.location; }); if (!this.readyToRender) { @@ -627,7 +776,9 @@ module powerbi.extensibility.visual { for (let i: number = 0; i < len; ++i) { const renderDatum: GlobeMapDataPoint = this.data.dataPoints[i]; - if (!renderDatum.location || renderDatum.location.longitude === undefined || renderDatum.location.latitude === undefined) { + if (!renderDatum.location || renderDatum.location.longitude === undefined || renderDatum.location.latitude === undefined + || (renderDatum.location.longitude === 0 && renderDatum.location.latitude === 0) + ) { continue; } @@ -658,7 +809,7 @@ module powerbi.extensibility.visual { const dataPointToolTip: any = []; if (renderDatum.heightBySeries) { for (let c: number = 0; c < renderDatum.heightBySeries.length; c++) { - if (renderDatum.heightBySeries[c]) { + if (renderDatum.heightBySeries[c] || renderDatum.heightBySeries[c] === 0) { measuresBySeries.push(renderDatum.heightBySeries[c]); } dataPointToolTip.push(renderDatum.seriesToolTipData[c]); @@ -713,7 +864,8 @@ module powerbi.extensibility.visual { } private geocodeRenderDatum(renderDatum: GlobeMapDataPoint) { - if (renderDatum.location || this.globeMapLocationCache[renderDatum.placeKey]) { + // zero valued locations should be updated + if ((renderDatum.location && renderDatum.location.longitude !== 0 && renderDatum.location.latitude !== 0) || this.globeMapLocationCache[renderDatum.placeKey]) { return; } @@ -947,7 +1099,7 @@ module powerbi.extensibility.visual { this.camera = null; if (this.renderer) { if (this.renderer.context) { - const extension: any = this.renderer.context.getExtension('WEBGL_lose_context'); + const extension: any = this.renderer.context.getExtension("WEBGL_lose_context"); if (extension) { extension.loseContext(); } @@ -976,143 +1128,69 @@ module powerbi.extensibility.visual { this.hideTooltip(); } + private static ZoomControlSettings = { + height: 145, + width: 145, + markup: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + zoomStep: 1, + angleOfRotation: 5 - private static zoomControlRatio: number = 8.5; - private static radiusRatio: number = 3; - private static gapRadiusRatio: number = 2; + }; private initZoomControl() { - const radius: number = 17; - const zoomControlWidth: number = radius * GlobeMap.zoomControlRatio; - const zoomControlHeight: number = radius * GlobeMap.zoomControlRatio; - const startX: number = radius * GlobeMap.radiusRatio; - const startY: number = radius + GlobeMap.radiusRatio; - const gap: number = radius * GlobeMap.gapRadiusRatio; - - this.zoomContainer = d3.select(this.root[0]) - .append('div') - .classed('zoomContainer', true); - - this.zoomControl = this.zoomContainer.append("svg").attr({ - 'width': zoomControlWidth, - 'height': zoomControlHeight - }).classed('zoomContainerSvg', true); - - const bottom: d3.Selection = this.zoomControl.append("g") - .on("mousedown", () => onMouseDown(() => this.rotateCam(0, -5))); - - bottom.append("circle") - .attr({ - cx: startX + gap, - cy: startY + (2 * gap), - r: radius - }) - .classed('zoomControlCircle', true); - bottom.append("path") - .attr({ - d: "M" + (startX + (2 * radius)) + " " + (startY + (radius * 4.7)) + " l12 -20 a40,70 0 0,1 -24,0z", - fill: "gray" - }); - - const left: d3.Selection = this.zoomControl - .append("g") - .on("mousedown", () => onMouseDown(() => this.rotateCam(5, 0))); - left.append("circle") - .attr({ - cx: startX, - cy: startY + gap, - r: radius - }) - .classed('zoomControlCircle', true); - left.append("path") - .attr({ - d: "M" + (startX - radius / 1.5) + " " + (startY + (radius * 2)) + " l20 -12 a70,40 0 0,0 0,24z" - }) - .classed('zoomControlPath', true); - - const top: d3.Selection = this.zoomControl - .append("g") - .on("mousedown", () => onMouseDown(() => this.rotateCam(0, 5))); - top - .append("circle").attr({ - cx: startX + gap, - cy: startY, - r: radius - }) - .classed('zoomControlCircle', true); - top - .append("path").attr({ - d: "M" + (startX + (2 * radius)) + " " + (startY - (radius / 1.5)) + " l12 20 a40,70 0 0,0 -24,0z" - }).classed('zoomControlPath', true); - - const right: d3.Selection = this.zoomControl - .append("g") - .on("mousedown", () => onMouseDown(() => this.rotateCam(-5, 0))); - right - .append("circle").attr({ - cx: startX + (2 * gap), - cy: startY + gap, - r: radius - }) - .classed('zoomControlCircle', true); - right - .append("path").attr({ - d: "M" + (startX + (4.7 * radius)) + " " + (startY + (radius * 2)) + " l-20 -12 a70,40 0 0,1 0,24z" - }).classed('zoomControlPath', true); - - const zoomIn: d3.Selection = this.zoomControl - .append("g") - .on("mousedown", () => onMouseDown(() => this.zoomClicked(-1))); - zoomIn.append("circle") - .attr({ - cx: startX + 4 * radius, - cy: startY + 6 * radius, - r: radius - }) - .classed('zoomControlCircle', true); - zoomIn.append("rect") - .attr({ - x: startX + 3.5 * radius, - y: startY + 5.9 * radius, - width: radius, - height: radius / 3, - fill: "gray" - }); - zoomIn.append("rect") - .attr({ - x: startX + (4 * radius) - radius / 6, - y: startY + 5.55 * radius, - width: radius / 3, - height: radius - }) - .classed('zoomControlPath', true); - - const zoomOut: d3.Selection = this.zoomControl - .append("g") - .on("mousedown", () => onMouseDown(() => this.zoomClicked(1))); - zoomOut - .append("circle").attr({ - cx: startX, - cy: startY + 6 * radius, - r: radius - }) - .classed('zoomControlCircle', true); - zoomOut.append("rect") - .attr({ - x: startX - (radius / 2), - y: startY + 5.9 * radius, - width: radius, - height: radius / 3 - }) - .classed('zoomControlPath', true); - - function onMouseDown(callback: () => void) { + const controlContainer: HTMLElement = document.createElement("div"); + controlContainer.classList.add("controls-container"); + controlContainer.innerHTML = GlobeMap.ZoomControlSettings.markup; + this.root.append(controlContainer); + function onMouseDown(callback: (element: SVGElement) => void) { (d3.event as MouseEvent).stopPropagation(); if ((d3.event).button === 0) { - callback(); + callback((d3.event).currentTarget); } } + this.zoomContainer = d3.select(controlContainer); + this.zoomContainer + .selectAll("g") + .on("mousedown", () => onMouseDown( + (element: SVGElement) => { + const controlType = element.classList.toString().split(" ").filter(className => className.search("js-") !== -1)[0]; + switch (controlType) { + case "js-control--move-up": this.rotateCam(0, GlobeMap.ZoomControlSettings.angleOfRotation); break; + case "js-control--move-down": this.rotateCam(0, -GlobeMap.ZoomControlSettings.angleOfRotation); break; + case "js-control--move-left": this.rotateCam(GlobeMap.ZoomControlSettings.angleOfRotation, 0); break; + case "js-control--move-right": this.rotateCam(-GlobeMap.ZoomControlSettings.angleOfRotation, 0); break; + case "js-control--zoom-up": this.zoomClicked(-GlobeMap.ZoomControlSettings.zoomStep); break; + case "js-control--zoom-down": this.zoomClicked(GlobeMap.ZoomControlSettings.zoomStep); break; + } + })); } - private initMercartorSphere() { if (GlobeMap.MercartorSphere) return; @@ -1218,79 +1296,5 @@ module powerbi.extensibility.visual { MercartorSphere.prototype = Object.create(THREE.Geometry.prototype); GlobeMap.MercartorSphere = MercartorSphere; } - - private getBingMapCanvas(resolution): JQuery { - const tileSize: number = 256; - let numSegments: number = Math.pow(2, resolution); - let numTiles: number = numSegments * numSegments; - let tilesLoaded: number = 0; - const canvasSize: number = tileSize * numSegments; - const canvas: JQuery = $('').attr({ width: canvasSize, height: canvasSize }); - - const canvasElem: HTMLCanvasElement = canvas.get(0); - const canvasContext: CanvasRenderingContext2D = canvasElem.getContext("2d"); - - function generateQuads(res, quad) { - if (res <= resolution) { - if (res === resolution) { - loadTile(quad); - } - - generateQuads(res + 1, quad + "0"); - generateQuads(res + 1, quad + "1"); - generateQuads(res + 1, quad + "2"); - generateQuads(res + 1, quad + "3"); - } - } - - function loadTile(quad) { - const template: any = "https://t{server}.tiles.virtualearth.net/tiles/r{quad}.jpeg?g=0&mkt={language}"; - const numServers: number = 7; - const server: number = Math.round(Math.random() * numServers); - const languages: string = "languages"; - const language: string = (navigator[languages] && navigator[languages].length) ? navigator[languages][0] : navigator.language; - const url: any = template.replace("{server}", server) - .replace("{quad}", quad) - .replace("{language}", language); - const coords: any = getCoords(quad); - const tile: HTMLImageElement = new Image(); - tile.onload = () => { - tilesLoaded++; - canvasContext.drawImage(tile, coords.x * tileSize, coords.y * tileSize, tileSize, tileSize); - if (tilesLoaded === numTiles) { - canvas.trigger("ready", resolution); - } - }; - - // So the canvas doesn't get tainted - tile.crossOrigin = ''; - tile.src = url; - } - - function getCoords(quad) { - let x: number = 0; - let y: number = 0; - const last: number = quad.length - 1; - - for (let i: number = last; i >= 0; i--) { - const chr: any = quad.charAt(i); - const pow: number = Math.pow(2, last - i); - - if (chr === "1") { - x += pow; - } else if (chr === "2") { - y += pow; - } else if (chr === "3") { - x += pow; - y += pow; - } - } - - return { x: x, y: y }; - } - - generateQuads(0, ""); - return canvas; - } } } diff --git a/src/settings.ts b/src/settings.ts index 371fda4..f49f073 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -25,6 +25,7 @@ */ module powerbi.extensibility.visual { + // powerbi.extensibility.utils.dataview import DataViewObjectsParser = powerbi.extensibility.utils.dataview.DataViewObjectsParser; export class GlobeMapSettings extends DataViewObjectsParser { @@ -32,6 +33,5 @@ module powerbi.extensibility.visual { } export class DataPointSettings { - public fill: string = "#005c55"; } } diff --git a/src/visualLayout.ts b/src/visualLayout.ts index efbca3e..76b87ed 100644 --- a/src/visualLayout.ts +++ b/src/visualLayout.ts @@ -25,11 +25,12 @@ */ module powerbi.extensibility.visual { - // powerbi - import IViewport = powerbi.IViewport; - - // powerbi.visuals - import IMargin = powerbi.extensibility.utils.chart.axis.IMargin; + export interface IMargin { + top: number; + bottom: number; + left: number; + right: number; + } export class VisualLayout { private marginValue: IMargin; diff --git a/style/globemap.less b/style/globemap.less index 1a3e958..7298519 100644 --- a/style/globemap.less +++ b/style/globemap.less @@ -1,26 +1,31 @@ -.globeMapView{ +.globeMapView { width: 100%; height: 100%; position: relative; } -.zoomContainer{ - position: absolute; - bottom: -5px; +.controls-container { + position: fixed; + right: 5px; + bottom: 5px; z-index: 1000; pointer-events: none; } -.zoomContainerSvg{ +.controls { pointer-events: all; + width: 145; + height: 145; } -.zoomControlCircle{ - fill: white; - stroke: gray; - opacity: 0.5; -} +.control { + circle { + fill: white; + stroke: gray; + opacity: 0.5; + } -.zoomControlPath{ - fill: gray; + path, rect { + fill: gray; + } } \ No newline at end of file diff --git a/test/_references.ts b/test/_references.ts index cb2fb41..c45108d 100644 --- a/test/_references.ts +++ b/test/_references.ts @@ -29,15 +29,12 @@ /// // Power BI API -/// +/// // Power BI Extensibility /// /// -/// /// - -/// /// /// /// diff --git a/test/visualBuilder.ts b/test/visualBuilder.ts index b4f7d8a..19ddf38 100644 --- a/test/visualBuilder.ts +++ b/test/visualBuilder.ts @@ -29,16 +29,35 @@ module powerbi.extensibility.visual.test { // powerbi.extensibility.utils.test import VisualBuilderBase = powerbi.extensibility.utils.test.VisualBuilderBase; + import renderTimeout = powerbi.extensibility.utils.test.helpers.renderTimeout; // GlobeMap1447669447624 import VisualClass = powerbi.extensibility.visual.GlobeMap1447669447624.GlobeMap; import VisualPlugin = powerbi.visuals.plugins.GlobeMap1447669447624; export class GlobeMapBuilder extends VisualBuilderBase { + private static ChangeAllType: number = 62; constructor(width: number, height: number) { super(width, height, VisualPlugin.name); } + public update(dataView: DataView[] | DataView, updateType?: VisualUpdateType): void { + this.visual.update({ + dataViews: _.isArray(dataView) ? dataView : [dataView], + viewport: this.viewport, + type: updateType + }); + } + + public updateRenderTimeout( + dataViews: DataView[] | DataView, + fn: Function, + updateType: VisualUpdateType = GlobeMapBuilder.ChangeAllType, + timeout?: number): number { + this.update(dataViews, updateType); + return renderTimeout(fn, timeout); + } + protected build(options: any) { return new VisualClass(options); } diff --git a/test/visualTest.ts b/test/visualTest.ts index 36476b0..d98d7a7 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -54,7 +54,7 @@ module powerbi.extensibility.visual.test { beforeEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - visualBuilder = new GlobeMapBuilder(1000, 500); + visualBuilder = new GlobeMapBuilder(1024, 1024); defaultDataViewBuilder = new GlobeMapDataViewBuilder(); dataView = defaultDataViewBuilder.getDataView(); diff --git a/tsconfig.json b/tsconfig.json index 7050f16..69b0765 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,15 +10,13 @@ "declaration": true }, "files": [ - ".api/v1.5.0/PowerBI-visuals.d.ts", + ".api/v1.6.0/PowerBI-visuals.d.ts", "node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-interactivityutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts", - "node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-colorutils/lib/index.d.ts", - "node_modules/bingmaps/scripts/MicrosoftMaps/Microsoft.Maps.d.ts", "src/UrlUtils/UrlUtils.ts", "src/dataInterfaces.ts", "src/settings.ts", diff --git a/tslint.json b/tslint.json index f2a424d..9ae0853 100644 --- a/tslint.json +++ b/tslint.json @@ -23,7 +23,6 @@ "spaces" ], "no-duplicate-variable": true, - "no-eval": true, "no-internal-module": false, "no-trailing-whitespace": true, "no-unsafe-finally": true,