UNPKG

@stratusjs/idx

Version:

AngularJS idx/property Service and Components bundle to be used as an add on to StratusJS

367 lines (345 loc) 15.9 kB
/** * @file IdxMap Component @stratusjs/idx/map/map.component * @example <stratus-idx-map> * @see https://github.com/Sitetheory/stratus/wiki/Idx-Map-Widget */ // Runtime import {Stratus} from '@stratusjs/runtime/stratus' import {IAttributes} from 'angular' import {cookie} from '@stratusjs/core/environment' import {isJSON, safeUniqueId} from '@stratusjs/core/misc' import {IdxComponentScope, IdxEmitter, IdxListScope, IdxService, Member, Property} from '@stratusjs/idx/idx' import {MapComponent, MarkerSettings} from '@stratusjs/map/map.component' import {numeralFormat} from '@stratusjs/angularjs-extras/filters/numeral' // Stratus Preload import '@stratusjs/angular' // sa-map // Environment const min = !cookie('env') ? '.min' : '' const packageName = 'idx' const moduleName = 'map' const componentName = 'map' // There is not a very consistent way of pathing in Stratus at the moment const localDir = `${Stratus.BaseUrl}${Stratus.DeploymentPath}@stratusjs/${packageName}/src/${moduleName}/` export type IdxMapScope = IdxComponentScope & { listId: string initialized: boolean mapInitialized: boolean listInitialized: boolean mapMarkers: MarkerSettings[] map: MapComponent list: IdxListScope instancePath: string instanceFullPath: string googleMapsKey: string mapType: string zoom: number zoomControl: boolean scrollwheel: boolean width?: string height?: string markerClickScroll: boolean markerClickHighlight: boolean markerPrice: boolean markerIcon: string markerIconHover: string markerIconLabelOriginX?: number markerIconLabelOriginY?: number getGoogleMapsKey(): string | null mapInitialize(map: MapComponent): Promise<void> mapUpdate(): Promise<void> } Stratus.Components.IdxMap = { /** @see https://github.com/Sitetheory/stratus/wiki/Idx-Package-Usage#Map */ bindings: { /** * Type: string * Id of Idx List widget to attach to and render the available collection from. The counterpart List widget's * `element-id` must be defined and the same as this `list-id` (See Property List). Multiple Map widgets may * attach to the same List widget but, this Map widget may only display a single collection at this time. */ listId: '@', /** * Type: string * Client will need to provide a Google Maps Api key with Javascript access. Without a provided key, the map * will default into 'Development Mode' and be known to the user that it's just for testing right now. */ googleMapsKey: '@', /** * Type: string * Set type of tiles displayed on the Map. * Options: 'roadmap', 'hybrid', 'satellite', 'terrain' */ mapType: '@', /** * Type: string * Set a height for the displayed map. Suggested to pixel sizes such as `500px`. * If not set, map relies on default css auto loaded. */ height: '@', /** * Type: string * Set a width for the displayed map. Suggested to sizes such as `100%`. * If not set, map relies on default css auto loaded. */ width: '@', /** * Type: boolean * Default: false * Set the map to retain 100% height of parent element (window/document by default) */ fullHeight: '@', /** * Type: string[] * Set the map to retain 100% height of parent element (window/document by default). Also removes from itself * the height of specified surrounding elements to maintain a proper. E.g.: `["#header-container",]` to have a * 100% page height map minus the height of a header and toolbar. */ fullHeightMinusElements: '@', /** * Type: string * Default: 'document' * For use with automatic sizing, what element should be referred to when process the full-height of a page. * Possible options being `document`, `window`, and any document query selector, e.g. `#body-container` */ referenceParent: '@', /** * Type: number * Default: 18 * Set the zoom level that the Map starts at. */ zoom: '@', /** * Type: boolean * Default: true * Set to display the Zoom controls on the map. */ zoomControl: '@', /** * Type: boolean * Default: false * Set if the user can adjust the Map zoom level via the mouse scrollwheel. */ scrollwheel: '@', /** * Type: boolean * Default: false * Upon clicking a marker, attempt to scroll to the listing on the page. */ markerClickScroll: '@', /** * Type: boolean * Default: false * Upon clicking a marker, attempt to highlight the lighting on the page momentarily. */ markerClickHighlight: '@', /** * Type: boolean * Default: false * Enables Property Prices to be shown on the page atop markers. */ markerPrice: '@', /** * Type: string * Set a default marker icon, providing a url path to the image. */ markerIcon: '@', /** * Type: string * Set a default marker icon on mouse hover-over, providing a url path to the image. By default there is none. * FIXME This is currently buggy if the mouse is moving too fast. */ markerIconHover: '@', /** * Type: number * When supplying a custom icon with a label, sets a position of position of the text, starting left on the x axis */ markerIconLabelOriginX: '@', /** * Type: number * When supplying a custom icon with a label, sets a position of position of the text, starting top on the y axis */ markerIconLabelOriginY: '@', /** * Type: string * Default: 'map' * The file name in which is loaded for the view of the widget. The name will automatically be appended with * '.component.min.html'. The default is 'map.component.html' / 'map'. * TODO: Will need to allow setting a custom path of views outside the library directory. */ template: '@', }, controller( $attrs: IAttributes, $scope: IdxMapScope, Idx: IdxService, ) { // Initialize $scope.uid = safeUniqueId(packageName, moduleName, componentName) Stratus.Instances[$scope.uid] = $scope $scope.elementId = $attrs.elementId || $scope.uid $scope.instancePath = $scope.elementId $scope.instanceFullPath = `Stratus.Instances.${$scope.instancePath}` this.$onInit = () => { $scope.Idx = Idx $scope.listId = $attrs.listId || null $scope.initialized = false $scope.mapInitialized = false $scope.listInitialized = false $scope.googleMapsKey = $attrs.googleMapsKey || null $scope.mapMarkers = [] $scope.mapType = $attrs.mapType || 'roadmap' $scope.zoom = $attrs.zoom || 18 $scope.zoomControl = $attrs.zoomControl || true $scope.scrollwheel = $attrs.scrollwheel || false $scope.height = $attrs.height || null // '500px' $scope.width = $attrs.width || null // '100%' $scope.markerClickScroll = $attrs.markerClickScroll && isJSON($attrs.markerClickScroll) ? JSON.parse($attrs.markerClickScroll) : false $scope.markerClickHighlight = $attrs.markerClickHighlight && isJSON($attrs.markerClickHighlight) ? JSON.parse($attrs.markerClickHighlight) : false $scope.markerPrice = $attrs.markerPrice && isJSON($attrs.markerPrice) ? JSON.parse($attrs.markerPrice) : false $scope.markerIcon = $attrs.markerIcon || ($scope.markerPrice ? `${localDir}images/map-marker-black.png` : null) $scope.markerIconLabelOriginX = $attrs.markerIconLabelOriginX || ($scope.markerPrice ? 33 : null) $scope.markerIconLabelOriginY = $attrs.markerIconLabelOriginX || ($scope.markerPrice ? 13 : null) $scope.markerIconHover = $attrs.markerIconHover || null $scope.fullHeight = $attrs.fullHeight || null $scope.fullHeightMinusElements = $attrs.fullHeightMinusElements || null $scope.referenceParent = $attrs.referenceParent || 'document' // Register this Map with the Property service Idx.registerMapInstance($scope.elementId, $scope) if ($scope.listId) { Idx.devLog($scope.elementId, 'is watching for map to update from', $scope.listId) Idx.on($scope.listId, 'init', (source: IdxListScope<Member | Property>) => { $scope.list = source $scope.listInitialized = true // $scope.initialized = true prepareMapMarkers(source) $scope.mapUpdate().then() }) Idx.on($scope.listId, 'searched', (source: IdxListScope<Member | Property>) => { // page changing and searched triggers 'searched' prepareMapMarkers(source) $scope.mapUpdate().then() }) } if ($scope.googleMapsKey) { $scope.initialized = true } else { Idx.on('Idx', 'sessionInit', () => { Idx.devLog('map received session init') $scope.initialized = true }) } Idx.emit('init', $scope) } const prepareMapMarkers = (source: IdxListScope<Member | Property>): void => { // console.log('checking $scope.collection.models', $scope.collection.models) const markers: MarkerSettings[] = [] let zIndexCounter = 100 source.getPageModels().forEach((model) => { // console.log('looping listing', listing) if ( Object.prototype.hasOwnProperty.call(model, 'Latitude') && Object.prototype.hasOwnProperty.call(model, 'Longitude') ) { const address = Idx.getStreetAddress(model as Property) // TODO handle Member? const marker: MarkerSettings = { position: {lat: model.Latitude, lng: model.Longitude}, title: address, options: { animation: 2 // DROP: 2 | BOUNCE: 1 }, // Example icon // https://mts.googleapis.com/vt/icon/name=icons/spotlight/spotlight-waypoint-a.png&text=A // &psize=16&font=fonts/Roboto-Regular.ttf&color=ff333333&ax=44&ay=48&scale=1.1 // https://mts.googleapis.com/vt/icon/name=icons/spotlight/spotlight-waypoint-a.png&color=ff333333&scale=1.1 // https://mts.googleapis.com/vt/icon/name=icons/spotlight/spotlight-waypoint-blue.png&psize=16 // &font=fonts/Roboto-Regular.ttf&color=ff333333&ax=44&ay=48&scale=1 // https://mts.googleapis.com/vt/icon/name=icons/spotlight/spotlight-waypoint-blue.png?scale=1 // https://maps.gstatic.com/mapfiles/api-3/images/spotlight-poi2.png click: { action: 'function', // function: (marker: any, markerSetting: any) => { function: () => { if ($scope.list) { if ($scope.markerClickScroll) { // Scroll to Model $scope.list.scrollToModel(model) } if ($scope.markerClickHighlight) { // Highlight the model for 6 seconds $scope.list.highlightModel(model, 6000) } } } } } // See https://developers.google.com/maps/documentation/javascript/reference/marker#MarkerLabel // for adding font details if ( $scope.markerPrice && ( Object.prototype.hasOwnProperty.call(model, 'ListPrice') || Object.prototype.hasOwnProperty.call(model, 'ClosePrice') ) && (model.ClosePrice || model.ListPrice) ) { // FIXME the format will need to change depending on the number range. // We'll want to only use '0.0a' when there is 5 charcters // marker.label = $scope.getShortCurrency(model.ClosePrice || model.ListPrice) marker.label = { color: 'white', // text: $scope.getShortCurrency(model.ClosePrice || model.ListPrice) // providing format to reduce processing text: numeralFormat(model.ClosePrice || model.ListPrice, 1, '$0[.]0a').toUpperCase() } // console.log('has price label of', marker.label) marker.collisionBehavior = 'REQUIRED_AND_HIDES_OPTIONAL' marker.zIndex = zIndexCounter-- } markers.push(marker) } }) // console.log('markers', markers) $scope.mapMarkers = markers } /*$scope.stopWatchingModel = $scope.$watch('ngModel', (data: any) => { // TODO might wanna check something else just to not import Model if (data instanceof Model && data !== $scope.model) { $scope.model = data $scope.stopWatchingModel() } })*/ $scope.mapInitialize = async (map: MapComponent) => { // console.log('idx map is running the map!!!!', map) $scope.map = map $scope.$applyAsync(() => { Idx.devLog('Map initialized', $scope.map) $scope.mapInitialized = true }) await $scope.mapUpdate() } /** * Refresh the Map with the latest markers */ $scope.mapUpdate = async () => { if ($scope.map) { $scope.map.removeMarkers() await $scope.mapMarkers.reduce((_unused: any, marker) => { $scope.map.addMarker(marker) }, undefined) // Also sleep for 0.5 seconds just to ensure everything is loaded in await new Promise(r => setTimeout(r, 500)) $scope.map.fitMarkerBounds() } } $scope.getGoogleMapsKey = (): string | null => { return $scope.googleMapsKey || Idx.getGoogleMapsKey() } $scope.on = (emitterName: string, callback: IdxEmitter) => Idx.on($scope.elementId, emitterName, callback) $scope.remove = (): void => { } }, templateUrl: ($attrs: IAttributes): string => `${localDir}${$attrs.template || componentName}.component${min}.html` }