migrate to maplibre-gl for maps

This commit is contained in:
j 2025-08-05 18:50:06 +02:00
commit 8cebad9fb4
7 changed files with 605 additions and 234 deletions

View file

@ -99,6 +99,7 @@ Ox.Map = function(options, self) {
return place.name || '<span class="OxLight">Unnamed</span>';
},
maxMarkers: 100,
nominatim: 'https://nominatim.openstreetmap.org',
places: null,
selected: '',
showControls: false,
@ -106,6 +107,7 @@ Ox.Map = function(options, self) {
showStatusbar: false,
showToolbar: false,
showZoombar: false,
style: 'https://tiles.openfreemap.org/styles/liberty',
zoomOnlyWhenFocused: false
// fixme: width, height
})
@ -143,7 +145,7 @@ Ox.Map = function(options, self) {
self.map.fitBounds(mapBounds);
} else {
self.map.setZoom(self.minZoom);
self.map.setCenter(new google.maps.LatLng(0, 0));
self.map.setCenter(new maplibregl.LngLat(0, 0));
}
// fixme: the following is just a guess
self.boundsChanged = true;
@ -509,23 +511,27 @@ Ox.Map = function(options, self) {
$placeControl.css({opacity: 0}).hide();
});
if (window.google) {
if (window.maplibregl) {
// timeout needed so that the map is in the DOM
setTimeout(initMap);
} else if (window.googleCallback) {
(function interval() {
isLoaded() ? initMap() : setTimeout(interval, 100);
}());
} else {
window.googleCallback = function() {
delete window.googleCallback;
initMap();
};
$.getScript(
document.location.protocol
+ '//maps.google.com/maps/api/js?callback=googleCallback&sensor=false'
+ (Ox.Map.GoogleApiKey ? '&key=' + Ox.Map.GoogleApiKey : '')
);
Ox.getStylesheet([
Ox.PATH + 'UI/maplibre-gl/maplibre-gl.css',
Ox.PATH + 'UI/maplibre-gl/maplibre-gl-geocoder.css'
], () => {})
$.getScript([
Ox.PATH + 'UI/maplibre-gl/maplibre-gl.js',
Ox.PATH + 'UI/maplibre-gl/maplibre-gl-geocoder.min.js',
], initMap)
}
function equalBonds(a, b) {
return (
a._sw.lat == b._sw.lat &&
a._sw.lng == b._sw.lng &&
a._ne.lat == b._ne.lat &&
a._ne.lng == b._ne.lng
)
}
function addPlaceToMap(place) {
@ -535,11 +541,11 @@ Ox.Map = function(options, self) {
if (!place) {
var bounds = self.map.getBounds(),
center = self.map.getCenter(),
southwest = new google.maps.LatLngBounds(
bounds.getSouthWest(), center
southwest = new maplibregl.LngLatBounds(
bounds._sw, center
).getCenter(),
northeast = new google.maps.LatLngBounds(
center, bounds.getNorthEast()
northeast = new maplibregl.LngLatBounds(
center, bounds._ne
).getCenter(),
place = new Ox.MapPlace({
alternativeNames: [],
@ -550,14 +556,14 @@ Ox.Map = function(options, self) {
map: that,
name: '',
type: 'feature',
south: southwest.lat(),
west: southwest.lng(),
north: northeast.lat(),
east: northeast.lng()
south: southwest.lat,
west: southwest.lng,
north: northeast.lat,
east: northeast.lng
});
}
Ox.forEach(self.places, function(p, i) {
if (place.bounds.equals(p.bounds)) {
if (equalBonds(place.bounds, p.bounds)) {
place = p;
exists = true;
return false; // break
@ -593,13 +599,12 @@ Ox.Map = function(options, self) {
// checks if outerBounds _can_ contain innerBounds
var outerSpan = outerBounds.toSpan(),
innerSpan = innerBounds.toSpan();
return outerSpan.lat() > innerSpan.lat() &&
outerSpan.lng() > innerSpan.lng();
return outerSpan.lat > innerSpan.lat &&
outerSpan.lng > innerSpan.lng;
}
function centerChanged() {
var tooltip = $('.OxMapMarkerTooltip');
tooltip.length && Ox.$elements[$(tooltip[0]).data('oxid')].hide();
that.tooltip.remove()
self.center = self.map.getCenter();
self.centerChanged = true;
}
@ -611,7 +616,7 @@ Ox.Map = function(options, self) {
function clickMap(event) {
var place = getSelectedPlace();
if (self.options.clickable/* && !editing()*/) {
getPlaceByLatLng(event.latLng, self.map.getBounds(), function(place) {
getPlaceByLatLng(event.lngLat, self.map.getBounds(), function(place) {
if (place) {
addPlaceToMap(place);
//selectPlace(place.id);
@ -656,7 +661,7 @@ Ox.Map = function(options, self) {
function crossesDateline() {
var bounds = self.map.getBounds();
return bounds.getSouthWest().lng() > bounds.getNorthEast().lng();
return bounds._sw.lng > bounds._ne.lng;
}
function editing() {
@ -681,9 +686,9 @@ Ox.Map = function(options, self) {
// get initial map bounds
self.options.places({}, function(result) {
var area = result.data.area;
callback(new google.maps.LatLngBounds(
new google.maps.LatLng(area.south, area.west),
new google.maps.LatLng(area.north, area.east)
callback(new maplibregl.LngLatBounds(
new maplibregl.LngLat(area.west, area.south),
new maplibregl.LngLat(area.east, area.north)
));
});
}
@ -704,15 +709,22 @@ Ox.Map = function(options, self) {
callback = point;
point = self.map.getCenter();
}
let maxZoom = self.map.getMaxZoom()
setTimeout(() => {
callback(maxZoom)
})
/*
self.maxZoomService.getMaxZoomAtLatLng(point, function(data) {
callback(data.status == 'OK' ? data.zoom : null);
});
*/
}
function getMetersPerPixel() {
// m/px = m/deg * deg/px
var degreesPerPixel = 360 / (self.tileSize * Math.pow(2, self.map.getZoom()));
return Ox.getMetersPerDegree(self.map.getCenter().lat()) * degreesPerPixel;
const degreesPerPixel = 360 / (self.tileSize * Math.pow(2, self.map.getZoom()));
const center = self.map.getCenter();
return Ox.getMetersPerDegree(center.lat) * degreesPerPixel;
}
function getMinZoom() {
@ -734,61 +746,50 @@ Ox.Map = function(options, self) {
: Ox.getObjectById(self.places, id);
}
function getPlaceByLatLng(latlng, bounds, callback) {
async function getPlaceByLatLng(latlng, bounds, callback) {
// gets the largest place at latlng that would fit in bounds
var callback = arguments.length == 3 ? callback : bounds,
bounds = arguments.length == 3 ? bounds : null;
self.$loadingIcon && self.$loadingIcon.start();
self.geocoder.geocode({
latLng: latlng
}, function(results, status) {
self.$loadingIcon && self.$loadingIcon.stop();
if (status == google.maps.GeocoderStatus.OK) {
if (bounds) {
Ox.forEach(results.reverse(), function(result, i) {
if (
i == results.length - 1 ||
canContain(bounds, result.geometry.bounds || result.geometry.viewport)
) {
callback(new Ox.MapPlace(parseGeodata(results[i])));
return false; // break
}
});
} else {
callback(new Ox.MapPlace(parseGeodata(results[0])));
}
}
if (
status == google.maps.GeocoderStatus.OK ||
status == google.maps.GeocoderStatus.ZERO_RESULTS
) {
triggerGeocodeEvent({
latLng: latlng,
results: results
var results = await reverseGeocode(latlng);
self.$loadingIcon && self.$loadingIcon.stop();
if (true) {
if (bounds) {
Ox.forEach(results, function(result, i) {
if (
i == results.length - 1 ||
canContain(bounds, result.geometry.bounds || result.geometry.viewport)
) {
callback(new Ox.MapPlace(parseGeodata(results[i])));
return false; // break
}
});
} else {
Ox.Log('Map', 'geocode failed:', status);
callback(null);
callback(new Ox.MapPlace(parseGeodata(results[0])));
}
});
}
if (!results.length) {
triggerGeocodeEvent({
latLng: latlng,
results: results
});
} else {
Ox.Log('Map', 'geocode failed:', status);
callback(null);
}
}
function getPlaceByName(name, callback) {
self.$loadingIcon && self.$loadingIcon.start();
self.geocoder.geocode({
address: name
}, function(results, status) {
forwardGeocode({
query: name
}).then(function(results) {
self.$loadingIcon && self.$loadingIcon.stop();
if (status == google.maps.GeocoderStatus.OK) {
callback(new Ox.MapPlace(parseGeodata(results[0])));
}
if (
status == google.maps.GeocoderStatus.OK
&& status != google.maps.GeocoderStatus.ZERO_RESULTS
) {
if (results.features.length) {
callback(new Ox.MapPlace(parseGeodata(results.features[0])));
triggerGeocodeEvent({
address: name,
results: results
results: results.features
});
} else {
Ox.Log('Map', 'geocode failed:', status);
@ -831,36 +832,167 @@ Ox.Map = function(options, self) {
: null;
}
function bbox2bounds(bbox) {
return new maplibregl.LngLatBounds(
new maplibregl.LngLat(bbox[0], bbox[1]),
new maplibregl.LngLat(bbox[2], bbox[3])
)
}
function converNominatimFeature(feature) {
const center = [
feature.bbox[0] +
(feature.bbox[2] - feature.bbox[0]) / 2,
feature.bbox[1] +
(feature.bbox[3] - feature.bbox[1]) / 2
];
const polygon = {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [
[feature.bbox[0], feature.bbox[1]],
[feature.bbox[1], feature.bbox[2]],
[feature.bbox[2], feature.bbox[3]],
[feature.bbox[0], feature.bbox[3]],
]
},
bounds: bbox2bounds(feature.bbox),
place_name: feature.properties.display_name,
properties: feature.properties,
text: feature.properties.display_name,
address_components: [
{
long_name: feature.properties.display_name,
short_name: feature.properties.display_name,
types: ['place']
}
],
place_type: ['place'],
};
return polygon
}
async function reverseGeocode(config) {
const features = [];
try {
const request = `${self.options.nominatim}/reverse?lat=${
config.lat
}&lon=${
config.lng
}&format=geojson&polygon_geojson=1&addressdetails=1`;
const response = await fetch(request);
const geojson = await response.json();
for (const feature of geojson.features) {
features.push(converNominatimFeature(feature));
}
} catch (e) {
console.error(`Failed to reverseGeocode with error: ${e}`);
}
return {
features
};
}
async function forwardGeocode(config) {
const features = [];
try {
const request = `${self.options.nominatim}/search?q=${
config.query
}&format=geojson&polygon_geojson=1&addressdetails=1`;
const response = await fetch(request);
const geojson = await response.json();
for (const feature of geojson.features) {
features.push(converNominatimFeature(feature));
}
} catch (e) {
console.error(`Failed to forwardGeocode with error: ${e}`);
}
return {
features
};
}
function initMap() {
getMapBounds(function(mapBounds) {
//Ox.Log('Map', 'init', mapBounds.getSouthWest(), mapBounds.getNorthEast(), mapBounds.getCenter())
//Ox.Log('Map', 'init', mapBounds._sw, mapBounds._ne, mapBounds.getCenter())
self.elevationService = new google.maps.ElevationService();
self.geocoder = new google.maps.Geocoder();
self.maxZoomService = new google.maps.MaxZoomService();
//self.elevationService = new google.maps.ElevationService();
//self.maxZoomService = new google.maps.MaxZoomService();
//
self.center = mapBounds ? mapBounds.getCenter() : new google.maps.LatLng(0, 0);
self.center = mapBounds ? mapBounds.getCenter() : new maplibregl.LngLat(0, 0);
self.zoom = self.minZoom;
that.map = self.map = new google.maps.Map(self.$map[0], {
window.map = that.map = self.map = new maplibregl.Map({
container: self.$map[0],
center: self.center,
disableDefaultUI: true,
disableDoubleClickZoom: true,
mapTypeId: google.maps.MapTypeId[getMapType()],
noClear: true,
scrollwheel: !self.options.zoomOnlyWhenFocused,
style2: self.options.style,
style: {
'version': 8,
'sources': {
'raster-tiles': {
'type': 'raster',
'tiles': [
// FIXME: use protocol and make use of all subdomains
//'https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}'
'https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}'
],
'subdomains':['mt0','mt1','mt2','mt3'],
'tileSize': 256,
'attribution':
'FIXME',
}
},
'layers': [
{
'id': 'simple-tiles',
'type': 'raster',
'source': 'raster-tiles',
'minzoom': 0,
'maxzoom': 22
}
]
},
//mapTypeId: google.maps.MapTypeId[getMapType()],
//noClear: true,
//scrollwheel: !self.options.zoomOnlyWhenFocused,
zoom: self.zoom
})
self.geocoder = new MaplibreGeocoder({
forwardGeocode: forwardGeocode,
reverseGeocode: reverseGeocode,
})
/*
map.addControl(self.geocoder, {
maplibregl
});
*/
that.map.on('click', clickMap)
that.map.on('zoom', zoomChanged)
that.map.on('idle', mapChanged)
that.map.on('moveend', boundsChanged)
that.map.on('dragend', boundsChanged)
that.map.on('zoomend', boundsChanged)
/*
that.map.on('resize', () => {
that.resizeMap()
})
*/
/*
google.maps.event.addListener(self.map, 'bounds_changed', boundsChanged);
google.maps.event.addListener(self.map, 'center_changed', centerChanged);
google.maps.event.addListener(self.map, 'click', clickMap);
google.maps.event.addListener(self.map, 'idle', mapChanged);
google.maps.event.addListener(self.map, 'zoom_changed', zoomChanged);
google.maps.event.trigger(self.map, 'resize');
*/
// needed to get mouse x/y coordinates on marker mouseover,
// see http://code.google.com/p/gmaps-api-issues/issues/detail?id=2342
/*
that.overlayView = new google.maps.OverlayView();
that.overlayView.setMap(self.map);
that.overlayView.draw = function () {
@ -870,52 +1002,55 @@ Ox.Map = function(options, self) {
}
}
that.overlayView.draw();
*/
Ox.forEach(self.$controls, function($control) {
$control.appendTo(self.$map);
});
Ox.forEach(self.$placeControls, function($placeControl) {
$placeControl.appendTo(self.$map);
});
that.map.on('load', () => {
Ox.forEach(self.$controls, function($control) {
$control.appendTo(self.$map);
});
Ox.forEach(self.$placeControls, function($placeControl) {
$placeControl.appendTo(self.$map);
});
if (self.options.find) {
self.$findInput
.value(self.options.find)
.triggerEvent('submit', {value: self.options.find});
} else {
if (self.options.selected) {
selectPlace(self.options.selected, true);
}
if (mapBounds) {
if (isEmpty(mapBounds)) {
if (self.options.find) {
self.$findInput
.value(self.options.find)
.triggerEvent('submit', {value: self.options.find});
} else {
if (self.options.selected) {
selectPlace(self.options.selected, true);
}
if (mapBounds) {
if (isEmpty(mapBounds)) {
self.map.setZoom(self.minZoom);
} else {
self.map.fitBounds(mapBounds);
}
}
if (self.map.getZoom() < self.minZoom) {
self.map.setZoom(self.minZoom);
} else {
self.map.fitBounds(mapBounds);
}
}
if (self.map.getZoom() < self.minZoom) {
self.map.setZoom(self.minZoom);
}
}
updateFormElements();
updateFormElements();
self.loaded = true;
that.triggerEvent('load');
that.resizeMap()
self.loaded = true;
self.boundsChanged = true;
that.triggerEvent('load');
})
});
}
function isEmpty(bounds) {
// Google's bounds.isEmpty() is not reliable
var southWest = bounds.getSouthWest(),
northEast = bounds.getNorthEast();
return southWest.lat() == northEast.lat()
&& southWest.lng() == northEast.lng();
return bounds._sw.lat == bounds._ne.lat
&& bounds._sw.lng == bounds._ne.lng;
}
function isLoaded() {
return window.google && window.google.maps && window.google.maps.LatLng;
return window.maplibregl && window.maplibregl.maps && window.maplibregl.LngLat;
}
function mapChanged() {
@ -927,12 +1062,11 @@ Ox.Map = function(options, self) {
self.boundsChanged = false;
return
}
var southWest = bounds.getSouthWest(),
northEast = bounds.getNorthEast(),
south = southWest.lat(),
west = southWest.lng(),
north = northEast.lat(),
east = northEast.lng();
var south = bounds._sw.lat,
west = bounds._sw.lng,
north = bounds._ne.lat,
east = bounds._ne.lng;
self.options.places({
keys: self.placeKeys,
query: {
@ -1015,22 +1149,24 @@ Ox.Map = function(options, self) {
}
function parseGeodata(data) {
var bounds = data.geometry.bounds || data.geometry.viewport,
northEast = bounds.getNorthEast(),
southWest = bounds.getSouthWest(),
console.log("parseGeodata", data)
// FIXME: data is geojson Feature with Polygon geometry now
var bounds = data.bounds || data.geometry.bounds || data.geometry.viewport,
northEast = bounds._ne,
southWest = bounds._sw,
place = {
alternativeNames: [],
components: data.address_components,
countryCode: getCountryCode(data.address_components),
east: northEast.lng(),
east: northEast.lng,
editable: self.options.editable,
fullGeoname: getFullGeoname(data.address_components),
id: '_' + Ox.encodeBase32(Ox.uid()),
map: that,
north: northEast.lat(),
south: southWest.lat(),
north: northEast.lat,
south: southWest.lat,
type: getType(data.address_components[0].types),
west: southWest.lng()
west: southWest.lng
};
place.geoname = data.formatted_address || place.fullGeoname;
place.name = (place.geoname || place.fullGeoname).split(', ')[0];
@ -1338,8 +1474,8 @@ Ox.Map = function(options, self) {
// so we create properly named properties, for json encoding
if (data.latLng) {
data.latLng = {
lat: data.latLng.lat(),
lng: data.latLng.lng()
lat: data.latLng.lat,
lng: data.latLng.lng
}
}
data.results.forEach(function(result) {
@ -1347,20 +1483,20 @@ Ox.Map = function(options, self) {
if (result.geometry[key]) {
result.geometry[key] = {
northEast: {
lat: result.geometry[key].getNorthEast().lat(),
lng: result.geometry[key].getNorthEast().lng()
lat: result.geometry[key]._ne.lat,
lng: result.geometry[key]._ne.lng
},
southWest: {
lat: result.geometry[key].getSouthWest().lat(),
lng: result.geometry[key].getSouthWest().lng()
lat: result.geometry[key]._sw.lat,
lng: result.geometry[key]._sw.lng
}
}
}
});
if (result.geometry.location) {
result.geometry.location = {
lat: result.geometry.location.lat(),
lng: result.geometry.location.lng()
lat: result.geometry.location.lat,
lng: result.geometry.location.lng
}
}
});
@ -1402,7 +1538,7 @@ Ox.Map = function(options, self) {
}
function zoomChanged() {
var zoom = self.map.getZoom();
var zoom = parseInt(self.map.getZoom());
if (zoom < self.minZoom) {
self.map.setZoom(self.minZoom);
} else if (self.maxZoom && zoom > self.maxZoom) {
@ -1443,8 +1579,7 @@ Ox.Map = function(options, self) {
lng <n> Longitude
@*/
that.getCenter = function() {
var center = self.map.getCenter();
return {lat: center.lat(), lng: center.lng()};
return self.map.getCenter();
};
/*@
@ -1544,7 +1679,7 @@ Ox.Map = function(options, self) {
});
updateFormElements();
Ox.Log('Map', 'triggering google maps resize event, height', self.options.height)
google.maps.event.trigger(self.map, 'resize');
self.map.triggerRepaint()
// self.map.setCenter(center);
}
return that;
@ -1558,7 +1693,7 @@ Ox.Map = function(options, self) {
lng <n> Longitude
@*/
that.setCenter = function(center) {
self.map.setCenter(new google.maps.LatLng(center.lat, center.lng));
self.map.setCenter(new maplibregl.LngLat(center.lng, center.lat));
return that;
};