395 lines
13 KiB
JavaScript
395 lines
13 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
/*global L*/
|
|
|
|
import { renderToString } from "@web/core/utils/render";
|
|
|
|
const { Component, onWillUnmount, onWillUpdateProps, useEffect, useRef, useState } = owl;
|
|
|
|
const apiTilesRouteWithToken =
|
|
"https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}";
|
|
const apiTilesRouteWithoutToken = "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
|
|
const colors = [
|
|
"#F06050",
|
|
"#6CC1ED",
|
|
"#F7CD1F",
|
|
"#814968",
|
|
"#30C381",
|
|
"#D6145F",
|
|
"#475577",
|
|
"#F4A460",
|
|
"#EB7E7F",
|
|
"#2C8397",
|
|
];
|
|
|
|
const mapTileAttribution = `
|
|
© <a href="https://www.mapbox.com/about/maps/">Mapbox</a>
|
|
© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>
|
|
<strong>
|
|
<a href="https://www.mapbox.com/map-feedback/" target="_blank">
|
|
Improve this map
|
|
</a>
|
|
</strong>`;
|
|
|
|
export class MapRenderer extends Component {
|
|
setup() {
|
|
this.leafletMap = null;
|
|
this.markers = [];
|
|
this.polylines = [];
|
|
this.mapContainerRef = useRef("mapContainer");
|
|
this.state = useState({
|
|
closedGroupIds: [],
|
|
});
|
|
this.nextId = 1;
|
|
|
|
useEffect(
|
|
() => {
|
|
this.leafletMap = L.map(this.mapContainerRef.el, {
|
|
maxBounds: [L.latLng(180, -180), L.latLng(-180, 180)],
|
|
});
|
|
L.tileLayer(this.apiTilesRoute, {
|
|
attribution: mapTileAttribution,
|
|
tileSize: 512,
|
|
zoomOffset: -1,
|
|
minZoom: 2,
|
|
maxZoom: 19,
|
|
id: "mapbox/streets-v11",
|
|
accessToken: this.props.model.metaData.mapBoxToken,
|
|
}).addTo(this.leafletMap);
|
|
},
|
|
() => []
|
|
);
|
|
useEffect(() => {
|
|
this.updateMap();
|
|
});
|
|
|
|
onWillUpdateProps(this.onWillUpdateProps);
|
|
onWillUnmount(this.onWillUnmount);
|
|
}
|
|
/**
|
|
* Update group opened/closed state.
|
|
*/
|
|
async onWillUpdateProps(nextProps) {
|
|
if (this.props.model.data.groupByKey !== nextProps.model.data.groupByKey) {
|
|
this.state.closedGroupIds = [];
|
|
}
|
|
}
|
|
/**
|
|
* Remove map and the listeners on its markers and routes.
|
|
*/
|
|
onWillUnmount() {
|
|
this.removeMarkers();
|
|
this.removeRoutes();
|
|
if (this.leafletMap) {
|
|
this.leafletMap.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the route to the tiles api with or without access token.
|
|
*
|
|
* @returns {string}
|
|
*/
|
|
get apiTilesRoute() {
|
|
return this.props.model.data.useMapBoxAPI
|
|
? apiTilesRouteWithToken
|
|
: apiTilesRouteWithoutToken;
|
|
}
|
|
|
|
/**
|
|
* If there's located records, adds the corresponding marker on the map.
|
|
* Binds events to the created markers.
|
|
*/
|
|
addMarkers() {
|
|
this.removeMarkers();
|
|
|
|
const markersInfo = {};
|
|
let records = this.props.model.data.records;
|
|
if (this.props.model.data.isGrouped) {
|
|
records = Object.entries(this.props.model.data.recordGroups)
|
|
.filter(([key]) => !this.state.closedGroupIds.includes(key))
|
|
.flatMap(([groupId, value]) => value.records.map((elem) => ({ ...elem, groupId })));
|
|
}
|
|
|
|
const pinInSamePlace = {};
|
|
for (const record of records) {
|
|
const partner = record.partner;
|
|
if (partner && partner.partner_latitude && partner.partner_longitude) {
|
|
const lat_long = `${partner.partner_latitude}-${partner.partner_longitude}`;
|
|
const group = this.props.model.data.recordGroups ? `-${record.groupId}` : "";
|
|
const key = `${lat_long}${group}`;
|
|
if (key in markersInfo) {
|
|
markersInfo[key].record = record;
|
|
markersInfo[key].ids.push(record.id);
|
|
} else {
|
|
pinInSamePlace[lat_long] = ++pinInSamePlace[lat_long] || 0;
|
|
markersInfo[key] = {
|
|
record: record,
|
|
ids: [record.id],
|
|
pinInSamePlace: pinInSamePlace[lat_long],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const markerInfo of Object.values(markersInfo)) {
|
|
const params = {
|
|
count: markerInfo.ids.length,
|
|
isMulti: markerInfo.ids.length > 1,
|
|
number: this.props.model.data.records.indexOf(markerInfo.record) + 1,
|
|
numbering: this.props.model.metaData.numbering,
|
|
};
|
|
|
|
if (this.props.model.data.isGrouped) {
|
|
const groupId = markerInfo.record.groupId;
|
|
params.color = this.getGroupColor(groupId);
|
|
params.number = this.props.model.data.recordGroups[groupId].records.findIndex(
|
|
(record) => {
|
|
return record.id === markerInfo.record.id;
|
|
}
|
|
);
|
|
}
|
|
|
|
// Icon creation
|
|
const iconInfo = {
|
|
className: "o-map-renderer--marker",
|
|
html: renderToString("web_map.marker", params),
|
|
};
|
|
|
|
const offset = markerInfo.pinInSamePlace * 0.000025;
|
|
// Attach marker with icon and popup
|
|
const marker = L.marker(
|
|
[
|
|
markerInfo.record.partner.partner_latitude + offset,
|
|
markerInfo.record.partner.partner_longitude - offset,
|
|
],
|
|
{ icon: L.divIcon(iconInfo) }
|
|
);
|
|
marker.addTo(this.leafletMap);
|
|
marker.on("click", () => {
|
|
this.createMarkerPopup(markerInfo, offset);
|
|
});
|
|
this.markers.push(marker);
|
|
}
|
|
}
|
|
/**
|
|
* If there are computed routes, create polylines and add them to the map.
|
|
* each element of this.props.routeInfo[0].legs array represent the route between
|
|
* two waypoints thus each of these must be a polyline.
|
|
*/
|
|
addRoutes() {
|
|
this.removeRoutes();
|
|
if (!this.props.model.data.useMapBoxAPI || !this.props.model.data.routes.length) {
|
|
return;
|
|
}
|
|
|
|
for (const leg of this.props.model.data.routes[0].legs) {
|
|
const latLngs = [];
|
|
for (const step of leg.steps) {
|
|
for (const coordinate of step.geometry.coordinates) {
|
|
latLngs.push(L.latLng(coordinate[1], coordinate[0]));
|
|
}
|
|
}
|
|
|
|
const polyline = L.polyline(latLngs, {
|
|
color: "blue",
|
|
weight: 5,
|
|
opacity: 0.3,
|
|
}).addTo(this.leafletMap);
|
|
|
|
const polylines = this.polylines;
|
|
polyline.on("click", function () {
|
|
for (const polyline of polylines) {
|
|
polyline.setStyle({ color: "blue", opacity: 0.3 });
|
|
}
|
|
this.setStyle({ color: "darkblue", opacity: 1.0 });
|
|
});
|
|
this.polylines.push(polyline);
|
|
}
|
|
}
|
|
/**
|
|
* Create a popup for the specified marker.
|
|
*
|
|
* @param {Object} markerInfo
|
|
* @param {Number} latLongOffset
|
|
*/
|
|
createMarkerPopup(markerInfo, latLongOffset = 0) {
|
|
const popupFields = this.getMarkerPopupFields(markerInfo);
|
|
const partner = markerInfo.record.partner;
|
|
const popupHtml = renderToString("web_map.markerPopup", {
|
|
fields: popupFields,
|
|
hasFormView: this.props.model.metaData.hasFormView,
|
|
url: `https://www.google.com/maps/dir/?api=1&destination=${partner.partner_latitude},${partner.partner_longitude}`,
|
|
});
|
|
|
|
const popup = L.popup({ offset: [0, -30] })
|
|
.setLatLng([
|
|
partner.partner_latitude + latLongOffset,
|
|
partner.partner_longitude - latLongOffset,
|
|
])
|
|
.setContent(popupHtml)
|
|
.openOn(this.leafletMap);
|
|
|
|
const openBtn = popup
|
|
.getElement()
|
|
.querySelector("button.o-map-renderer--popup-buttons-open");
|
|
if (openBtn) {
|
|
openBtn.onclick = () => {
|
|
this.props.onMarkerClick(markerInfo.ids);
|
|
};
|
|
}
|
|
return popup;
|
|
}
|
|
/**
|
|
* @param {Number} groupId
|
|
*/
|
|
getGroupColor(groupId) {
|
|
const index = Object.keys(this.props.model.data.recordGroups).indexOf(groupId);
|
|
return colors[index % colors.length];
|
|
}
|
|
/**
|
|
* Creates an array of latLng objects if there is located records.
|
|
*
|
|
* @returns {latLngBounds|boolean} objects containing the coordinates that
|
|
* allows all the records to be shown on the map or returns false
|
|
* if the records does not contain any located record.
|
|
*/
|
|
getLatLng() {
|
|
const tabLatLng = [];
|
|
for (const record of this.props.model.data.records) {
|
|
const partner = record.partner;
|
|
if (partner && partner.partner_latitude && partner.partner_longitude) {
|
|
tabLatLng.push(L.latLng(partner.partner_latitude, partner.partner_longitude));
|
|
}
|
|
}
|
|
if (!tabLatLng.length) {
|
|
return false;
|
|
}
|
|
return L.latLngBounds(tabLatLng);
|
|
}
|
|
/**
|
|
* Get the fields' name and value to display in the popup.
|
|
*
|
|
* @param {Object} markerInfo
|
|
* @returns {Object} value contains the value of the field and string
|
|
* contains the value of the xml's string attribute
|
|
*/
|
|
getMarkerPopupFields(markerInfo) {
|
|
const record = markerInfo.record;
|
|
const fieldsView = [];
|
|
// Only display address in multi coordinates marker popup
|
|
if (markerInfo.ids.length > 1) {
|
|
if (!this.props.model.metaData.hideAddress) {
|
|
fieldsView.push({
|
|
id: this.nextId++,
|
|
value: record.partner.contact_address_complete,
|
|
string: this.env._t("Address"),
|
|
});
|
|
}
|
|
return fieldsView;
|
|
}
|
|
if (!this.props.model.metaData.hideName) {
|
|
fieldsView.push({
|
|
id: this.nextId++,
|
|
value: record.display_name,
|
|
string: this.env._t("Name"),
|
|
});
|
|
}
|
|
if (!this.props.model.metaData.hideAddress) {
|
|
fieldsView.push({
|
|
id: this.nextId++,
|
|
value: record.partner.contact_address_complete,
|
|
string: this.env._t("Address"),
|
|
});
|
|
}
|
|
for (const field of this.props.model.metaData.fieldNamesMarkerPopup) {
|
|
if (record[field.fieldName]) {
|
|
const fieldName =
|
|
record[field.fieldName] instanceof Array
|
|
? record[field.fieldName][1]
|
|
: record[field.fieldName];
|
|
fieldsView.push({
|
|
id: this.nextId++,
|
|
value: fieldName,
|
|
string: field.string,
|
|
});
|
|
}
|
|
}
|
|
return fieldsView;
|
|
}
|
|
/**
|
|
* Remove the markers from the map and empty the markers array.
|
|
*/
|
|
removeMarkers() {
|
|
for (const marker of this.markers) {
|
|
marker.off("click");
|
|
this.leafletMap.removeLayer(marker);
|
|
}
|
|
this.markers = [];
|
|
}
|
|
/**
|
|
* Remove the routes from the map and empty the the polyline array.
|
|
*/
|
|
removeRoutes() {
|
|
for (const polyline of this.polylines) {
|
|
polyline.off("click");
|
|
this.leafletMap.removeLayer(polyline);
|
|
}
|
|
this.polylines = [];
|
|
}
|
|
/**
|
|
* Update position in the map, markers and routes.
|
|
*/
|
|
updateMap() {
|
|
if (this.props.model.data.shouldUpdatePosition) {
|
|
const initialCoord = this.getLatLng();
|
|
if (initialCoord) {
|
|
this.leafletMap.flyToBounds(initialCoord, { animate: false });
|
|
} else {
|
|
this.leafletMap.fitWorld();
|
|
}
|
|
this.leafletMap.closePopup();
|
|
}
|
|
this.addMarkers();
|
|
this.addRoutes();
|
|
}
|
|
|
|
/**
|
|
* Center the map on a certain pin and open the popup linked to it.
|
|
*
|
|
* @param {Object} record
|
|
*/
|
|
centerAndOpenPin(record) {
|
|
const popup = this.createMarkerPopup({
|
|
record: record,
|
|
ids: [record.id],
|
|
});
|
|
const px = this.leafletMap.project([
|
|
record.partner.partner_latitude,
|
|
record.partner.partner_longitude,
|
|
]);
|
|
const popupHeight = popup.getElement().offsetHeight;
|
|
px.y -= popupHeight / 2;
|
|
const latlng = this.leafletMap.unproject(px);
|
|
this.leafletMap.panTo(latlng, { animate: true });
|
|
}
|
|
/**
|
|
* @param {Number} id
|
|
*/
|
|
toggleGroup(id) {
|
|
if (this.state.closedGroupIds.includes(id)) {
|
|
const index = this.state.closedGroupIds.indexOf(id);
|
|
this.state.closedGroupIds.splice(index, 1);
|
|
} else {
|
|
this.state.closedGroupIds.push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
MapRenderer.template = "web_map.MapRenderer";
|
|
MapRenderer.props = {
|
|
model: Object,
|
|
onMarkerClick: Function,
|
|
};
|