1/** 2 settings.geojsonObject 3 settings.layerStyle 4 settings.target 5*/ 6 7 8// Feel free to add/edit styles here. 9// They are used as "constants" 10 11const iconFont = 'Font Awesome 5 Pro'; 12const iconFontText = "\uf3c5"; 13const iconFontWeight = 900; 14const iconSize = 40; 15const iconColor = 'rgba(236, 87, 38, 1)'; 16const iconColorSelect = 'rgba(54, 94, 123, 1)'; 17const iconColorPros = 'rgba(0, 0, 0, 0.4)'; 18const strokeColor = new ol.style.Stroke({ color: 'rgba(255, 255, 255,0.9)'}); 19const strokeColorPros = new ol.style.Stroke({ color: 'rgba(0, 0, 0, 0.6)'}); 20const clusterColorStroke = 'rgba(255, 255, 255,0.9)'; 21const pathStrokeColor = 'rgba(73, 111, 140, 0.9)'; 22 23// Set fonticon icon 24function locationText(color,stroke) { 25 return new ol.style.Text({ 26 text: iconFontText, 27 font: '900 40px "Font Awesome 5 Pro"', 28 textBaseline: 'bottom', 29 fill: new ol.style.Fill({ color: color }), 30 stroke: stroke, 31 }) 32} 33 34function selectText(color,stroke) { 35 return new ol.style.Text({ 36 text: iconFontText, 37 font: '900 40px "Font Awesome 5 Pro"', 38 textBaseline: 'bottom', 39 fill: new ol.style.Fill({ color: color }), 40 stroke: stroke, 41 }) 42} 43 44// Set cluster style 45function clusterStyle(size, iconColor, strokeColor) { 46 return new ol.style.Style({ 47 image: new ol.style.Circle({ 48 radius: 18, 49 stroke: new ol.style.Stroke({ 50 color: strokeColor 51 }), 52 fill: new ol.style.Fill({ 53 color: iconColor, 54 }) 55 }), 56 text: new ol.style.Text({ 57 text: size.toString(), 58 font:'bold 18px serif', 59 fill: new ol.style.Fill({ 60 color: strokeColor 61 }) 62 }) 63 }) 64} 65 66var MSTMapStyles = { 67 68 ITINERARY: new ol.style.Style({ 69 stroke: new ol.style.Stroke({ 70 color: pathStrokeColor, 71 width: 4 72 }), 73 }), 74 LOCATIONS: new ol.style.Style({ 75 text: locationText(iconColor, strokeColor) 76 }), 77 LOCATIONS_CLUSTER: function (feature) { 78 if (!feature.get('features') || feature.get('features').length === 1) { 79 return new ol.style.Style({ 80 text: locationText(iconColor, strokeColor) 81 }); 82 } 83 let size = feature.get('features').length; 84 return clusterStyle(size, iconColor, clusterColorStroke); 85 }, 86 PROS: new ol.style.Style({ 87 text: locationText(iconColorPros, strokeColorPros) 88 }), 89 PROS_CLUSTER: function (feature) { 90 if (!feature.get('features') || feature.get('features').length === 1) { 91 return new ol.style.Style({ 92 text: locationText(iconColorPros, strokeColorPros) 93 }); 94 } 95 let size = feature.get('features').length; 96 return clusterStyle(size, iconColorPros, clusterColorStroke); 97 }, 98 DS: new ol.style.Style({ 99 text: locationText(iconColor, strokeColor) 100 }), 101 DS_CLUSTER: function (feature) { 102 // if (!feature.get('features') || feature.get('features').length === 1) { 103 return new ol.style.Style({ 104 text: locationText(iconColor, strokeColor), 105 }); 106 // } 107 // let size = feature.get('features').length; 108 // return clusterStyle(size, iconColor, clusterColorStroke); 109 }, 110}; 111 112class MSTMap { 113 constructor (identifier, settings = []) { 114 let self = this; 115 // Defaults 116 let options = { 117 onClickEmpty: function () {}, 118 }; 119 $.extend(options, settings) 120 121 /**** 122 * PROPERTIES 123 */ 124 this.map = undefined; 125 this.attribution = undefined; 126 this.layers = {}; 127 this.onPointerMove = {}; 128 this.onPointerClick = {}; 129 this.popupMouseOver = undefined; 130 this.popupClick = undefined; 131 this.clickedFeature = undefined; 132 this.hoveredFeature = undefined; 133 this.view = undefined; 134 this.menu = undefined; 135 this.select = undefined; 136 this.featuresList = {}; 137 this.options = options; 138 139 // Types of geometry that are interactable (hover, click) 140 this.INTERACTABLE_TYPES = ['Point']; 141 // Contributors that will be displayed (html format) 142 this.CONTRIBUTORS = '© <a href="/">Marseille Tourisme</a> & \ 143 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors.'; 144 // Minimum size of the map for contributors to appear expanded in pixels. (they will appear collapsed if size is inferior) 145 this.MIN_SIZE_EXPANDED_CONTRIBUTORS = 9999; 146 // Projection used by the DB is 4326 (World Geodetic System 84) 147 this.DATA_PROJECTION = 'EPSG:4326'; 148 // Projection used by the map view is 3857 (Web Mercator projection) 149 this.MAPVIEW_PROJECTION = 'EPSG:3857'; 150 151 152 this.attribution = new ol.control.Attribution({ 153 collapsible: true, 154 }); 155 156 let layers = Array(new ol.layer.Tile({ 157 source: new ol.source.XYZ({ 158 url: MAP_TILE_XYZ_URL, 159 attributions: [this.CONTRIBUTORS], 160 }), 161 preload: 10 162 })); 163 164 this.view = new ol.View({ 165 projection: this.MAPVIEW_PROJECTION, 166 center: [MAP_INITIAL_LON, MAP_INITIAL_LAT], 167 zoom: MAP_MIN_ZOOM, 168 minZoom: MAP_MIN_ZOOM, 169 maxZoom: MAP_MAX_ZOOM 170 }); 171 172 this.map = new ol.Map({ 173 layers: layers, 174 target: identifier, 175 interactions: ol.interaction.defaults({ 176 //mouseWheelZoom: true 177 constrainResolution: true, 178 onFocusOnly: true 179 }), 180 view: this.view, 181 controls: [ 182 new ol.control.ScaleLine({ 183 units: 'metric' 184 }), 185 new ol.control.Zoom({ 186 'zoomInTipLabel': "{% trans 'Zoom in' %}", 187 'zoomOutTipLabel': "{% trans 'Zoom out' %}" 188 }), 189 ], 190 }); 191 this.map.addControl(this.attribution); 192 193 window.addEventListener('resize', function () { 194 self.checkSize() 195 }); 196 197 // ADD AN OVERLAY TO DISPLAY FEATURES 198 this.menu = new ol.control.Overlay({ 199 closeBox : true, 200 className: "slide-left menu", 201 content: $("#menu").get(0), 202 }); 203 this.map.addControl(this.menu); 204 205 // ADD A SELECT STATE 206 this.select = new ol.interaction.Select({ 207 style: new ol.style.Style({ 208 text: locationText(iconColorSelect,strokeColor) 209 }), 210 }); 211 this.map.addInteraction(this.select); 212 213 this.popupMouseOver = new Popup(); 214 this.map.addOverlay(this.popupMouseOver); 215 this.map.on('pointermove', function(evt) { 216 // If that's a drag event 217 if (evt.dragging) { self.popupMouseOver.hide(); return; } 218 // Find the feature 219 var feature = self.getInteractableFeatureAtPixel(evt.pixel); 220 // Last feature hovered was the same. Return 221 if (feature && self.hoveredFeature && self.hoveredFeature.ol_uid == feature.ol_uid) return; 222 self.hoveredFeature = feature; 223 if (feature !== null) { 224 let subFeatures = feature.get('features'); 225 // Fix : in some cases (TBD), only one feature is returned by getInterectableFeatureAtPixel 226 if (subFeatures === undefined) { 227 subFeatures = [feature] 228 } 229 // Array of features with the same layer_id 230 let finalFeatures = []
231 subFeatures.forEach(function (ft) { 232 if (finalFeatures.length === 0) { 233 // First item 234 finalFeatures.push(ft) 235 } else { 236 if (ft.layer_id === finalFeatures[0].layer_id) { 237 finalFeatures.push(ft) 238 } 239 } 240 }); 241 242 // Call the corresponding pointerMove callbacks 243 if (finalFeatures.length > 0) { 244 let onPointerMove = self.onPointerMove[finalFeatures[0].layer_id] 245 if (!onPointerMove.interactWithCluster && subFeatures.length > 1) { 246 // Disable popup & interaction for clusters if not needed 247 self.hidePopup(self.popupMouseOver); return; 248 } 249 self.setCursorStyle('pointer'); 250 let featureIsClicked = (self.getClickedFeature() && feature.ol_uid === self.getClickedFeature().ol_uid) 251 if (onPointerMove.interactWithCluster) { 252 onPointerMove.callback(self, evt, finalFeatures, featureIsClicked); 253 } else { 254 onPointerMove.callback(self, evt, finalFeatures[0], featureIsClicked); 255 } 256 } 257 258 } else { 259 // Default cursor + hide popup 260 self.hidePopup(self.popupMouseOver); 261 } 262 }); 263 264 this.popupClick = new Popup(); 265 this.map.addOverlay(this.popupClick); 266 this.map.on('click', function(evt) { 267 // If that's a drag event 268 if (evt.dragging) { self.popupClick.hide(); return; } 269 self.hidePopups() 270 // Find the feature 271 var feature = self.getInteractableFeatureAtPixel(evt.pixel); 272 if (feature !== null) { 273 let subFeatures = feature.get('features'); 274 // Fix : in some cases (TBD), only one feature is returned by getInterectableFeatureAtPixel 275 if (subFeatures === undefined) { 276 subFeatures = [feature] 277 } 278 // Array of features with the same layer_id 279 let finalFeatures = [] 280 subFeatures.forEach(function (ft) { 281 if (finalFeatures.length === 0) { 282 // First item 283 finalFeatures.push(ft) 284 } else { 285 if (ft.layer_id === finalFeatures[0].layer_id) { 286 finalFeatures.push(ft) 287 } 288 } 289 }); 290 291 // Call the corresponding pointerClick callbacks 292 if (finalFeatures.length > 0) { 293 let onPointerClick = self.onPointerClick[finalFeatures[0].layer_id] 294 if (!onPointerClick.interactWithCluster && subFeatures.length > 1) { 295 // Disable popup & interaction for clusters if not needed 296 self.hidePopup(self.popupMouseOver); return; 297 } 298 self.setCursorStyle('pointer'); 299 if (onPointerClick.interactWithCluster) { 300 onPointerClick.callback(self, evt, finalFeatures); 301 } else { 302 onPointerClick.callback(self, evt, finalFeatures[0]); 303 } 304 } 305 } else { 306 // Default cursor + hide popup 307 self.hidePopup(self.popupClick); 308 self.options.onClickEmpty(self); 309 } 310 self.clickedFeature = feature; 311 }); 312 313 } 314 315 getClickedFeature() { 316 return this.clickedFeature; 317 } 318 319 showMenu(content){ 320 this.menu.show(); 321 322 var contentOld = $("<div>") 323 .append(content); 324 $(".data").html(contentOld); 325 }; 326 327 setCenter(source) { 328 this.map.getView().fit(source.getExtent(), { 329 size: this.map.getSize(), 330 maxZoom: 14 331 }); 332 } 333 334 checkSize() { 335 // under MIN_SIZE_EXPANDED_CONTRIBUTORS px width, the contributions will be collapsed 336 var small = this.map.getSize()[0] < this.MIN_SIZE_EXPANDED_CONTRIBUTORS; 337 this.attribution.setCollapsible(small); 338 this.attribution.setCollapsed(small); 339 } 340 341 // Settings : 342 // features, style, isCentered (default: false), onPointerMove: defaulted, onPointerClick: defaulted 343 addLayer(settings) { 344 345 // Is used to calculate the layer_id if ID is not given 346 function randomString(length) { 347 var result = ''; 348 var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 349 var charactersLength = characters.length; 350 for ( var i = 0; i < length; i++ ) { 351 result += characters.charAt(Math.floor(Math.random() * charactersLength)); 352 } 353 return result; 354 } 355 356 // Defaults 357 let options = { 358 isCentered: false, 359 defaultCenter: null, // Center if features is empty and isCentered is true 360 interactWithCluster: false, 361 zIndex:0, 362 featuresList:'', 363 onPointerMove: function () {}, 364 onPointerClick: function () {}, 365 }; 366 $.extend(options, settings) 367 368 if (!options.features) { 369 throw "addLayer parameter should have an `features` attribute." 370 } 371 if (!options.style) { 372 throw "addLayer parameter should have an `style` attribute." 373 } 374 if (options.id && this.layers[options.id]) { 375 throw "This `id` is already taken. You may use the function updateLayer (still unemplemented) to update the layer with a given id." 376 } 377 378 // Find out an unique identifier for the layer (layer_id) 379 let identifier = options.id 380 while (!identifier) { 381 identifier = randomString(10) 382 if (this.layers[identifier]) identifier = undefined; 383 } 384 385 // All features of this addLayer() call will end up in finalFeatures 386 let finalFeatures = []; 387 let self = this; 388 389 // Since we can feed features with an array of features, act like it's always an array.
390 if (!Array.isArray(options.features)) options.features = [options.features]; 391 options.features.forEach(function (geojsonFeature) { 392 let jsonFeatures = new ol.format.GeoJSON({ 393 dataProjection: self.DATA_PROJECTION, 394 featureProjection: self.MAPVIEW_PROJECTION 395 }).readFeatures(geojsonFeature); 396 // Push everything in finalFeatures & tag its layer_id in order to find it afterwards 397 for (var i=0; i < jsonFeatures.length; i++) { 398 var feature = jsonFeatures[i]; 399 if (feature.getGeometry()) { 400 feature.layer_id = identifier; 401 finalFeatures.push(feature); 402 } 403 } 404 }); 405 let source = new ol.source.Vector({ 406 features: finalFeatures 407 }); 408 409 // if options.style is a function, this means it is a clustered style. 410 // To apply a clustered style, we have to create a ol.source.cluster 411 if (options.style instanceof Function) { 412 var clusterSource = new ol.source.Cluster({ 413 distance: parseInt(MAP_CLUSTER_DISTANCE, 10), 414 source: source, 415 }); 416 var vector = new ol.layer.Vector({ 417 source: clusterSource, 418 style: options.style, 419 zIndex: options.zIndex, 420 }); 421 } else { 422 var vector = new ol.layer.Vector({ 423 source: source, 424 style: options.style, 425 zIndex: options.zIndex, 426 }); 427 } 428 429 // Actually add the layer to the OpenLayers map 430 this.map.addLayer(vector) 431 this.layers[identifier] = finalFeatures; 432 433 // Center on the layer if asked to do so 434 if (options.isCentered) { 435 if (finalFeatures.length > 0) { 436 this.setCenter(source) 437 } else { 438 let centerFeature = new ol.format.GeoJSON({ 439 dataProjection: self.DATA_PROJECTION, 440 featureProjection: self.MAPVIEW_PROJECTION 441 }).readFeatures(options.defaultCenter); 442 this.setCenter(new ol.source.Vector({ 443 features: centerFeature 444 })); 445 } 446 } 447 448 // Register callbacks functions 449 this.onPointerMove[identifier] = { 450 'interactWithCluster': options.interactWithCluster, 451 'callback': options.onPointerMove, 452 } 453 this.onPointerClick[identifier] = { 454 'interactWithCluster': options.interactWithCluster, 455 'callback': options.onPointerClick, 456 } 457 458 // Preload popup content. 459 // This is useful to preload images and async assets 460 // If we dont preload, the image will load asynchronously and will change the size of the popup after a short time period. 461 // The popup will look OK but the map wont be able to adapt its position to show the whole popup, since the calculation is 462 // done at the popup.show() and not afterwards. 463 // finalFeatures.forEach(function (feature) { 464 // var img = new Image() 465 // img.src = feature.get('image') 466 // }); 467 468 return { 469 identifier: identifier, 470 features: finalFeatures, 471 } 472 } 473 474 // Hide all popups 475 hidePopups() { 476 if (this.popupMouseOver !== undefined) { 477 this.popupMouseOver.hide(); 478 } 479 if (this.popupClick !== undefined) { 480 this.popupClick.hide(); 481 } 482 } 483 484 /** 485 * Default cursor + hide popup 486 */ 487 hidePopup(popup) { 488 this.setCursorStyle(''); 489 490 if (popup !== undefined){ 491 popup.hide(); 492 } 493 } 494 495 getFeature(layer_id, ol_uid) { 496 // Get the feature using ol_uid 497 let features = this.layers[layer_id]; 498 let feature = features.filter(x => x.ol_uid == ol_uid)[0] 499 return feature 500 } 501 502 centerOnFeature(feature) { 503 504 // Center on it 505 let geometry = feature.getGeometry(); 506 let coord = geometry.getCoordinates(); 507 508 // Move map in order to offset menu width 509 coord[0] += -1000 510 511 this.view.setCenter(coord); 512 } 513 514 selectFeature(feature) { 515 516 // Empty selected features 517 while (this.select.getFeatures().values_.length > 0) { 518 this.select.getFeatures().pop(); 519 } 520 this.select.getFeatures().push(feature); 521 522 // Add selected class 523 $("div.card").removeClass("selected"); 524 $("div#ol-id-" + feature.ol_uid ).addClass("selected"); 525 } 526 527 showPopup(popup, feature, content, positionModifier) { 528 let geometry = feature.getGeometry();
529 let coord = geometry.getCoordinates(); 530 let pixel = this.map.getPixelFromCoordinate(coord); 531 pixel[0] += positionModifier[0]; 532 pixel[1] += positionModifier[1]; 533 popup.show(this.map.getCoordinateFromPixel(pixel), content); 534 } 535 536 // Return the feature found at this position, null if none. 537 // To find if a feature is interactable, it tries to match its type against the INTERECTABLE_TYPES constant. 538 getInteractableFeatureAtPixel(pixel) { 539 let features = this.map.getFeaturesAtPixel(pixel); 540 if (features !== null) { 541 for (let i = 0; i < features.length; i++) { 542 let feature = features[i]; 543 if (this.INTERACTABLE_TYPES.indexOf(feature.getGeometry().getType()) !== -1) { 544 return feature; 545 } 546 } 547 } 548 return null; 549 } 550 551 // Changes the cursor style (pointer for example) 552 setCursorStyle(cursorStyle) { 553 $("#" + this.map.getTarget())[0].style.cursor = cursorStyle; 554 } 555 556 centerOnLocation() { 557 let self = this; 558 559 // set up the geolocation api to track our position 560 var geolocation = new ol.Geolocation({ 561 tracking: true, 562 projection: this.map.getView().getProjection(), 563 }); 564 // bind the view's projection 565 //geolocation.bindTo('projection', this.view); 566 var stopInterval = function() {}; 567 var centerOnPosition = function () { 568 var coordinate = geolocation.getPosition(); 569 if (coordinate !== undefined) { 570 self.view.setCenter(coordinate); 571 self.view.setZoom(13); 572 stopInterval(); 573 } 574 } 575 var interval = setInterval(centerOnPosition, 500); 576 var stopInterval = function() { 577 clearInterval(interval); 578 }; 579 580 } 581}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.