我在美国市政府工作,并建立了一个演示页面,求职者可以在其中输入他们的地址并查看他们居住地通勤距离内的办公室和其他工作地点。他们可以设置出行模式和最长通勤时间。
我正在使用 Geoapify 的 Isoline API 来获取“isochrones”(“iso”表示相等;“chrone”表示时间,如“chronometer”中的时间),效果很好。
我使用 Leaflet JS 绘图工具将等时线覆盖在城市地图上,并使用“标记”指示所有城市工作地点。这也很好用。
我需要帮助的是如何识别哪些工作位置位于等时线边界(或多个边界)内,哪些位置位于等时线边界之外,以便我可以应用不同的颜色(使用过滤器:hue-rotate(_deg) 技巧在其他地方提到) StackOverflow)并从职位搜索结果中排除这些位置。用户还可以单击标记来包含/排除特定位置。
我在 Geoapify 的等值线文档或其他任何内容中找不到告诉我如何做到这一点的任何内容。我想我也许可以使用 Geometry API 中的“交集”选项,但这仅适用于 Geoapify 先前返回的多边形,而不适用于先前返回的多边形和任意点。
我在互联网上的其他地方也找不到任何有用的东西,这让我感到惊讶,因为我认为确定一个位置是否位于 Geoapify 多边形或一组多边形的内部或外部将是一个常见问题。
如有任何建议,我们将不胜感激。
编辑 1:这是 CodePen 上的: https://codepen.io/PeterNotInPT/pen/jOXvEBj
编辑 2:机器人说我需要提供一些代码,所以...
我有一系列工作地点:
const workLocations = [
{address: "1100 4th St SW", checkBoxId: "1100_4th_St_SW", lat: 38.8779236210903, long: -77.01734764473007},
{address: "901 G St NW (MLK Library)", checkBoxId: "901_G_St_NW", lat: 38.898857617145985, long: -77.02490318890965},
{address: "200 I St SE", checkBoxId: "200_I_St_SE", lat: 38.88009836346278, long: -77.00301703123823},
{address: "250 M St SE", checkBoxId: "250_M_St_SE", lat: 38.87710278028664, long: -77.00228744473007},
{address: "1200 First St NE", checkBoxId: "1200_1st_St NE", lat: 38.9062224010956, long: -77.00633570240119},
{address: "401 E St SW", checkBoxId: "401_E_St_SW", lat: 38.88384810576917, long: -77.01837154472987},
{address: "5171 S. Dakota Ave NE", checkBoxId: "5171_S_Dakota_Ave_NE", lat: 38.953866767991535, long: -76.99689878892342},
{address: "4058 Minnesota Ave NE", checkBoxId: "4058_Minnesota_Ave_NE", lat: 38.8972271711626, long: -76.94782147356544},
{address: "64 New York Ave NE", checkBoxId: "64_New_York_Ave NE", lat: 38.909404103508585, long: -77.00682069859793},
{address: "441 4th St NW", checkBoxId: "441_4th_St_NW", lat: 38.89557803243445, long: -77.01559446369988},
{address: "250 E St SW", checkBoxId: "250_E_St_SW", lat: 38.88308925938507, long: -77.01442327356588},
{address: "899 N Capitol St NE", checkBoxId: "899_N_Capitol_St_NE", lat: 38.90122286178571, long: -77.00851884472931},
{address: "300 Indiana Ave NW", checkBoxId: "300_Indiana_Ave_NW", lat: 38.89434842828109, long: -77.01654850240159},
{address: "450 H St NW", checkBoxId: "450_H_St_NW", lat: 38.8996997262375, long: -77.01834512531399},
{address: "1350 Pennsylvania Ave NW", checkBoxId: "1350_Pennsylvania_Ave_NW", lat: 38.89524892650393, long: -77.0311579735655},
{address: "100 M St SE", checkBoxId: "100_M_St_SE", lat: 38.87693687601847, long: -77.00543590240223},
{address: "1000 Mt Olivet Rd NE", checkBoxId: "1000_Mt_Olivet_Rd_NE", lat: 38.912293364817415, long: -76.98856784061661},
{address: "1015 Half St SE", checkBoxId: "1015_Half_St_SE", lat: 38.878250824689125, long: -77.00787066007439},
{address: "1050 1st St NE", checkBoxId: "1050_1st_St_NE", lat: 38.90351025490712, long: -77.00626776007343},
{address: "4665 Blue Plains Dr SW (Police Academy)", checkBoxId: "4665_Blue_Plains_Dr_SW", lat: 38.82174895414032, long: -77.01337855584724},
{address: "2000 14th St NW", checkBoxId: "2000_14th_St_NW", lat: 38.91751656148031, long: -77.03237447356476},
{address: "1133 15th St NW", checkBoxId: "1133_15th_St_NW", lat: 38.90504625093286, long: -77.0340116735652},
{address: "1325 G St NW", checkBoxId: "1325_G_St_NW", lat: 38.898638267512005, long: -77.03104443123755},
{address: "2720 Martin Luther King Jr SE (St. Elizabeths Hospital)", checkBoxId: "2720_Martin_Luther_King_Jr_SE", lat: 38.853848229101054, long: -76.9951069305929},
{address: "3924 Minnesota Ave NE", checkBoxId: "3924_Minnesota_Ave_NE", lat: 38.89411232957772, long: -76.95144373123782},
{address: "515 5th st NW", checkBoxId: "515_5th_St_NW", lat: 38.896895522050734, long: -77.01835873123757},
{address: "655 15th St NW", checkBoxId: "655_15th_St_NW", lat: 38.89808214620608, long: -77.03307029705881},
{address: "2001 E. Capitol St (DC Armory)", checkBoxId: "2001_E_Capitol_St", lat: 38.88910535380775, long: -76.97550222355162},
{address: "1901 D St SE (Detention Facility)", checkBoxId: "1901_D_St_SE", lat: 38.883812015891536, long: -76.97599977044904}
];
使用 Leaflet 地图库获取底图并将工作地点添加到该地图中:
let lastIsochrone;
let lastMarker;
let map;
const drawBaseMap = function() {
map = L.map('map').setView([centerDC.lat, centerDC.long], zoom);
// Retina displays require different mat tiles quality
var isRetina = L.Browser.retina;
var baseUrl = `https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png?apiKey=${apiKey}`;
var retinaUrl = `https://maps.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}@2x.png?apiKey=${apiKey}`;
// Tiles
L.tileLayer(isRetina ? retinaUrl : baseUrl, {
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 20,
id: 'osm-bright',
}).addTo(map);
/* I intended to call addWorkLocationsToMap() from the initialize method but there is a scope problem because Leaflet
is creating a variable 't' and even though 'map' is a global variable, 't' is not so there is an 'undefined' error.
I also tried passing 'L' that is apparently global but that did not help so dumping contents of that method here.
I'll try again later. */
let markerLocations =[];
workLocations.forEach(function (location, index) {
markerLocations[index] = L.marker([location.lat, location.long], {title: location.address}).addTo(map);
markerLocations[index]._icon.classList.add("green");
markerLocations[index]._icon.setAttribute("linkedchkbox", location.checkBoxId);
markerLocations[index].addEventListener("click", function(e){
let linkedChkBoxID = markerLocations[index]._icon.getAttribute("linkedchkbox");
let linkedChkBox = document.getElementById(linkedChkBoxID);
markerLocations[index]._icon.classList.toggle("green");
markerLocations[index]._icon.classList.toggle("red");
if(markerLocations[index]._icon.classList.contains("green")) {
linkedChkBox.checked = true;
linkedChkBox.setAttribute("checked", "checked");
}
else {
linkedChkBox.checked = false;
linkedChkBox.removeAttribute("checked");
}
// POST to https://careers.dc.gov/psc/erecruit/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBL
// Current page uses fake checkboxes with ID PTS_SELECT$chk$<index>
});
});
};
最后添加来自 Geoapify 的“等时线”,即用户可以使用所需的出行方式在指定时间内出行的区域
const getCommuterMap = function(startAddress) {
warningNotice.classList.add("hidden");
const encodedAddressText = encodeURI(startAddress);
var getAddressDetailsUrl = `https://api.geoapify.com/v1/geocode/search?text=${encodedAddressText}&format=json&apiKey=${apiKey}&filter=circle:${centerDC.long},${centerDC.lat},${searchRadius}`;
fetch(getAddressDetailsUrl, {
method: 'GET'
})
.then(response => {
if(response.ok) {
return response.json();
}
else {
displayErrorMsg("Invalid server response. Status code: " + response.status + " (" + response.statusText +")");
}
})
.then(data => {
if(data.results && ["building", "amenity"].includes(data.results[0].result_type) && data.results[0].rank.confidence > 0.95) {
var addressInfo = {lat: data.results[0].lat, lon: data.results[0].lon, addressLine1: data.results[0].address_line1};
var transitMode = getTransitMode();
var maxCommuteTime = getMaxCommuteTime();
addStartMarkerToMap(addressInfo);
addIsochroneToMap(addressInfo, transitMode, maxCommuteTime * 60);
}
else {
displayErrorMsg("Sorry, the address '" + data.query.text + "' was not recognized.");
}
})
.catch((error) => {
displayErrorMsg(error);
});
};
const addStartMarkerToMap = function(addressInfo) {
lastMarker = L.marker([addressInfo.lat, addressInfo.lon], {title: addressInfo.addressLine1}).addTo(map);
};
const addIsochroneToMap = function(addressInfo, transitMode, range) {
fetch(`https://api.geoapify.com/v1/isoline?lat=${addressInfo.lat}&lon=${addressInfo.lon}&type=time&mode=${transitMode}&range=${range}&apiKey=${apiKey}`)
.then(data => data.json())
.then(geoJSONFeatures => {
lastIsochrone = L.geoJSON(geoJSONFeatures, {
style: (feature) => {
return {
stroke: true,
color: '#9933ff',
weight: 2,
opacity: 0.7,
fill: true,
fillColor: '#333399',
fillOpacity: 0.15,
smoothFactor: 0.5,
interactive: false
};
}
}).addTo(map);
});
};
最后,地图上位置标记的样式。我想将等时线内的那些设为绿色,将等时线外的设为红色。问题是我不知道如何判断哪些位置标记位于等时线内。
<style>
.red { filter: hue-rotate(150deg); }
.green { filter: hue-rotate(270deg); }
</style>
编辑3:看起来我可以使用此处的信息通过自己的解决方案来实现: http://alienryderflex.com/polygon/
正如我在上面的编辑 3 中指出的,确定点是否在多边形内部的方法在 http://alienryderflex.com/polygon/ 中有详细介绍。然而,Geoapify 数据可能包含“飞地”——多边形内的多边形——所以我必须跳过一些额外的环节来处理这个问题。
请注意,Geoapify 数据返回一个点为 [经度,纬度],而如果您右键单击 Google 地图,顶部选项是表示为“纬度,经度”的坐标,这就是我们通常所说的方式。我最初的一些代码在需要经度时使用纬度,反之亦然。
两个新功能:
const locationIsInsidePolygon = function(location, polygon) { // Polygon may be an "enclave" within an outer polygon!
// From http://alienryderflex.com/polygon/ (written in C, not Javascript)
// The function will return true if the point x,y is inside the polygon, or false if it is not.
let i;
let j = polygon.length - 1;
let numberOfNodesIsOdd = false;
for (i = 0; i < polygon.length; i++) {
//for (i=0; i<polyCorners; i++) {
if((polygon[i][1] < location.lat && polygon[j][1] >= location.lat) ||
(polygon[j][1] < location.lat && polygon[i][1] >= location.lat)) {
if(polygon[i][0] + (location.lat - polygon[i][1]) / (polygon[j][1] - polygon[i][1]) * (polygon[j][0] - polygon[i][0]) < location.lon) {
numberOfNodesIsOdd =! numberOfNodesIsOdd;
}
}
j=i;
}
return numberOfNodesIsOdd;
};
const updateWorkLocationMarkers = function(responseObj) {
/*
The response from Geoapify contains features -> geometry -> coordinates which is an array of arrays of arrays.
The top-level contans one element for each isochrone which is represented by a "blob" on the map.
However, there can be "enclaves" within blobs that a user can't reach within the travel time specified.
If there are enclaves, the first of the second-level arrays is the "outer" polygon, then subsequent elements
are the "enclaves". If there are no enclaves, there is a single array element at the second level.
Third-level elements are the longitude/latitude points for each corner of the polygon.
*/
const topLevelArrayElements = responseObj.features[0].geometry.coordinates;
let secondLevelArrayElements;
let isInsideTravelZone;
let isInsidePolygon;
let isEnclave;
let i, j;
workLocations.forEach(function (location, index) {
markerLocation = {lat: location.lat, lon: location.long};
isInsideTravelZone = false;
isEnclave = false;
for(i=0; i < topLevelArrayElements.length; i++) {
secondLevelArrayElements = topLevelArrayElements[i];
isEnclave = false;
//hasEnclaves = secondLevelArrayElements.length == 1 ? false : true;
for(j=0; j < secondLevelArrayElements.length; j++) {
if(j > 0) {
isEnclave = true;
}
isInsidePolygon = locationIsInsidePolygon(markerLocation, secondLevelArrayElements[j]);
if(isInsidePolygon) {
if(isEnclave){
isInsideTravelZone = false;
break;
}
else {
isInsideTravelZone = true; // May be reset to false if there are enclaves.
}
}
}
if(isInsideTravelZone) {
break; // No need to test other isoclines/polygons
}
}
if(!isInsideTravelZone) {
setMarkerAsInsideSearchArea(index, false);
}
});
};
第二个方法在 addIsochroneToMap 的底部调用,紧接在
.addTo(map);
之后
updateWorkLocationMarkers(geoJSONFeatures);