I pretty new with Drupal and I am writing a custom map module.
My module work fine, I can dispay my map and informations but I wrote the calling of my geojson with an hard path and I didn't find in the documentation how to call with relative/dynamic path my file to make possible to deploy my module on other website.
I think I must use route but I don't understand how that could work.
So here my code :
my javascript (carto_annuaire.js), this is the first line my real problem
var url = "/drupal-site/recommended-project/web/modules/custom/carto_conseillers/src/geojson/annuaire_cf.geojson";
var centerProj = new ol.proj.transform([1.33333,47.583328], 'EPSG:4326', 'EPSG:3857');
const source = new ol.source.Vector({
url: url,
format: new ol.format.GeoJSON(),
});
const view = new ol.View({
center: centerProj,
zoom: 8
});
const vectorLayer = new ol.layer.Vector({
source: source,
style: function (feature) {
const color = feature.get('color') || '#c7c7c7';
style.getFill().setColor(color);
return style;
},
});
My module.librairies.yml
carto_annuaire:
js:
js/carto_annuaire.js: {}
dependencies:
# - core/drupal
css:
theme:
css/carto_annuaire.css: {}
openlayer:
remote: https://github.com/openlayers/openlayers/
version: 7.2.2
license:
name: BSD 2-Clause License
url: https://github.com/openlayers/openlayers/blob/main/LICENSE.md
gpl-compatible: true
js:
https://cdn.jsdelivr.net/npm/ol#v7.2.2/dist/ol.js: { type: external }
my module.routing.yml
carto_conseillers.content:
path: '/carto_conseillers'
defaults:
_controller: '\Drupal\carto_conseillers\Controller\CartoConseillersController::content'
_title: 'Une page exemple avec un contenu simple'
requirements:
_permission: 'access content'
core: 8.x
and my ModuleController.php
namespace Drupal\carto_conseillers\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Component\Utility\Html;
class CartoConseillersController {
public function content() {
return array(
'#type' => 'markup',
'#markup' => t('<div id="map" class="map"></div>
<div id="info"> </div>
<div id="popup"></div>'),
'#attached' => array(
'library' => array(
'carto_conseillers/openlayer',
'carto_conseillers/carto_annuaire',
)
),
);
}
}
Any clue is welcome.
Thanks for your time.
Regards
I find out how to resolve as it was explain there :
Attaching configurable JavaScript
In the controller just add this line :
$block['#attached']['drupalSettings']['myVar'] = 'myPath';
and in the javascript you can access to the value via
var url = drupalSettings.myVar;
Related
Rails 7 importmap leaflet-css images path fix? showed how to get leaflet working in a Stimulus controller.
import { Controller } from "#hotwired/stimulus";
import "leaflet-css";
export default class extends Controller {
static targets = [ "trial" ]
connect(){
import("leaflet").then( L => {
this.map = L.map(this.trialTarget).setView([ 51.472814, 7.321673 ], 14);
var base_map = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap | SwmmGo',
transparency: true,
opacity: 0.5
}).addTo(this.map);
Can't figure out how to do this with OpenLayers. import("leaflet").then( L => { mapping is probably the key, but I don't understand what this is doing.
Beginning of controller:
import { Controller } from "#hotwired/stimulus";
export default class extends Controller {
static targets = [ "map" ]
connect(){
import("openlayers").then( ol => {
import GeoJSON from 'ol/format/GeoJSON';
etc.
Error is: controller: street (controllers/street_controller) SyntaxError: Unexpected identifier 'GeoJSON'
# config/importmaps.rb
pin "application", preload: true
pin "#hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "#hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "#hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true # Rail7 default also
pin_all_from "app/javascript/controllers", under: "controllers"
pin "jquery", to: "https://ga.jspm.io/npm:jquery#3.6.1/dist/jquery.js", preload: true
pin "popper", to: 'popper.js', preload: true
pin "bootstrap", to: 'bootstrap.min.js', preload: true
pin "leaflet", to: "https://ga.jspm.io/npm:leaflet#1.9.2/dist/leaflet-src.js", preload: true
pin "leaflet-css", to: "https://ga.jspm.io/npm:leaflet-css#0.1.0/dist/leaflet.css.min.js", preload: true
pin "leaflet.timeline", to: "https://ga.jspm.io/npm:leaflet.timeline#1.4.3/dist/index.js", preload: true
pin "openlayers", to: "https://ga.jspm.io/npm:openlayers#4.6.5/dist/ol.js"
This is a partial answer in that I changed to esbuild since it aligned better with node_modules and I could leverage some suggestions I saw for frameworks other than Rails. I made a simple controller to sort out the problems.
// app/javascript/controllers/ol_map_test_controller.js
import { Controller } from "#hotwired/stimulus"
import ol from 'openlayers/dist/ol.js'
// import Map from 'ol/Map' // does not work. Errors in compilation
export default class extends Controller {
static targets = [ "test" ] // is for Stimulus
connect() {
this.map = new ol.Map({
// this.map = new Map({
target: 'map', // #map defined separately from Stimulus target
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
}
}
and the HTML
<div id="map" data-controller="ol-map-test" data-ol-map-test-target="test" style="height:200px"></div>
Two things had to be done. Abandon defining Map from 'ol/Map' and associated wizardry of OpenLayers, i.e., not 'new Map'. One hopes that will get sorted out.
Secondly the div id has to be explicitly defined in HTML since Stimulus doesn't add that. The two targets could both be named map but for my sanity I named them differently.
Unfortunately this doesn't solve my problem as an OpenLayers module I need has the modern wizardry and doesn't compile the js.
I may see if this works with importmaps, but wanted to post this while I remembered what I had done.
One advantage to Stimulus is that one can easily experiment with other sample controllers with only one line of HTML.
I have a parcel layer integrated into my Esri Map inside the Angular application. Now I want to filter and display only the specific parcels that meet the following criteria.
building_area equals to 0, Number_of_units equals to 0 likewise.
How can I filter the feature layer according to these conditions?
.ts
const parcelLayer = new FeatureLayer({
url: this.featureLayerUrl,
});
const esriLayers = [parcelLayer,ageLayer];
const map = new Map({
basemap: 'topo-vector',
layers: esriLayers
});
const view = new MapView({
container,
map: map,
zoom: 4,
center: [-97.63, 38.34],
});
const createEsriPopupTemplate = function(layer) {
const config = {
fields: layer.fields.map(field => (
{
name: field.name,
type: field.type,
alias: formatName(field.alias)
}
)),
title: formatName(layer.title)
};
return popupUtils.createPopupTemplate(config);
}
for (const layer of esriLayers) {
view.whenLayerView(layer).then(function (layerView) {
const popupTemplate = createEsriPopupTemplate(layer)
if (!popupTemplate) {
console.log("FeatureLayer has no fields.")
} else {
layer.popupTemplate = popupTemplate;
}
});
}
You can use FeatureLayer definitionExpression property to achieve what you are aiming for.
ArcGIS JS API - FeatureLayer definitionExpression
This property let you filter the features that will be request to the server and, as a consequence, show in the map. It is a really powerful way to filter data of layers for analysis, view and performance.
In your case this should work,
const parcelLayer = new FeatureLayer({
url: this.featureLayerUrl,
definitionExpression: "building_area=0 AND Number_of_units=0"
});
I am trying to figure out how I can access the ArCGIS JS API from a map after the map has been rendered, outside of require (ArcGIS JS API uses Dojo). For example, so I can do stuff like add (or remove) points, and perform other operations on the map.
I can create a map as follows:
require(["esri/config", "esri/Map", "esri/views/MapView", "esri/Graphic",
"esri/layers/GraphicsLayer"
], function(esriConfig, Map, MapView, Graphic, GraphicsLayer) {
esriConfig.apiKey = "";
const map = new Map({
basemap: "arcgis-topographic"
});
const view = new MapView({
map: map,
center: [-81, 41],
zoom: 9,
container: "viewDiv"
});
});
And I can add a point using this function:
function plotPoint(lat, long, props) {
const popupTemplate = {
title: "{Name}",
content: "{Description}"
}
const attributes = {
Name: props.name,
Description: props.desc
}
const graphicsLayer = new GraphicsLayer();
map.add(graphicsLayer);
const point = {
type: "point",
longitude: long,
latitude: lat
};
const simpleMarkerSymbol = {
type: "simple-marker",
color: [226, 119, 40],
outline: {
color: [255, 255, 255],
width: 1
}
};
const pointGraphic = new Graphic({
geometry: point,
symbol: simpleMarkerSymbol,
attributes: attributes,
popupTemplate: popupTemplate
});
graphicsLayer.add(pointGraphic);
}
But plotPoint needs to be within the require callback so it can access the referenced modules (like GraphicsLayer). I could assign it to the global window object so I could call it outside of require, but then I may run into an issue where the function is called before it's defined.
I may need to perform other operations too from other points in the code, like removing points, adding feature layers, etc. Unfortunately, this must all exist inside some legacy code, so I can't refactor the entire application.
Is there a better pattern for accessing the API outside of require?
I think that the easy way to achieve what you want, if I understand you correctly, is just to define modules and include it in you application.
A simple example base on you code would be something like this,
GraphicsManager.js
define([
"dojo/_base/declare",
"esri/Graphic",
"esri/layers/GraphicsLayer"
], function(declare, Graphic, GraphicsLayer){
return declare(null, {
plotPoint: function(lat, long, props){
// .. here the logic
return graphicsLayer;
}
});
});
main.js
require(["esri/config", "esri/Map", "esri/views/MapView", "app/GraphicsManager"
], function(esriConfig, Map, MapView, GraphicsManager) {
esriConfig.apiKey = "";
const map = new Map({
basemap: "arcgis-topographic",
});
const view = new MapView({
map: map,
center: [-81, 41],
zoom: 9,
container: "viewDiv"
});
// ... some logic to get the point data
const gm = new GraphicsManager();
map.add(gm.plotPoint(lat, long, props));
// .. some other logic
});
There you see that the main.js is where the application start, things are set there or in others modules. You know, map, layers, widgets, etc.
Then you have your other code in modules, and you use import them as required.
dojotoolkit - intro to modules
I am currently working on adding functionality to convert an OpenLayers Map into a png file (The example is here). However, when calling domtoimage.toPng() in the below code, Firefox (Ubuntu version 68.0.2) gives me the error SecurityError: This operation is insecure. I have checked all around and no one else seems to be having this problem with the dom-to-image library, and so I am stuck on how to fix this error. My JavaScript code for the Map is very similar to the code given in the example and is given here:
<script type="text/javascript">
var extent = [0, 0, 3000, 4213];
var projection = new ol.proj.Projection({
code: 'my-image',
units: 'pixels',
extent: extent,
});
var map = new ol.Map({
controls: ol.control.defaults().extend([
new ol.control.FullScreen()
]),
layers: [
new ol.layer.Image({
source: new ol.source.ImageStatic({
attributions: 'My Image Attributions',
url: "{{record | img_url}}", // Django stuff defined earlier
projection: projection,
imageExtent: extent
})
})
],
target: 'map',
view: new ol.View({
projection: projection,
center: ol.extent.getCenter(extent),
zoom: 2,
maxZoom: 8
})
});
map.addOverlay(new ol.Overlay({
position: [0, 0],
element: document.getElementById('null')
}));
// export options for dom-to-image.
var exportOptions = {
filter: function(element) {
return element.className ? element.className.indexOf('ol-control') === -1 : true;
}
};
document.getElementById('export-png').addEventListener('click', function() {
map.once('rendercomplete', function() {
domtoimage.toPng(map.getTargetElement(), exportOptions)
.then(function(dataURL) {
var link = document.getElementById('image-download');
link.href = dataURL;
link.click();
});
});
map.renderSync();
});
The HTML is effectively the same as in the example and so I believe the problem lies somewhere in here. Perhaps it is something with using a StaticImage in the Map? Or maybe going through the Django framework tampers with it in some unknown way? I am not entirely sure, and any diagnosis/help with fixing this issue would be much appreciated.
I think there should be something like:
new ol.layer.Tile({
name: 'name',
source: new ol.source.TileWMS({
...
crossOrigin: 'anonymous' // <-- Add this to the json.
})
})
Read more:
https://openlayers.org/en/v4.6.5/apidoc/ol.source.ImageWMS.html
https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
UPDATED:
I am trying to translate the leaflet routing component with language:'sp'
but it does not work for me.
const createRoutingControl = () => {
L.Routing.control({
router: L.Routing.mapbox(config.features.routing.key),
plan: new (L.Routing.Plan.extend({
createGeocoders: function () {
let container = L.Routing.Plan.prototype.createGeocoders.call(this)
let reverseRoute = createButton('↑↓', container)
let copyToClipboard = createButton('5', container, true)
return container
}
}))([], {
geocoder: geocoder,
language: 'sp'
}),
units: config.features.routing.units,
showAlternatives: true,
autoRoute: true,
routeWhileDragging: true,
}).addTo(map)
}
With " language:'sp' " the form is traslated but not the instruccions.
I know I have to use formatter but I tried to put it in routing.control, routing.plan... (and more places only to test it) and it does not work (the map is not displayed)
The response from #IvanSanchez is almost correct: Control does indeed not have a language option, but several other classes have (not sure this is even properly documented, sorry).
Having said that, as a default, the Control class passes on any option you give it when it instantiates its child components, so passing language as an option to Control will also pass it to the default formatter, the router, etc. The exception is when you provide child components that you instantiate yourself, like the Plan in your example: for that case, you need to explicitly provide the options (like you already do).
So I would assume this code should fix the issue:
const createRoutingControl = () => {
L.Routing.control({
router: L.Routing.mapbox(config.features.routing.key),
plan: new (L.Routing.Plan.extend({
createGeocoders: function () {
let container = L.Routing.Plan.prototype.createGeocoders.call(this)
let reverseRoute = createButton('↑↓', container)
let copyToClipboard = createButton('5', container, true)
return container
}
}))([], {
geocoder: geocoder,
language: 'sp'
}),
units: config.features.routing.units,
showAlternatives: true,
autoRoute: true,
routeWhileDragging: true,
language: 'sp'
}).addTo(map)
I'm sorry to dig up this topic, but I can not translate the instructions in french...
I set the language option 'fr' directly in L.Routing.control and also in the formatter...
I try to debug the lines where the localization is done, and I see my problem at line 16353 of leaflet-routing-machine.js :
formatInstruction: function(instr, i) {
if (instr.text === undefined) {
return this.capitalize(L.Util.template(this._getInstructionTemplate(instr, i),
.....
})));
} else {
return instr.text;
}
where instr.text is already initialized...
If I reset instr.text to "undefined" at debug time, the instruction is well translated...
Do you have any idea of my problem?
My code is below:
$script(['https://unpkg.com/leaflet/dist/leaflet-src.js'], () =>{
$script(['https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.js'], () => {
this.map = L.map(element);
L.tileLayer('//{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap - rendu OSM France ',
maxZoom: 18
}).addTo(this.map);
this.map = L.map(element);
L.tileLayer('//{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap - rendu OSM France ',
maxZoom: 18
}).addTo(this.map);
let control1 = L.Routing.control(
{
router: L.routing.mapbox(this.key.MapboxKey),
language: 'fr',
formatter: new L.Routing.Formatter({
language: 'fr'
}),
waypoints: [
L.latLng(47.928927, 7.538860),
L.latLng(48.044444, 7.299279),
L.latLng(47.857503, 6.821690),
L.latLng(47.506390, 6.996181),
L.latLng(47.586881, 7.25652)
],
routeWhileDragging: true
})
.on('routingerror', function(e) {
try {
this.map.getCenter();
} catch (e) {
this.map.fitBounds(L.latLngBounds(control1.getWaypoints().map(function(wp) { return wp.latLng; })));
}
handleError(e);
})
.addTo(this.map);
L.Routing.errorControl(control1).addTo(this.map);
====================================================================
As I found the solution in the meantime, I give it away because it seems not to be documented :
I have to add the language option as parameter of the mapbox function, when creating the "router" :
L.routing.mapbox(this.key.MapboxKey, {language: 'fr'}),
This is a case of RTFM.
If you look a bit closer at the API documentation for Leaflet Routing Machine, you'll notice that L.Routing.Control does not have a language option, and that the language option only applies to instances of L.Routing.Formatter, and that a L.Routing.Control can take a formatter option which can hold an instance of L.Routing.Formatter. So putting everything together...
L.Routing.control({
router: L.Routing.mapbox(config.features.routing.key),
formatter: L.Routing.formatter({
language: 'sp'
units: config.features.routing.units,
}),
showAlternatives: true,
autoRoute: true,
// ... etc
},
(P.S.: "prop" is reactjs slang, and that word does not apply here)