Merge branch 'dev_robotaxi-d-app-module_280_220608_2.8.0' into dev_robotaxi-d-app-module_280_taxi_passenger
This commit is contained in:
@@ -13,4 +13,5 @@ import mogo.telematics.pad.MessagePad;
|
||||
public interface IBusPassengerAutopilotPlanningCallback {
|
||||
void routeResult(List<LatLng> models);
|
||||
void routePlanningToNextStationChanged(long meters, long timeInSecond);
|
||||
void setLineMarker(List<LatLng> models);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class BusPassengerConst {
|
||||
|
||||
// 轮询line
|
||||
const val LOOP_LINE_2S = 2 * 1000L
|
||||
const val LOOP_LINE_1S = 1 * 1000L
|
||||
const val LOOP_DELAY = 100L
|
||||
|
||||
// 无状态
|
||||
|
||||
@@ -349,13 +349,23 @@ public class BusPassengerModel {
|
||||
public void onAutopilotRotting(@Nullable MessagePad.GlobalPathResp routeList) {
|
||||
CallerLogger.INSTANCE.d(M_BUS_P + TAG, "onAutopilotRotting = "
|
||||
+ GsonUtil.jsonFromObject(routeList));
|
||||
List<MessagePad.Location> mRoutePoints = routeList.getWayPointsList();
|
||||
if (null != routeList && mRoutePoints.size() > 0){
|
||||
updateRoutePoints(mRoutePoints);
|
||||
List<MessagePad.Location> routePoints = routeList.getWayPointsList();
|
||||
if (null != routePoints && routePoints.size() > 0){
|
||||
updateRoutePoints(routePoints);
|
||||
startRemainRouteInfo();
|
||||
setRouteLineMarker();
|
||||
startToRouteAndWipe();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void updateRoutePoints(List<MessagePad.Location> routePoints){
|
||||
mRoutePoints.clear();
|
||||
List<LatLng> latLngModels = CoordinateCalculateRouteUtil
|
||||
.coordinateConverterWgsToGcjListCommon(mContext,routePoints);
|
||||
mRoutePoints.addAll(latLngModels);
|
||||
}
|
||||
|
||||
public void dynamicCalculateRouteInfo() {
|
||||
List<LatLng> lastPoints = CoordinateCalculateRouteUtil
|
||||
.getRemainPointListByCompare(mRoutePoints,mLongitude,mLatitude);
|
||||
@@ -376,20 +386,46 @@ public class BusPassengerModel {
|
||||
|
||||
}
|
||||
|
||||
public void updateRoutePoints(List<MessagePad.Location> routePoints) {
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.routeResult(
|
||||
CoordinateCalculateRouteUtil.coordinateConverterWgsToGcjListCommon(mContext
|
||||
, routePoints));
|
||||
}
|
||||
|
||||
//转换成高德坐标系
|
||||
mRoutePoints.clear();
|
||||
mRoutePoints.addAll(CoordinateCalculateRouteUtil.coordinateConverterWgsToGcjListCommon(mContext,routePoints));
|
||||
public void startRemainRouteInfo() {
|
||||
//开启实时计算剩余距离,剩余时间,预计时间
|
||||
startOrStopCalculateRouteInfo(true);
|
||||
}
|
||||
|
||||
public void startToRouteAndWipe() {
|
||||
startOrStopRouteAndWipe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时轨迹擦除
|
||||
* @param isStart
|
||||
*/
|
||||
public void startOrStopRouteAndWipe(boolean isStart){
|
||||
if (isStart){
|
||||
BusPassengerModelLoopManager.getInstance().startOrStopRouteAndWipe();
|
||||
}else {
|
||||
BusPassengerModelLoopManager.getInstance().stopOrStopRouteAndWipe();
|
||||
}
|
||||
}
|
||||
|
||||
public void loopRouteAndWipe() {
|
||||
if (mRoutePoints != null && mRoutePoints.size() > 0){
|
||||
List<LatLng> lastPoints = CoordinateCalculateRouteUtil
|
||||
.getRemainPointListByCompare(mRoutePoints,mLongitude,mLatitude);
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.routeResult(lastPoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置小地图路径的起终点marker
|
||||
*/
|
||||
public void setRouteLineMarker(){
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.setLineMarker(mRoutePoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始轮询计算剩余里程和时间
|
||||
* @param isStart
|
||||
|
||||
@@ -13,6 +13,7 @@ import io.reactivex.schedulers.Schedulers;
|
||||
import static com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.M_BUS_P;
|
||||
import static com.mogo.och.bus.passenger.constant.BusPassengerConst.LOOP_DELAY;
|
||||
import static com.mogo.och.bus.passenger.constant.BusPassengerConst.LOOP_LINE_2S;
|
||||
import static com.mogo.och.bus.passenger.constant.BusPassengerConst.LOOP_LINE_1S;
|
||||
|
||||
/**
|
||||
* Created on 2021/11/22
|
||||
@@ -34,6 +35,28 @@ public class BusPassengerModelLoopManager {
|
||||
}
|
||||
|
||||
private Disposable mHeartbeatDisposable; //心跳轮询
|
||||
private Disposable mRouteWipeDisposable; //轨迹擦除
|
||||
|
||||
public void startOrStopRouteAndWipe() {
|
||||
if (mRouteWipeDisposable != null && !mRouteWipeDisposable.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
CallerLogger.INSTANCE.i(M_BUS_P + TAG, "startOrStopRouteWipe()");
|
||||
mRouteWipeDisposable = Observable.interval(LOOP_DELAY,
|
||||
LOOP_LINE_1S, TimeUnit.MILLISECONDS)
|
||||
.map((aLong -> aLong + 1))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(aLong -> BusPassengerModel.getInstance().loopRouteAndWipe());
|
||||
}
|
||||
|
||||
public void stopOrStopRouteAndWipe() {
|
||||
if (mRouteWipeDisposable != null) {
|
||||
CallerLogger.INSTANCE.i(M_BUS_P + TAG, "stopOrStopRouteWipe()");
|
||||
mRouteWipeDisposable.dispose();
|
||||
mRouteWipeDisposable = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void startQueryDriverLineLoop() {
|
||||
if (mHeartbeatDisposable != null && !mHeartbeatDisposable.isDisposed()) {
|
||||
|
||||
@@ -144,4 +144,9 @@ public class BaseBusPassengerPresenter extends Presenter<BusPassengerRouteFragme
|
||||
public void routePlanningToNextStationChanged(long meters, long timeInSecond) {
|
||||
runOnUIThread(() -> mView.updateRoutePlanningToNextStation(meters, timeInSecond));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLineMarker(List<LatLng> models) {
|
||||
runOnUIThread(() -> mView.setLineMarker(models));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,23 +208,18 @@ public class BusPassengerMapDirectionView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void drawablePolyline() {
|
||||
clearPolyline();
|
||||
if (mPolyline != null) {
|
||||
mPolyline.remove();
|
||||
}
|
||||
if (mAMap != null) {
|
||||
|
||||
addRouteColorList();
|
||||
|
||||
if (mCoordinatesLatLng.size() > 2) {
|
||||
// 设置开始结束Marker位置
|
||||
|
||||
LatLng startLatLng = mCoordinatesLatLng.get(0);
|
||||
LatLng endLatLng = mCoordinatesLatLng.get(mCoordinatesLatLng.size() - 1);
|
||||
|
||||
mStartMarker.setPosition(startLatLng);
|
||||
mEndMarker.setPosition(endLatLng);
|
||||
mStartMarker.setVisible(true);
|
||||
mEndMarker.setVisible(true);
|
||||
|
||||
//设置线段纹理
|
||||
PolylineOptions polylineOptions = new PolylineOptions();
|
||||
@@ -263,6 +258,27 @@ public class BusPassengerMapDirectionView
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLineMarker() {
|
||||
if (mStartMarker != null) {
|
||||
mStartMarker.setVisible(false);
|
||||
}
|
||||
if (mEndMarker != null) {
|
||||
mEndMarker.setVisible(false);
|
||||
}
|
||||
if (mCoordinatesLatLng.size() > 2) {
|
||||
// 设置开始结束Marker位置
|
||||
|
||||
LatLng startLatLng = mCoordinatesLatLng.get(0);
|
||||
LatLng endLatLng = mCoordinatesLatLng.get(mCoordinatesLatLng.size() - 1);
|
||||
|
||||
mStartMarker.setPosition(startLatLng);
|
||||
mEndMarker.setPosition(endLatLng);
|
||||
mStartMarker.setVisible(true);
|
||||
mEndMarker.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearCoordinatesLatLng(){
|
||||
mCoordinatesLatLng.clear();
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.amap.api.maps.model.LatLng;
|
||||
import com.elegant.utils.UiThreadHandler;
|
||||
import com.mogo.eagle.core.function.call.hmi.CallerHmiManager;
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger;
|
||||
import com.mogo.eagle.core.utilcode.util.UiThreadHandler;
|
||||
import com.mogo.och.bus.passenger.R;
|
||||
import com.mogo.och.bus.passenger.adapter.BusPassengerLineStationsAdapter;
|
||||
import com.mogo.och.bus.passenger.bean.BusPassengerStation;
|
||||
@@ -148,6 +148,22 @@ public class BusPassengerRouteFragment extends
|
||||
}
|
||||
}
|
||||
|
||||
public void setLineMarker(List<LatLng> latLngList){
|
||||
if (latLngList.size() > 0) {
|
||||
if (mMapDirectionView != null) {
|
||||
mMapDirectionView.setCoordinatesLatLng(latLngList);
|
||||
UiThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mMapDirectionView.setLineMarker();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
clearPolyline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制
|
||||
*
|
||||
|
||||
@@ -15,4 +15,9 @@ public interface IBusPassengerMapDirectionView {
|
||||
* 清除路径线
|
||||
*/
|
||||
void clearPolyline();
|
||||
|
||||
/**
|
||||
* 设置路径中起终点marker
|
||||
*/
|
||||
void setLineMarker();
|
||||
}
|
||||
|
||||
@@ -25,182 +25,6 @@ public class BPRouteDataTestUtils {
|
||||
" ]\n" +
|
||||
"}";
|
||||
|
||||
//13号路口西-汇源果汁
|
||||
// static String jsonStr = "{\"models\":[{\n" +
|
||||
// "\t\t\"lat\": 40.19927810144466,\n" +
|
||||
// "\t\t\"lon\": 116.73527259387767\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19927836356079,\n" +
|
||||
// "\t\t\"lon\": 116.73513114732762\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19927759500293,\n" +
|
||||
// "\t\t\"lon\": 116.73497660879111\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.199264819842284,\n" +
|
||||
// "\t\t\"lon\": 116.73480063747202\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1992510141554,\n" +
|
||||
// "\t\t\"lon\": 116.73463922037767\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.199245872804,\n" +
|
||||
// "\t\t\"lon\": 116.73445960685193\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19924673374912,\n" +
|
||||
// "\t\t\"lon\": 116.73427704009703\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19924747108264,\n" +
|
||||
// "\t\t\"lon\": 116.7340707102972\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19924828745573,\n" +
|
||||
// "\t\t\"lon\": 116.73385916927226\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19924941093133,\n" +
|
||||
// "\t\t\"lon\": 116.73364048294795\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19924939253381,\n" +
|
||||
// "\t\t\"lon\": 116.73340837408566\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19924949105934,\n" +
|
||||
// "\t\t\"lon\": 116.73317368725336\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19925040039033,\n" +
|
||||
// "\t\t\"lon\": 116.73296532811216\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1992515355653,\n" +
|
||||
// "\t\t\"lon\": 116.73277787366743\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1992512720328,\n" +
|
||||
// "\t\t\"lon\": 116.73263377253741\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.199205174954606,\n" +
|
||||
// "\t\t\"lon\": 116.73249773114644\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1991015743076,\n" +
|
||||
// "\t\t\"lon\": 116.7324219601283\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.198971862686285,\n" +
|
||||
// "\t\t\"lon\": 116.73239393296355\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19883883071582,\n" +
|
||||
// "\t\t\"lon\": 116.73237676435652\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19870171355796,\n" +
|
||||
// "\t\t\"lon\": 116.73236052150362\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1985491853193,\n" +
|
||||
// "\t\t\"lon\": 116.73234157857011\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1983890047355,\n" +
|
||||
// "\t\t\"lon\": 116.73232167996464\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1982209877466,\n" +
|
||||
// "\t\t\"lon\": 116.73230101645792\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.198037574138326,\n" +
|
||||
// "\t\t\"lon\": 116.73227735486083\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19787327856243,\n" +
|
||||
// "\t\t\"lon\": 116.73225676816314\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19771917207499,\n" +
|
||||
// "\t\t\"lon\": 116.73223814728027\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.197548305175935,\n" +
|
||||
// "\t\t\"lon\": 116.73221624705808\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19739568979691,\n" +
|
||||
// "\t\t\"lon\": 116.73219618210774\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19724703821575,\n" +
|
||||
// "\t\t\"lon\": 116.73217598293311\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1970956560885,\n" +
|
||||
// "\t\t\"lon\": 116.73215773721505\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19697703483188,\n" +
|
||||
// "\t\t\"lon\": 116.73214337172284\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19687000725696,\n" +
|
||||
// "\t\t\"lon\": 116.73210037067965\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.196833449601726,\n" +
|
||||
// "\t\t\"lon\": 116.73196646708011\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19685833847804,\n" +
|
||||
// "\t\t\"lon\": 116.73181315361103\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.196889170203264,\n" +
|
||||
// "\t\t\"lon\": 116.73164355747393\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19692242860347,\n" +
|
||||
// "\t\t\"lon\": 116.7314555399657\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19696431701069,\n" +
|
||||
// "\t\t\"lon\": 116.7312261834129\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19700025925464,\n" +
|
||||
// "\t\t\"lon\": 116.73102774016093\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19703414798773,\n" +
|
||||
// "\t\t\"lon\": 116.73084270562073\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19707287604138,\n" +
|
||||
// "\t\t\"lon\": 116.73062835248406\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19710951629977,\n" +
|
||||
// "\t\t\"lon\": 116.73041744082339\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19714593807105,\n" +
|
||||
// "\t\t\"lon\": 116.73021414314803\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.197183297026285,\n" +
|
||||
// "\t\t\"lon\": 116.7300057066447\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.1972247359487,\n" +
|
||||
// "\t\t\"lon\": 116.7297751515664\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19726518822745,\n" +
|
||||
// "\t\t\"lon\": 116.72954958923812\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19730538240706,\n" +
|
||||
// "\t\t\"lon\": 116.72932440756041\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19734272112662,\n" +
|
||||
// "\t\t\"lon\": 116.72911631453036\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.197379191549075,\n" +
|
||||
// "\t\t\"lon\": 116.72890982812105\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.197417565369314,\n" +
|
||||
// "\t\t\"lon\": 116.72869447869044\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19746052080799,\n" +
|
||||
// "\t\t\"lon\": 116.72845641541247\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19750040582118,\n" +
|
||||
// "\t\t\"lon\": 116.72823569991117\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19753999704064,\n" +
|
||||
// "\t\t\"lon\": 116.72801998373052\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19757796882569,\n" +
|
||||
// "\t\t\"lon\": 116.72781280504363\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.197617062364586,\n" +
|
||||
// "\t\t\"lon\": 116.72759949431683\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19765391602761,\n" +
|
||||
// "\t\t\"lon\": 116.72739776789756\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19768973009218,\n" +
|
||||
// "\t\t\"lon\": 116.72719980764646\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.197726191028785,\n" +
|
||||
// "\t\t\"lon\": 116.72699719861669\n" +
|
||||
// "\t}, {\n" +
|
||||
// "\t\t\"lat\": 40.19776233489642,\n" +
|
||||
// "\t\t\"lon\": 116.72679516155276\n" +
|
||||
// "\t}]}\n";
|
||||
public static void converToRouteData(){
|
||||
List<MessagePad.Location> list = new ArrayList<>();
|
||||
|
||||
@@ -215,6 +39,9 @@ public class BPRouteDataTestUtils {
|
||||
list.add(builder.build());
|
||||
}
|
||||
BusPassengerModel.getInstance().updateRoutePoints(list);
|
||||
BusPassengerModel.getInstance().startRemainRouteInfo();
|
||||
BusPassengerModel.getInstance().setRouteLineMarker();
|
||||
BusPassengerModel.getInstance().startToRouteAndWipe();
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -77,10 +77,10 @@ public class BusFragment extends BaseBusTabFragment<BusFragment, BusPresenter>
|
||||
mPresenter.queryBusRoutes();
|
||||
});
|
||||
|
||||
mBus.setOnLongClickListener(view -> {
|
||||
getActivity().finish();
|
||||
return true;
|
||||
});
|
||||
// mBus.setOnLongClickListener(view -> {
|
||||
// getActivity().finish();
|
||||
// return true;
|
||||
// });
|
||||
//debug下调用测试面板
|
||||
mCurrentStationName.setOnLongClickListener(v -> {
|
||||
debugTestBar();
|
||||
@@ -288,7 +288,6 @@ public class BusFragment extends BaseBusTabFragment<BusFragment, BusPresenter>
|
||||
// 出车的时候重制站点状态
|
||||
mPresenter.queryBusRoutes();
|
||||
tvOperationStatus.setText("收车");
|
||||
showSlidePanle("滑动出发");
|
||||
showPanel();
|
||||
} else {
|
||||
AIAssist.getInstance(getContext()).speakTTSVoice("已收车");
|
||||
|
||||
@@ -11,6 +11,6 @@ import mogo.telematics.pad.MessagePad;
|
||||
* @date: 2021/11/1
|
||||
*/
|
||||
public interface IOCHTaxiPassengerAutopilotPlanningCallback {
|
||||
void routeResult(List<MessagePad.Location> models);
|
||||
void setLineMarker(List<LatLng> models);
|
||||
void routeResultByServer(List<LatLng> models);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.mogo.cloud.commons.utils.CoordinateUtils;
|
||||
import com.mogo.commons.debug.DebugConfig;
|
||||
import com.mogo.eagle.core.data.autopilot.AutopilotStatusInfo;
|
||||
import com.mogo.eagle.core.data.config.FunctionBuildConfig;
|
||||
import com.mogo.eagle.core.data.map.MogoLatLng;
|
||||
import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotPlanningListener;
|
||||
import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotStatusListener;
|
||||
import com.mogo.eagle.core.function.api.v2x.LimitingVelocityListener;
|
||||
@@ -57,6 +58,7 @@ import com.mogo.service.statusmanager.StatusDescriptor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -104,6 +106,8 @@ public class TaxiPassengerModel implements IOCHTaxiPassengerNaviChangedCallback
|
||||
|
||||
private double mLongitude, mLatitude;
|
||||
|
||||
private List<LatLng> mLocationsModels = new ArrayList<>();
|
||||
|
||||
private TaxiPassengerModel() {
|
||||
}
|
||||
|
||||
@@ -572,12 +576,54 @@ public class TaxiPassengerModel implements IOCHTaxiPassengerNaviChangedCallback
|
||||
public void onAutopilotRotting(@Nullable MessagePad.GlobalPathResp routeList) {
|
||||
if (null != routeList && routeList.getWayPointsList().size() > 0){
|
||||
calculateRouteLineSum(CoordinateCalculateRouteUtil.coordinateConverterWgsToGcjListCommon(mContext,routeList.getWayPointsList()));
|
||||
updateRouteResult(routeList.getWayPointsList());
|
||||
setRouteLineMarker(routeList.getWayPointsList());
|
||||
startToRouteAndWipe(routeList.getWayPointsList());
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public void startToRouteAndWipe(List<MessagePad.Location> models) {
|
||||
List<LatLng> latLngModels = CoordinateCalculateRouteUtil
|
||||
.coordinateConverterWgsToGcjListCommon(mContext,models);
|
||||
mLocationsModels.clear();
|
||||
mLocationsModels.addAll(latLngModels);
|
||||
startOrStopRouteAndWipe(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时轨迹擦除
|
||||
* @param isStart
|
||||
*/
|
||||
public void startOrStopRouteAndWipe(boolean isStart){
|
||||
if (isStart){
|
||||
TaxiPassengerModelLoopManager.getInstance().startOrStopRouteAndWipe();
|
||||
}else {
|
||||
TaxiPassengerModelLoopManager.getInstance().stopOrStopRouteAndWipe();
|
||||
}
|
||||
}
|
||||
|
||||
public void loopRouteAndWipe() {
|
||||
if (mLocationsModels != null && mLocationsModels.size() > 0){
|
||||
List<LatLng> lastPoints = CoordinateCalculateRouteUtil
|
||||
.getRemainPointListByCompare(mLocationsModels,mLongitude,mLatitude);
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.routeResultByServer(lastPoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置小地图路径的起终点marker
|
||||
*/
|
||||
public void setRouteLineMarker(List<MessagePad.Location> models){
|
||||
List<LatLng> latLngModels = CoordinateCalculateRouteUtil
|
||||
.coordinateConverterWgsToGcjListCommon(mContext,models);
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.setLineMarker(latLngModels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 限速监听
|
||||
*/
|
||||
@@ -591,11 +637,6 @@ public class TaxiPassengerModel implements IOCHTaxiPassengerNaviChangedCallback
|
||||
}
|
||||
};
|
||||
|
||||
public void updateRouteResult(List<MessagePad.Location> models){
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.routeResult(models);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 导航订单起点到终点 获得剩余时间,里程,预计到达时间
|
||||
*/
|
||||
@@ -648,7 +689,12 @@ public class TaxiPassengerModel implements IOCHTaxiPassengerNaviChangedCallback
|
||||
if (data != null && data.data != null && data.data != null && data.data.size() > 0){
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
calculateRouteLineSum(data.data);
|
||||
mAutopilotPlanningCallback.routeResultByServer(data.data);
|
||||
if (mAutopilotPlanningCallback != null){
|
||||
mAutopilotPlanningCallback.setLineMarker(data.data);
|
||||
}
|
||||
mLocationsModels.clear();
|
||||
mLocationsModels.addAll(data.data);
|
||||
startOrStopRouteAndWipe(true);
|
||||
}
|
||||
}else {
|
||||
queryOrderRouteList();
|
||||
|
||||
@@ -31,6 +31,28 @@ public class TaxiPassengerModelLoopManager {
|
||||
|
||||
private Disposable mInAndWaitServiceDisposable; //进行中、待服务订单列表轮询
|
||||
private Disposable mQueryOrderRemainingDisposable; //心跳轮询
|
||||
private Disposable mRouteWipeDisposable; //轨迹擦除
|
||||
|
||||
public void startOrStopRouteAndWipe() {
|
||||
if (mRouteWipeDisposable != null && !mRouteWipeDisposable.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
CallerLogger.INSTANCE.i(M_TAXI_P + TAG, "startOrStopRouteWipe()");
|
||||
mRouteWipeDisposable = Observable.interval(TaxiPassengerConst.LOOP_DELAY,
|
||||
TaxiPassengerConst.LOOP_PERIOD_1S, TimeUnit.MILLISECONDS)
|
||||
.map((aLong -> aLong + 1))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(aLong -> TaxiPassengerModel.getInstance().loopRouteAndWipe());
|
||||
}
|
||||
|
||||
public void stopOrStopRouteAndWipe() {
|
||||
if (mRouteWipeDisposable != null) {
|
||||
CallerLogger.INSTANCE.i(M_TAXI_P + TAG, "stopOrStopRouteWipe()");
|
||||
mRouteWipeDisposable.dispose();
|
||||
mRouteWipeDisposable = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void startInAndWaitOrdersLoop() {
|
||||
if (mInAndWaitServiceDisposable != null && !mInAndWaitServiceDisposable.isDisposed()) {
|
||||
|
||||
@@ -69,15 +69,12 @@ public class TaxiPassengerServingOrderPresenter extends Presenter<TaxiPassengerS
|
||||
}
|
||||
|
||||
@Override
|
||||
public void routeResult(List<MessagePad.Location> models) {
|
||||
public void setLineMarker(List<LatLng> models) {
|
||||
if (models == null) return;
|
||||
List<MogoLatLng> latLngList = new ArrayList<>();
|
||||
for (MessagePad.Location routeModel : models) {
|
||||
latLngList.add(new MogoLatLng(routeModel.getLatitude(), routeModel.getLongitude()));
|
||||
}
|
||||
runOnUIThread(() -> mView.routeResult(latLngList));
|
||||
runOnUIThread(() -> mView.setLineMarker(models));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void routeResultByServer(List<LatLng> models) {
|
||||
if (models == null) return;
|
||||
|
||||
@@ -15,4 +15,9 @@ public interface ITaxiPassengerMapDirectionView {
|
||||
* 清除路径线
|
||||
*/
|
||||
void clearPolyline();
|
||||
|
||||
/**
|
||||
* 设置路径中起终点marker
|
||||
*/
|
||||
void setLineMarker();
|
||||
}
|
||||
|
||||
@@ -230,6 +230,14 @@ public class TaxiPassengerBaseFragment extends MvpFragment<TaxiPassengerBaseFrag
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
removeListener();
|
||||
}
|
||||
|
||||
private void removeListener(){
|
||||
if (mStartAutopilotView == null || mStartAutopilotView.get() == null){
|
||||
return;
|
||||
}
|
||||
mStartAutopilotView.get().setOnClickStartAutopilotBtnCallback(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -290,6 +298,7 @@ public class TaxiPassengerBaseFragment extends MvpFragment<TaxiPassengerBaseFrag
|
||||
if (mStartAutopilotView == null || mStartAutopilotView.get() == null){
|
||||
return;
|
||||
}
|
||||
mStartAutopilotView.get().setOnClickStartAutopilotBtnCallback(null);
|
||||
OverlayViewUtils.dismissOverlayView(mStartAutopilotView.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,24 +205,36 @@ public class TaxiPassengerMapDirectionView
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLineMarker() {
|
||||
if (mStartMarker != null) {
|
||||
mStartMarker.setVisible(false);
|
||||
}
|
||||
if (mEndMarker != null) {
|
||||
mEndMarker.setVisible(false);
|
||||
}
|
||||
if (mCoordinatesLatLng.size() > 2) {
|
||||
// 设置开始结束Marker位置
|
||||
LatLng startLatLng = mCoordinatesLatLng.get(0);
|
||||
LatLng endLatLng = mCoordinatesLatLng.get(mCoordinatesLatLng.size() - 1);
|
||||
|
||||
mStartMarker.setPosition(startLatLng);
|
||||
mEndMarker.setPosition(endLatLng);
|
||||
mStartMarker.setVisible(true);
|
||||
mEndMarker.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawablePolyline() {
|
||||
clearPolyline();
|
||||
if (mPolyline != null) {
|
||||
mPolyline.remove();
|
||||
}
|
||||
if (mAMap != null) {
|
||||
|
||||
addRouteColorList();
|
||||
|
||||
if (mCoordinatesLatLng.size() > 2) {
|
||||
// 设置开始结束Marker位置
|
||||
|
||||
LatLng startLatLng = mCoordinatesLatLng.get(0);
|
||||
LatLng endLatLng = mCoordinatesLatLng.get(mCoordinatesLatLng.size() - 1);
|
||||
|
||||
mStartMarker.setPosition(startLatLng);
|
||||
mEndMarker.setPosition(endLatLng);
|
||||
mStartMarker.setVisible(true);
|
||||
mEndMarker.setVisible(true);
|
||||
|
||||
//设置线段纹理
|
||||
PolylineOptions polylineOptions = new PolylineOptions();
|
||||
polylineOptions.addAll(mCoordinatesLatLng);
|
||||
@@ -273,7 +285,6 @@ public class TaxiPassengerMapDirectionView
|
||||
|
||||
@Override
|
||||
public void clearPolyline() {
|
||||
// mCoordinatesLatLng.clear();
|
||||
if (mPolyline != null) {
|
||||
mPolyline.remove();
|
||||
}
|
||||
|
||||
@@ -208,10 +208,17 @@ public class TaxiPassengerServingOrderFragment extends
|
||||
TaxiPassengerModel.getInstance().destoryGeocodeSearch();
|
||||
}
|
||||
|
||||
public void routeResult(List<MogoLatLng> latLngList) {
|
||||
CallerLogger.INSTANCE.d(M_TAXI_P + TAG, "routeResult:" + latLngList.size());
|
||||
public void setLineMarker(List<LatLng> latLngList){
|
||||
if (latLngList.size() > 0) {
|
||||
drawablePolyline(latLngList);
|
||||
if (mMapDirectionView != null) {
|
||||
mMapDirectionView.setCoordinatesLatLng(latLngList);
|
||||
UiThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mMapDirectionView.setLineMarker();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
clearPolyline();
|
||||
}
|
||||
@@ -226,23 +233,6 @@ public class TaxiPassengerServingOrderFragment extends
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制
|
||||
*
|
||||
* @param coordinates
|
||||
*/
|
||||
private void drawablePolyline(List<MogoLatLng> coordinates) {
|
||||
if (mMapDirectionView != null) {
|
||||
mMapDirectionView.convert(coordinates);
|
||||
UiThreadHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mMapDirectionView.drawablePolyline();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void drawablePolylineByServerRoute(List<LatLng> mCoordinatesLatLng){
|
||||
if (mMapDirectionView != null){
|
||||
mMapDirectionView.setCoordinatesLatLng(mCoordinatesLatLng);
|
||||
|
||||
@@ -37,7 +37,8 @@ public class TPRouteDataTestUtils {
|
||||
builder.setLongitude(s.getDouble("lon"));
|
||||
list.add(builder.build());
|
||||
}
|
||||
TaxiPassengerModel.getInstance().updateRouteResult(list);
|
||||
TaxiPassengerModel.getInstance().setRouteLineMarker(list);
|
||||
TaxiPassengerModel.getInstance().startToRouteAndWipe(list);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ class RxJavaBackPressureTest {
|
||||
@Test
|
||||
fun testIntervalBackPressure() = runBlocking(Dispatchers.Default) {
|
||||
val subscription = Flowable.interval(50, MILLISECONDS).doOnNext {
|
||||
Log.d("RxJava2", "-- do action --")
|
||||
}.subscribeOn(Schedulers.computation()).observeOn(Schedulers.io()).subscribe {
|
||||
Thread.sleep(2000)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ import com.zhidao.support.adas.high.OnAdasConnectStatusListener;
|
||||
import com.zhidao.support.adas.high.OnAdasListener;
|
||||
import com.zhidao.support.adas.high.OnMultiDeviceListener;
|
||||
import com.zhidao.support.adas.high.bean.VersionCompatibility;
|
||||
import com.zhidao.support.adas.high.common.ByteUtil;
|
||||
import com.zhidao.support.adas.high.common.Constants.IPC_CONNECTION_STATUS;
|
||||
import com.zhidao.support.adas.high.common.CupidLogUtils;
|
||||
import com.zhidao.support.adas.high.common.ProtocolStatus;
|
||||
@@ -884,7 +885,7 @@ public class MainActivity extends BaseActivity implements OnAdasListener, OnAdas
|
||||
|
||||
@Override
|
||||
public void onMessageResponseClient(MogoProtocolMsg msg, String sign, Channel channel) {
|
||||
Log.i("ddd", "dddd" + sign);
|
||||
Log.i(TAG, "司机端连接成功=" + sign);
|
||||
AdasManager.getInstance().decoderRaw(msg.getBody());
|
||||
}
|
||||
|
||||
@@ -892,8 +893,10 @@ public class MainActivity extends BaseActivity implements OnAdasListener, OnAdas
|
||||
public void onClientStatusConnectChanged(int statusCode, String sign, Channel channel) {
|
||||
if (statusCode == ConnectState.STATUS_CONNECT_SUCCESS) {
|
||||
connectStatus = IPC_CONNECTION_STATUS.CONNECTED;
|
||||
AdasManager.getInstance().startDispatchHandler();
|
||||
} else {
|
||||
connectStatus = IPC_CONNECTION_STATUS.DISCONNECTED;
|
||||
AdasManager.getInstance().stopDispatchHandler();
|
||||
}
|
||||
getHandler().sendEmptyMessage(WHAT_DRIVER_IP);
|
||||
onUpdateConnectStateView();
|
||||
@@ -915,6 +918,8 @@ public class MainActivity extends BaseActivity implements OnAdasListener, OnAdas
|
||||
NSDNettyManager.getInstance().startNSDNettyServerWithSN(this, new NettyServerListener<MogoProtocolMsg>() {
|
||||
@Override
|
||||
public void onMessageResponseServer(MogoProtocolMsg msg, Channel channel) {
|
||||
AdasManager.getInstance().sendWsMessage(msg.getBody());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -937,32 +942,27 @@ public class MainActivity extends BaseActivity implements OnAdasListener, OnAdas
|
||||
Log.i(TAG, "onChannelDisConnect channel=" + channel.id());
|
||||
}
|
||||
}, "1234567");
|
||||
|
||||
|
||||
}
|
||||
AdasManager.getInstance().create(options, this);
|
||||
|
||||
AdasManager.getInstance().setOnAdasListener(this);
|
||||
if (BuildConfig.IS_CLIENT) {
|
||||
/*—————————————作为乘客端———————————*/
|
||||
|
||||
} else {
|
||||
/*—————————————作为司機端———————————*/
|
||||
AdasManager.getInstance().setOnMultiDeviceListener(new OnMultiDeviceListener() {
|
||||
@Override
|
||||
public void onForwardingIPCMessage(byte[] bytes) {
|
||||
// 发送数据给乘客端
|
||||
if (NSDNettyManager.getInstance().isServerStart()) {
|
||||
NSDNettyManager.getInstance().sendMsgToAllClients(new MogoProtocolMsg(NORMAL_DATA, bytes.length, bytes));
|
||||
} else {
|
||||
AdasManager.getInstance().setOnMultiDeviceListener(new OnMultiDeviceListener() {
|
||||
@Override
|
||||
public void onForwardingDriverIPCMessage(byte[] bytes) {
|
||||
// 发送数据给乘客端
|
||||
if (NSDNettyManager.getInstance().isServerStart()) {
|
||||
NSDNettyManager.getInstance().sendMsgToAllClients(new MogoProtocolMsg(NORMAL_DATA, bytes.length, bytes));
|
||||
} else {
|
||||
// Log.d("dddd", "司机端Server未启动!");
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onForwardingPassengerIPCMessage(byte[] bytes) {
|
||||
NSDNettyManager.getInstance()
|
||||
.sendMogoProtocolMsgToServer(new MogoProtocolMsg(NORMAL_DATA, bytes.length, bytes), null);
|
||||
Log.i(TAG, "乘客屏发送数据=" + ByteUtil.byteArrToHex(bytes));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1059,7 +1059,11 @@ public class MainActivity extends BaseActivity implements OnAdasListener, OnAdas
|
||||
@Override
|
||||
public void onItemClick(int position, String data) {
|
||||
if (connectStatus != IPC_CONNECTION_STATUS.CONNECTED) {
|
||||
showToastCenter("IPC 未连接");
|
||||
String msg = "未连接工控机";
|
||||
if (BuildConfig.IS_CLIENT) {
|
||||
msg = "未连接司机端";
|
||||
}
|
||||
showToastCenter(msg);
|
||||
return;
|
||||
}
|
||||
switch (data) {
|
||||
|
||||
@@ -28,7 +28,8 @@ ext {
|
||||
androidxcardview : "androidx.cardview:cardview:1.0.0",
|
||||
localbroadcastmanager : "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0",
|
||||
// flexbox
|
||||
flexbox : 'com.google.android.flexbox:flexbox:3.0.0',
|
||||
flexbox : 'com.google.android.flexbox:flexbox:3.0.0',
|
||||
guava :'com.google.guava:guava:29.0-android',
|
||||
// 测试
|
||||
junit : "junit:junit:4.12",
|
||||
androidxjunit : "androidx.test.ext:junit:1.1.2",
|
||||
|
||||
@@ -47,6 +47,7 @@ import com.mogo.telematic.server.netty.NettyServerListener
|
||||
import com.mogo.telematic.server.netty.NettyTcpServer
|
||||
import com.zhidao.support.adas.high.AdasManager
|
||||
import com.zhidao.support.adas.high.AdasOptions
|
||||
import com.zhidao.support.adas.high.OnMultiDeviceListener
|
||||
import com.zhidao.support.adas.high.common.Constants
|
||||
import com.zhidao.support.adas.high.common.Constants.IPC_CONNECTION_STATUS
|
||||
import com.zhidao.support.adas.high.common.CupidLogUtils
|
||||
@@ -155,9 +156,14 @@ class MoGoAutopilotProvider :
|
||||
|
||||
// 监听ADAS-SDK获取到的工控机数据(乘客也需注册)
|
||||
AdasManager.getInstance().setOnAdasListener(MoGoAdasListenerImpl())
|
||||
// 司机端监听
|
||||
if (AppIdentityModeUtils.isDriver(FunctionBuildConfig.appIdentityMode)) {
|
||||
AdasManager.getInstance().setOnMultiDeviceListener { bytes ->
|
||||
// 乘客屏监听工控机基础信息回调
|
||||
if (!AppIdentityModeUtils.isDriver(FunctionBuildConfig.appIdentityMode)) {
|
||||
CallerAutopilotCarConfigListenerManager.addListener(TAG, this)
|
||||
}
|
||||
AdasManager.getInstance().setOnMultiDeviceListener(object : OnMultiDeviceListener {
|
||||
override fun onForwardingDriverIPCMessage(bytes: ByteArray?) {
|
||||
if (bytes == null)
|
||||
return
|
||||
// 发送数据给乘客端
|
||||
if (NSDNettyManager.getInstance().isServerStart) {
|
||||
msgHandler.synWriteTime()
|
||||
@@ -167,9 +173,19 @@ class MoGoAutopilotProvider :
|
||||
CallerLogger.d("$M_ADAS_IMPL$TAG", "司机端Server未启动!")
|
||||
}
|
||||
}
|
||||
} else {// 乘客屏监听工控机基础信息回调
|
||||
CallerAutopilotCarConfigListenerManager.addListener(TAG, this)
|
||||
}
|
||||
|
||||
override fun onForwardingPassengerIPCMessage(bytes: ByteArray?) {
|
||||
if (bytes == null)
|
||||
return
|
||||
NSDNettyManager.getInstance()
|
||||
.sendMogoProtocolMsgToServer(
|
||||
MogoProtocolMsg(NORMAL_DATA, bytes.size, bytes),
|
||||
null
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CallerLogger.i("$M_ADAS_IMPL$TAG", "initServer……")
|
||||
// 同步数据给工控机的服务
|
||||
@@ -230,7 +246,8 @@ class MoGoAutopilotProvider :
|
||||
|
||||
override fun startAutoPilot(controlParameters: AutopilotControlParameters) {
|
||||
if (AdasManager.getInstance().ipcConnectionStatus == IPC_CONNECTION_STATUS.CONNECTED) {
|
||||
val invokeResult = AdasManager.getInstance().sendAutoPilotModeReq(1, 1, controlParameters.toRouteInfo())
|
||||
val invokeResult = AdasManager.getInstance()
|
||||
.sendAutoPilotModeReq(1, 1, controlParameters.toRouteInfo())
|
||||
invokeAutoPilotResult(if (invokeResult) "自动驾驶调用成功" else "自动驾驶调用失败, socket 或者 rawPack 可能为空")
|
||||
} else {
|
||||
invokeAutoPilotResult("车机与工控机链接失败,无法开启自动驾驶")
|
||||
@@ -364,9 +381,9 @@ class MoGoAutopilotProvider :
|
||||
* isEnable = false 关闭
|
||||
*/
|
||||
override fun setRainMode(isEnable: Boolean) {
|
||||
if(isEnable){
|
||||
if (isEnable) {
|
||||
AdasManager.getInstance().sendRainModeReq(1)
|
||||
}else{
|
||||
} else {
|
||||
AdasManager.getInstance().sendRainModeReq(0)
|
||||
}
|
||||
}
|
||||
@@ -464,7 +481,8 @@ class MoGoAutopilotProvider :
|
||||
// 乘客屏才监听
|
||||
AppConfigInfo.plateNumber = carConfigResp.plateNumber
|
||||
Log.d("liyz", "onAutopilotCarConfig 乘客屏Mac地址为 = ${carConfigResp.macAddress}")
|
||||
CallerBindingcarManager.getBindingcarProvider().getBindingcarInfo(carConfigResp.macAddress, MoGoAiCloudClientConfig.getInstance().sn)
|
||||
CallerBindingcarManager.getBindingcarProvider()
|
||||
.getBindingcarInfo(carConfigResp.macAddress, MoGoAiCloudClientConfig.getInstance().sn)
|
||||
invokeNettyConnResult("乘客屏车牌号:${carConfigResp.plateNumber},Mac地址为:${carConfigResp.macAddress}")
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,14 @@ import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_ALIAS_C
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_ALIAS_CODE_ADAS_MESSAGE_AUTOPILOT_VEHICLE
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_ALIAS_CODE_ADAS_MESSAGE_CAR_CONFIG
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_ALIAS_CODE_ADAS_MESSAGE_CAR_STATE
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_ALIAS_CODE_ADAS_MESSAGE_PLANNING_OBJECTS
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_ALIAS_CODE_ADAS_MESSAGE_RECT_DATA
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_ADAS
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_CONNECT_STATUS
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_AUTOPILOT
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_DATA_TRACKED
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_GNSSINFO
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_TRAFFIC_LIGHT
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_TRAJECTORY
|
||||
import com.mogo.eagle.core.data.deva.chain.ChainConstant.Companion.CHAIN_LINK_LOG_WEB_SOCKET_VEHICLE
|
||||
@@ -33,6 +35,7 @@ import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotCarConfigListe
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotCarStatusListenerManager.invokeAutopilotCarStateData
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotIdentifyListenerManager
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotIdentifyListenerManager.invokeAutopilotIdentifyDataUpdate
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotIdentifyListenerManager.invokeAutopilotIdentifyPlanningObj
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotIdentifyListenerManager.invokeAutopilotWarnMessage
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotPlanningListenerManager.invokeAutopilotRotting
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotPlanningListenerManager.invokeAutopilotTrajectory
|
||||
@@ -102,7 +105,6 @@ class MoGoAdasListenerImpl : OnAdasListener {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//自车定位信息
|
||||
@ChainLog(
|
||||
linkChainLog = CHAIN_LINK_LOG_WEB_SOCKET_GNSSINFO,
|
||||
@@ -120,7 +122,7 @@ class MoGoAdasListenerImpl : OnAdasListener {
|
||||
CallerMapUIServiceManager.getMapUIController()?.syncLocation2Map(gnssInfo)
|
||||
// 同步更新经纬度和系统时间至 AutoPilotStatusListener
|
||||
CallerAutoPilotStatusListenerManager.updateAutoPilotLatLon(
|
||||
gnssInfo.satelliteTime.toLong(),
|
||||
gnssInfo.satelliteTime.toLong() * 1000,
|
||||
gnssInfo.longitude,
|
||||
gnssInfo.latitude
|
||||
)
|
||||
@@ -252,11 +254,22 @@ class MoGoAdasListenerImpl : OnAdasListener {
|
||||
//点云数据透传
|
||||
}
|
||||
|
||||
//planning障碍物
|
||||
@ChainLog(
|
||||
linkChainLog = CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS,
|
||||
linkCode = CHAIN_LINK_ADAS,
|
||||
endpoint = PAD,
|
||||
nodeAliasCode = CHAIN_ALIAS_CODE_ADAS_MESSAGE_PLANNING_OBJECTS,
|
||||
paramIndexes = [0, 1],
|
||||
clientPkFileName = "sn"
|
||||
)
|
||||
override fun onPlanningObjects(
|
||||
header: MessagePad.Header?,
|
||||
planningObjects: MessagePad.PlanningObjects?
|
||||
planningObjects: MessagePad.PlanningObjects
|
||||
) {
|
||||
//planning障碍物
|
||||
if (HdMapBuildConfig.isMapLoaded) {
|
||||
invokeAutopilotIdentifyPlanningObj(planningObjects.objsList as List<MessagePad.PlanningObject>)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBasicInfoReq(
|
||||
|
||||
@@ -102,11 +102,9 @@ public class MoGoHandAdasMsgManager implements
|
||||
public void onAutopilotBrakeLightData(boolean brakeLight) {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAutopilotCarConfig(@NotNull MessagePad.CarConfigResp carConfigResp) {
|
||||
if (carConfigResp != null && !TextUtils.isEmpty(carConfigResp.getMacAddress())) {
|
||||
Log.d("liyz", "司机端 onAutopilotCarConfig ---" + carConfigResp.getMacAddress());
|
||||
CallerBindingcarManager.getBindingcarProvider().getBindingcarInfo(carConfigResp.getMacAddress(), MoGoAiCloudClientConfig.getInstance().getSn());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.mogo.telematic.MogoProtocolMsg
|
||||
import com.mogo.telematic.NSDNettyManager
|
||||
import com.mogo.telematic.client.status.ConnectState
|
||||
import com.zhidao.support.adas.high.AdasManager
|
||||
import com.zhidao.support.adas.high.chain.AdasChain
|
||||
import com.zhjt.service.chain.ChainLog
|
||||
import com.zhjt.service.chain.TracingConstants
|
||||
import io.netty.channel.Channel
|
||||
@@ -59,9 +58,16 @@ class TeleMsgHandler : IMsgHandler {
|
||||
val carConfig = MessagePad.CarConfigResp.parseFrom(msg.body)
|
||||
AppConfigInfo.plateNumber = carConfig.plateNumber
|
||||
AppConfigInfo.iPCMacAddress = carConfig.macAddress
|
||||
invokeNettyConnResult("司机屏发送给乘客屏配置信息为:${TextFormat.printer().escapingNonAscii(false).printToString(carConfig)}")
|
||||
invokeNettyConnResult(
|
||||
"司机屏发送给乘客屏配置信息为:${
|
||||
TextFormat.printer().escapingNonAscii(false).printToString(carConfig)
|
||||
}"
|
||||
)
|
||||
Log.d("liyz", "TeleMsgHandler macAddress = " + carConfig.macAddress)
|
||||
CallerBindingcarManager.getBindingcarProvider().getBindingcarInfo(carConfig.macAddress, MoGoAiCloudClientConfig.getInstance().sn)
|
||||
CallerBindingcarManager.getBindingcarProvider().getBindingcarInfo(
|
||||
carConfig.macAddress,
|
||||
MoGoAiCloudClientConfig.getInstance().sn
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
@@ -89,6 +95,10 @@ class TeleMsgHandler : IMsgHandler {
|
||||
queryCarConfig()
|
||||
}
|
||||
}
|
||||
//乘客端发送过来的工控机数据交给司机端adas转发到工控机
|
||||
MogoProtocolMsg.NORMAL_DATA -> {
|
||||
AdasManager.getInstance().sendWsMessage(it.body)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.mogo.eagle.core.function.appupgrade.network;
|
||||
|
||||
import com.mogo.eagle.core.data.bindingcar.BindingcarInfo;
|
||||
import com.mogo.eagle.core.data.bindingcar.ModifyBindingcarInfo;
|
||||
import com.mogo.eagle.core.data.bindingcar.UpgradeAppInfo;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import okhttp3.RequestBody;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
/**
|
||||
* @author lixiaopeng
|
||||
* @description 绑定车辆
|
||||
* @since: 6/20/22
|
||||
*/
|
||||
public interface UpgradeApiService {
|
||||
/**
|
||||
* 获取升级信息
|
||||
*
|
||||
* @return {@link UpgradeAppInfo}
|
||||
*/
|
||||
@Headers("Content-Type:application/json;charset=UTF-8")
|
||||
@POST("pad/selectPadByMac")
|
||||
Observable<UpgradeAppInfo> getUpgradeInfo(@Body RequestBody requestBody);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.mogo.eagle.core.function.appupgrade.network;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mogo.cloud.passport.MoGoAiCloudClientConfig;
|
||||
import com.mogo.commons.constants.SharedPrefsConstants;
|
||||
import com.mogo.eagle.core.data.bindingcar.BindingcarInfo;
|
||||
import com.mogo.eagle.core.data.bindingcar.ModifyBindingcarInfo;
|
||||
import com.mogo.eagle.core.data.bindingcar.UpgradeAppInfo;
|
||||
import com.mogo.eagle.core.function.api.bindingcar.BindingcarCallBack;
|
||||
import com.mogo.eagle.core.function.call.hmi.CallerHmiManager;
|
||||
import com.mogo.eagle.core.network.MoGoRetrofitFactory;
|
||||
import com.mogo.eagle.core.network.utils.GsonUtil;
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger;
|
||||
import com.mogo.eagle.core.utilcode.mogo.storage.SharedPrefsMgr;
|
||||
import com.mogo.eagle.core.utilcode.mogo.toast.TipToast;
|
||||
import com.mogo.eagle.core.utilcode.util.AppUtils;
|
||||
import com.mogo.eagle.core.utilcode.util.GsonUtils;
|
||||
import com.mogo.module.common.constants.HostConst;
|
||||
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
|
||||
/**
|
||||
* @author lixiaopeng
|
||||
* @description 获取升级信息
|
||||
* @since: 3/25/22
|
||||
*/
|
||||
public class UpgradeAppNetWorkManager {
|
||||
private static volatile UpgradeAppNetWorkManager requestNoticeManager;
|
||||
private final UpgradeApiService mUpgradeApiService;
|
||||
private static final String TAG = "UpgradeAppNetWorkManager";
|
||||
|
||||
|
||||
private UpgradeAppNetWorkManager() {
|
||||
mUpgradeApiService = MoGoRetrofitFactory.getInstance(HostConst.UPGRADE_APP_HOST)
|
||||
.create(UpgradeApiService.class);
|
||||
}
|
||||
|
||||
public static UpgradeAppNetWorkManager getInstance() {
|
||||
if (requestNoticeManager == null) {
|
||||
synchronized (UpgradeAppNetWorkManager.class) {
|
||||
if (requestNoticeManager == null) {
|
||||
requestNoticeManager = new UpgradeAppNetWorkManager();
|
||||
}
|
||||
}
|
||||
}
|
||||
return requestNoticeManager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取app升级信息
|
||||
*/
|
||||
public void getAppUpgradeInfo(Context context, int screenType) {
|
||||
String sn = "X20202203105S688HZ";
|
||||
String versionCode = "2070000";
|
||||
String versionName = "2.7.0";
|
||||
int screenType1 = 1;
|
||||
|
||||
// String sn = MoGoAiCloudClientConfig.getInstance().getSn();
|
||||
// String versionCode = AppUtils.getAppVersionCode();
|
||||
// String versionName = AppUtils.getAppVersionName();
|
||||
|
||||
UpgradeAppRequest request = new UpgradeAppRequest("apps_control", sn, versionCode, versionName, screenType1);
|
||||
RequestBody requestBody = RequestBody.create(MediaType.get("application/json;charset=UTF-8"), GsonUtil.jsonFromObject(request));
|
||||
mUpgradeApiService.getUpgradeInfo(requestBody)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Observer<UpgradeAppInfo>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
Log.d("liyz", "UpgradeAppInfo ------> ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(@NonNull UpgradeAppInfo info) {
|
||||
if (info != null && info.getData() != null) {
|
||||
Log.d("liyz", "UpgradeAppInfo url = " + info.getData().getApp_url() + "----code = " + info.getData().getVersion_code());
|
||||
//TODO 弹框
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
CallerLogger.INSTANCE.e(TAG, "getBindingcarInfo onError e = " + e.toString() + "---e.getMessage = " + e.getMessage());
|
||||
Log.e("liyz", "UpgradeAppInfo onError e = " + e.toString() + "---e.getMessage = " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.mogo.eagle.core.function.appupgrade.network;
|
||||
|
||||
/**
|
||||
* @author lixiaopeng
|
||||
* @description 获取app升级信息
|
||||
* @since: 11/15/21
|
||||
*/
|
||||
public class UpgradeAppRequest {
|
||||
private String resources;
|
||||
private String sn;
|
||||
private String version_code;
|
||||
private String version_name;
|
||||
private int screen_type;
|
||||
|
||||
public UpgradeAppRequest(String resources, String sn, String versionCode, String versionName, int type) {
|
||||
this.resources = resources;
|
||||
this.sn = sn;
|
||||
this.version_code = versionCode;
|
||||
this.version_name = versionName;
|
||||
this.screen_type = type;
|
||||
}
|
||||
|
||||
public String getResources() {
|
||||
return resources;
|
||||
}
|
||||
|
||||
public void setResources(String resources) {
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
public String getSn() {
|
||||
return sn;
|
||||
}
|
||||
|
||||
public void setSn(String sn) {
|
||||
this.sn = sn;
|
||||
}
|
||||
|
||||
public String getVersion_code() {
|
||||
return version_code;
|
||||
}
|
||||
|
||||
public void setVersion_code(String version_code) {
|
||||
this.version_code = version_code;
|
||||
}
|
||||
|
||||
public String getVersion_name() {
|
||||
return version_name;
|
||||
}
|
||||
|
||||
public void setVersion_name(String version_name) {
|
||||
this.version_name = version_name;
|
||||
}
|
||||
|
||||
public int getScreen_type() {
|
||||
return screen_type;
|
||||
}
|
||||
|
||||
public void setScreen_type(int screen_type) {
|
||||
this.screen_type = screen_type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UpgradeAppRequest{" +
|
||||
"sn='" + sn + '\'' +
|
||||
", version_code='" + version_code + '\'' +
|
||||
", version_name='" + version_name + '\'' +
|
||||
", screen_type=" + screen_type +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.mogo.eagle.core.data.config.HmiBuildConfig;
|
||||
import com.mogo.eagle.core.data.constants.MogoServicePaths;
|
||||
import com.mogo.eagle.core.function.api.bindingcar.BindingcarCallBack;
|
||||
import com.mogo.eagle.core.function.api.bindingcar.IMoGoBindingcarProvider;
|
||||
import com.mogo.eagle.core.function.appupgrade.network.UpgradeAppNetWorkManager;
|
||||
import com.mogo.eagle.core.function.bindingcar.network.BindingcarNetWorkManager;
|
||||
import com.mogo.eagle.core.function.ipcupgrade.IPCUpgradeManager;
|
||||
import com.mogo.eagle.core.utilcode.mogo.AppIdentityModeUtils;
|
||||
@@ -96,7 +97,6 @@ public class BindingcarProvider implements IMoGoBindingcarProvider {
|
||||
long oldHour = SharedPrefsMgr.getInstance(mContext).getLong("typeDriver", 0);
|
||||
//如果2分钟内频繁调,需要拦截,业务导致的会多次请求工控机信息
|
||||
if (HmiBuildConfig.isShowSnBindingView) {
|
||||
Log.d("liyz", "driverScreen -----间隔时间 = " + (currentHour - oldHour) + "-- macAddress = " + macAddress + "--widevineIDWithMd5 = " + widevineIDWithMd5 + "--getScreenType() = " + getScreenType());
|
||||
if (currentHour - oldHour > 1) {
|
||||
SharedPrefsMgr.getInstance(mContext).putLong("typeDriver", System.currentTimeMillis() / (1000 * 60));
|
||||
BindingcarNetWorkManager.getInstance().getBindingcarInfo(mContext, macAddress, widevineIDWithMd5, getScreenType());
|
||||
@@ -109,7 +109,6 @@ public class BindingcarProvider implements IMoGoBindingcarProvider {
|
||||
long oldHour = SharedPrefsMgr.getInstance(mContext).getLong("typePassenger", 0);
|
||||
//如果2分钟内频繁调,需要拦截,业务导致的会多次请求工控机信息
|
||||
if (HmiBuildConfig.isShowSnBindingView) {
|
||||
Log.d("liyz", "passengerScreen --间隔时间 = " + (currentHour - oldHour) + "-- mAddress = " + macAddress + "--mWidevineIDWithMd5 = " + widevineIDWithMd5 + "--getScreenType() = " + getScreenType());
|
||||
if (currentHour - oldHour > 1) {
|
||||
SharedPrefsMgr.getInstance(mContext).putLong("typePassenger", System.currentTimeMillis() / (1000 * 60));
|
||||
BindingcarNetWorkManager.getInstance().getBindingcarInfo(mContext, macAddress, widevineIDWithMd5, getScreenType());
|
||||
@@ -129,5 +128,12 @@ public class BindingcarProvider implements IMoGoBindingcarProvider {
|
||||
return screenType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询app是否需要升级
|
||||
*/
|
||||
@Override
|
||||
public void queryAppUpgrade() {
|
||||
UpgradeAppNetWorkManager.getInstance().getAppUpgradeInfo(mContext, getScreenType());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ public class BindingcarNetWorkManager {
|
||||
// String macAddress = "48:b0:2d:3a:bc:78";
|
||||
// String sn = "X20202203105S688HZ";
|
||||
|
||||
Log.d("liyz", "getBindingcarInfo -- widevineIDWithMd5 = " + widevineIDWithMd5 + "--macAddress = " + macAddress + "--screenType = " + screenType);
|
||||
BindingcarRequest request = new BindingcarRequest(macAddress, widevineIDWithMd5, screenType);
|
||||
RequestBody requestBody = RequestBody.create(MediaType.get("application/json;charset=UTF-8"), GsonUtil.jsonFromObject(request));
|
||||
mBindingcarApiService.getBindingcarInfo(token, requestBody)
|
||||
@@ -76,7 +75,6 @@ public class BindingcarNetWorkManager {
|
||||
public void onNext(@NonNull BindingcarInfo info) {
|
||||
if (info != null && info.getData() != null) {
|
||||
CallerLogger.INSTANCE.d(TAG, "getBindingcarInfo onNext info.getData() =" + info.getData().toString());
|
||||
Log.d("liyz", "getBindingcarInfo onNext info.getData() =" + info.getData().toString() + "--compare = " + info.getData().getCompare());
|
||||
if (info.getData().getCompare().equals("0")) {
|
||||
CallerHmiManager.INSTANCE.showBindingcarDialog();
|
||||
} else if (info.getData().getCompare().equals("3")) {
|
||||
@@ -107,7 +105,6 @@ public class BindingcarNetWorkManager {
|
||||
* mac: 48:b0:2d:3a:9c:19
|
||||
*/
|
||||
public void modifyBindingcar(String macAddress, String widevineIDWithMd5, BindingcarCallBack callBack, int screenType) {
|
||||
Log.d("liyz", "modifyBindingcar --- widevineIDWithMd5 = " + widevineIDWithMd5 + "---macAddress = " + macAddress + "--screenType = " + screenType);
|
||||
BindingcarRequest request = new BindingcarRequest(macAddress, widevineIDWithMd5, screenType);
|
||||
RequestBody requestBody = RequestBody.create(MediaType.get("application/json;charset=UTF-8"), GsonUtil.jsonFromObject(request));
|
||||
mBindingcarApiService.modifyBindingcarInfo(token, requestBody)
|
||||
@@ -123,13 +120,11 @@ public class BindingcarNetWorkManager {
|
||||
if (info != null) {
|
||||
callBack.callBackResult(info);
|
||||
CallerLogger.INSTANCE.d(TAG, "modifyBindingcar onNext code = " + info.code + "---msg = " + info.msg + "--info.toString() = " + info.toString());
|
||||
Log.d("liyz", "modifyBindingcar onNext code = " + info.code + "---msg = " + info.msg + "--info.toString() = " + info.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
Log.e("liyz", "modifyBindingcar onError e = " + e.toString() + "---e.getMessage = " + e.getMessage());
|
||||
CallerLogger.INSTANCE.e(TAG, "modifyBindingcar onError e = " + e.toString() + "---e.getMessage = " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.mogo.eagle.core.data.report.ReportEntity
|
||||
import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotStatusListener
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutoPilotStatusListenerManager
|
||||
import com.mogo.eagle.core.function.call.hmi.CallerHmiManager
|
||||
import com.mogo.eagle.core.utilcode.mogo.AppIdentityModeUtils
|
||||
import com.mogo.eagle.core.utilcode.util.TimeUtils
|
||||
import mogo_msg.MogoReportMsg
|
||||
|
||||
@@ -25,8 +26,11 @@ class IPCReportManager : IMoGoAutopilotStatusListener {
|
||||
}
|
||||
|
||||
fun initServer(){
|
||||
// 添加 ADAS状态 监听
|
||||
CallerAutoPilotStatusListenerManager.addListener(TAG, this)
|
||||
//乘客屏不显示监控信息弹窗,只在司机端提示
|
||||
if(AppIdentityModeUtils.isDriver(FunctionBuildConfig.appIdentityMode)){
|
||||
// 添加 ADAS状态 监听
|
||||
CallerAutoPilotStatusListenerManager.addListener(TAG, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,15 +47,20 @@ class IPCReportManager : IMoGoAutopilotStatusListener {
|
||||
it.src,it.level,it.msg,it.code,it.resultList,it.actionsList))
|
||||
//当前不处于美化模式时,展示监控节点上报
|
||||
if(!FunctionBuildConfig.isDemoMode){
|
||||
CallerHmiManager.showIPCReportWindow(ipcReportList)
|
||||
if(FunctionBuildConfig.isReportWarning){
|
||||
CallerHmiManager.showIPCReportWindow(ipcReportList)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy(){
|
||||
// 移除 ADAS状态 监听
|
||||
CallerAutoPilotStatusListenerManager.removeListener(TAG)
|
||||
//乘客屏不显示监控信息弹窗,只在司机端提示
|
||||
if(AppIdentityModeUtils.isDriver(FunctionBuildConfig.appIdentityMode)){
|
||||
// 移除 ADAS状态 监听
|
||||
CallerAutoPilotStatusListenerManager.removeListener(TAG)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,6 @@ internal class CanImpl(ctx: Context): IFlow<CanStatus>(ctx), IMoGoAutopilotVehic
|
||||
private var job: Job? = null
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "-- onCreate --")
|
||||
send(CanStatus(CallerAutoPilotManager.isConnected()))
|
||||
CallerAutopilotVehicleStateListenerManager.addListener(TAG, this)
|
||||
CallerAutoPilotStatusListenerManager.addListener(TAG, this)
|
||||
@@ -32,7 +31,10 @@ internal class CanImpl(ctx: Context): IFlow<CanStatus>(ctx), IMoGoAutopilotVehic
|
||||
timeOutCheck()
|
||||
}
|
||||
|
||||
private fun isCanEnabled() = CallerAutoPilotManager.isConnected() && CallerAutoPilotStatusListenerManager.getAutoPilotReportMessageCode() != "EHW_CAN"
|
||||
private fun isCanEnabled(): Boolean {
|
||||
val code = CallerAutoPilotStatusListenerManager.getAutoPilotReportMessageCode()
|
||||
return CallerAutoPilotManager.isConnected() && code != "EHW_CAN"
|
||||
}
|
||||
|
||||
|
||||
override fun onAutopilotBrakeLightData(brakeLight: Boolean) {
|
||||
@@ -79,7 +81,6 @@ internal class CanImpl(ctx: Context): IFlow<CanStatus>(ctx), IMoGoAutopilotVehic
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job?.safeCancel()
|
||||
Log.d(TAG, "-- onDestroy --")
|
||||
CallerAutopilotVehicleStateListenerManager.removeListener(TAG)
|
||||
CallerAutoPilotStatusListenerManager.removeListener(TAG)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.zhjt.mogo_core_function_devatools.status.flow.ipc
|
||||
|
||||
import android.content.*
|
||||
import android.util.*
|
||||
import com.mogo.eagle.core.data.autopilot.*
|
||||
import com.mogo.eagle.core.function.api.autopilot.*
|
||||
import com.mogo.eagle.core.function.call.autopilot.*
|
||||
import com.zhjt.mogo_core_function_devatools.status.flow.IFlow
|
||||
@@ -17,7 +15,6 @@ internal class IpcImpl(ctx: Context): IFlow<IpcStatus>(ctx), IMoGoAutopilotStatu
|
||||
private var state: Int = -1
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "-- onCreate --")
|
||||
checkAndSend()
|
||||
CallerAutoPilotStatusListenerManager.addListener(TAG, this)
|
||||
}
|
||||
@@ -36,7 +33,6 @@ internal class IpcImpl(ctx: Context): IFlow<IpcStatus>(ctx), IMoGoAutopilotStatu
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "-- onDestroy --")
|
||||
CallerAutoPilotStatusListenerManager.removeListener(TAG)
|
||||
}
|
||||
}
|
||||
@@ -37,47 +37,39 @@ internal class NetsImpl(ctx: Context): IFlow<NetStatus>(ctx) {
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
Log.d(TAG, "-- onAvailable --:: $network")
|
||||
checkAndSend()
|
||||
}
|
||||
|
||||
override fun onLosing(network: Network, maxMsToLive: Int) {
|
||||
super.onLosing(network, maxMsToLive)
|
||||
Log.d(TAG, "-- onLosing --:: $network::$maxMsToLive")
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
super.onLost(network)
|
||||
Log.d(TAG, "-- onLost --:: $network")
|
||||
checkAndSend()
|
||||
}
|
||||
|
||||
override fun onUnavailable() {
|
||||
super.onUnavailable()
|
||||
Log.d(TAG, "-- onUnavailable --")
|
||||
checkAndSend()
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
Log.d(TAG, "-- onCapabilitiesChanged --:$network::$networkCapabilities")
|
||||
checkAndSend()
|
||||
}
|
||||
|
||||
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
|
||||
super.onLinkPropertiesChanged(network, linkProperties)
|
||||
Log.d(TAG, "-- onLinkPropertiesChanged --:$network::$linkProperties")
|
||||
}
|
||||
|
||||
override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
|
||||
super.onBlockedStatusChanged(network, blocked)
|
||||
Log.d(TAG, "-- onBlockedStatusChanged --:$network::$blocked")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "-- onCreate --")
|
||||
checkAndSend()
|
||||
val builder = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
@@ -106,7 +98,6 @@ internal class NetsImpl(ctx: Context): IFlow<NetStatus>(ctx) {
|
||||
sr = connectionInfo.rxLinkSpeedMbps
|
||||
}
|
||||
val speed = Speed(tr, sr)
|
||||
Log.d(TAG, "checkAndSend----:enable: $enabled :: name: $name")
|
||||
send(enabled, name, speed)
|
||||
}
|
||||
|
||||
@@ -129,7 +120,6 @@ internal class NetsImpl(ctx: Context): IFlow<NetStatus>(ctx) {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "-- onDestroy --")
|
||||
if (registered.compareAndSet(true, false)) {
|
||||
connectMgr.unregisterNetworkCallback(cb)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,16 @@ internal class RTKImpl(ctx: Context): IFlow<RTKStatus>(ctx), IMoGoAutopilotCarSt
|
||||
CallerAutoPilotStatusListenerManager.addListener(TAG, this)
|
||||
}
|
||||
|
||||
private fun isRTKEnabled() =
|
||||
CallerAutoPilotManager.isConnected() && (CallerAutoPilotStatusListenerManager.getAutoPilotReportMessageCode() != "EHW_RTK" && CallerAutoPilotStatusListenerManager.getAutoPilotReportMessageCode() != "EHW_GNSS") && CallerAutopilotCarStatusListenerManager.getCurrentGnssInfo() != null
|
||||
private fun isRTKEnabled(): Boolean {
|
||||
val code = CallerAutoPilotStatusListenerManager.getAutoPilotReportMessageCode()
|
||||
val gnssInfo = CallerAutopilotCarStatusListenerManager.getCurrentGnssInfo()
|
||||
return CallerAutoPilotManager.isConnected() && (
|
||||
code != "EHW_RTK" &&
|
||||
code != "EHW_GNSS" &&
|
||||
code != "ESYS_RTK_STATUS_FAULT" &&
|
||||
code != "ELCT_RTK_STATUS_FAULT" &&
|
||||
code != "ELCT_RTK_STATUS_UNKNOWN") && gnssInfo != null
|
||||
}
|
||||
|
||||
override fun onAutopilotCarStateData(gnssInfo: GnssInfo?) {
|
||||
send(RTKStatus(isRTKEnabled()))
|
||||
@@ -34,9 +42,7 @@ internal class RTKImpl(ctx: Context): IFlow<RTKStatus>(ctx), IMoGoAutopilotCarSt
|
||||
|
||||
override fun onAutopilotIpcConnectStatusChanged(status: Int, reason: String?) {
|
||||
super.onAutopilotIpcConnectStatusChanged(status, reason)
|
||||
if (!CallerAutoPilotManager.isConnected()) {
|
||||
send(RTKStatus(false))
|
||||
}
|
||||
send(RTKStatus(isRTKEnabled()))
|
||||
}
|
||||
|
||||
private fun timeOutCheck() {
|
||||
|
||||
@@ -19,7 +19,6 @@ internal class TracingImpl(ctx: Context): IFlow<TracingStatus>(ctx), IMoGoAutopi
|
||||
private var old: TracingStatus.Tracing = UNKNOWN
|
||||
|
||||
override fun onCreate() {
|
||||
Log.d(TAG, "-- onCreate --")
|
||||
val code = CallerAutoPilotStatusListenerManager.getAutoPilotReportMessageCode()
|
||||
val state = code.toState() ?: UNKNOWN
|
||||
old = state
|
||||
@@ -31,7 +30,6 @@ internal class TracingImpl(ctx: Context): IFlow<TracingStatus>(ctx), IMoGoAutopi
|
||||
override fun onAutopilotGuardian(guardianInfo: MogoReportMessage?) {
|
||||
super.onAutopilotGuardian(guardianInfo)
|
||||
val current = guardianInfo?.code
|
||||
Log.d(TAG, "-- onAutopilotGuardian --: $current")
|
||||
val newState = current?.toState()
|
||||
if (newState != null && newState != old) {
|
||||
send(TracingStatus(newState))
|
||||
@@ -42,7 +40,6 @@ internal class TracingImpl(ctx: Context): IFlow<TracingStatus>(ctx), IMoGoAutopi
|
||||
|
||||
override fun onAutopilotStatusResponse(autoPilotStatusInfo: AutopilotStatusInfo) {
|
||||
super.onAutopilotStatusResponse(autoPilotStatusInfo)
|
||||
//Log.d(TAG, "-- onAutopilotStatusResponse -- autopilotMode: ${autoPilotStatusInfo.pilotmode} :: state: ${autoPilotStatusInfo.state}")
|
||||
if (autoPilotStatusInfo.state == IMoGoAutopilotStatusListener.STATUS_AUTOPILOT_DISABLE) {
|
||||
send(TracingStatus(UNKNOWN))
|
||||
}
|
||||
@@ -53,7 +50,6 @@ internal class TracingImpl(ctx: Context): IFlow<TracingStatus>(ctx), IMoGoAutopi
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "-- onDestroy --")
|
||||
CallerAutoPilotStatusListenerManager.removeListener(TAG)
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,6 @@ internal class StatusModel : ViewModel() {
|
||||
val nv = ArrayList(v).also { it.updateOrInsert(s) }
|
||||
val data = Pair(getExceptionStatus(nv), nv)
|
||||
old.set(data)
|
||||
Log.d(TAG, "status: $s")
|
||||
Log.d(TAG, "data: ${data.second}")
|
||||
status.postValue(data)
|
||||
}
|
||||
}
|
||||
@@ -86,7 +84,6 @@ internal class StatusModel : ViewModel() {
|
||||
is TracingStatus -> {
|
||||
val c1 = CallerAutoPilotStatusListenerManager.getAutoPilotStatusInfo().state != IMoGoAutopilotStatusListener.STATUS_AUTOPILOT_DISABLE
|
||||
val c2 = s.state.isException()
|
||||
Log.d(TAG, "getExceptionStatus-::c1: $c1 -> c2: $c2")
|
||||
if (c1 && c2) {
|
||||
s
|
||||
} else {
|
||||
|
||||
@@ -49,6 +49,8 @@ class TraceManager {
|
||||
FwBuild(false, 30,pkgName + ChainConstant.CHAIN_LINK_LOG_ADAS_VEHICLE)
|
||||
fwBuildMap[ChainConstant.CHAIN_LINK_LOG_WEB_SOCKET_TRAFFIC_LIGHT] =
|
||||
FwBuild(false, 30,pkgName + ChainConstant.CHAIN_LINK_LOG_ADAS_TRAFFIC_LIGHT)
|
||||
fwBuildMap[ChainConstant.CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS] =
|
||||
FwBuild(false, 30,pkgName + ChainConstant.CHAIN_LINK_LOG_ADAS_PLANNING_OBJECTS)
|
||||
|
||||
traceInfoCache[ChainConstant.CHAIN_LINK_LOG_CONNECT_STATUS] =
|
||||
ChainLogParam(true, "ADAS连接状态")
|
||||
@@ -62,6 +64,8 @@ class TraceManager {
|
||||
ChainLogParam(false, "ADAS车前引导线")
|
||||
traceInfoCache[ChainConstant.CHAIN_LINK_LOG_WEB_SOCKET_VEHICLE] =
|
||||
ChainLogParam(false, "ADAS车辆底盘数据")
|
||||
traceInfoCache[ChainConstant.CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS] =
|
||||
ChainLogParam(false, "ADAS PLANNING 感知障碍物")
|
||||
|
||||
FileWriteManager.getInstance()
|
||||
.init(context, MoGoAiCloudClientConfig.getInstance().sn, pkgName, fwBuildMap)
|
||||
|
||||
@@ -50,9 +50,7 @@ open class DefaultAnimator : OnFloatAnimator {
|
||||
if (triple.third) params.x = value else params.y = value
|
||||
// 动画执行过程中页面关闭,出现异常
|
||||
windowManager.updateViewLayout(view, params)
|
||||
Log.d("XXX", "update ---> ${it.animatedValue}, ${it.animatedFraction}, $value")
|
||||
} catch (e: Exception) {
|
||||
Log.d("XXX", "exception ---> $e")
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.mogo.eagle.core.function.hmi.notification.WarningFloat
|
||||
import com.mogo.eagle.core.function.hmi.notification.anim.DefaultAnimator
|
||||
import com.mogo.eagle.core.function.hmi.ui.bindingcar.ModifyBindingCarDialog
|
||||
import com.mogo.eagle.core.function.hmi.ui.bindingcar.ToBindingCarDialog
|
||||
import com.mogo.eagle.core.function.hmi.ui.bindingcar.UpgradeAppDialog
|
||||
import com.mogo.eagle.core.function.hmi.ui.camera.CameraListView
|
||||
import com.mogo.eagle.core.function.hmi.ui.notice.NoticeBannerView
|
||||
import com.mogo.eagle.core.function.hmi.ui.notice.NoticeNormalBannerView
|
||||
@@ -58,6 +59,7 @@ import com.mogo.eagle.core.function.hmi.ui.widget.V2XNotificationView
|
||||
import com.mogo.eagle.core.utilcode.mogo.AppIdentityModeUtils
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.Companion.M_HMI
|
||||
import com.mogo.eagle.core.utilcode.util.SoundUtils
|
||||
import com.mogo.eagle.core.utilcode.util.ThreadUtils
|
||||
import com.mogo.eagle.core.utilcode.util.TimeUtils
|
||||
import com.mogo.eagle.core.utilcode.util.ToastUtils
|
||||
@@ -247,7 +249,7 @@ class MoGoHmiFragment : MvpFragment<MoGoHmiContract.View?, HmiPresenter?>(),
|
||||
}
|
||||
})
|
||||
ipcReportWindow?.let {
|
||||
AIAssist.getInstance(AbsMogoApplication.getApp()).speakTTSVoice("嘟")
|
||||
SoundUtils.playRing(requireContext())
|
||||
}
|
||||
}
|
||||
ipcReportWindow?.showFloatWindow()
|
||||
@@ -964,6 +966,7 @@ class MoGoHmiFragment : MvpFragment<MoGoHmiContract.View?, HmiPresenter?>(),
|
||||
|
||||
private var modifyBindingCarDialog: ModifyBindingCarDialog? = null
|
||||
private var toBindingCarDialog: ToBindingCarDialog? = null
|
||||
private var upgradeAppDialog: UpgradeAppDialog? = null
|
||||
|
||||
override fun showToBindingcarDialog() {
|
||||
if (toBindingCarDialog == null) {
|
||||
@@ -977,7 +980,16 @@ class MoGoHmiFragment : MvpFragment<MoGoHmiContract.View?, HmiPresenter?>(),
|
||||
modifyBindingCarDialog = ModifyBindingCarDialog(requireContext())
|
||||
}
|
||||
modifyBindingCarDialog!!.showModifyBindingcarDialog()
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级app弹框
|
||||
*/
|
||||
override fun showUpgradeDialog(name: String, url: String) {
|
||||
if (upgradeAppDialog == null) {
|
||||
upgradeAppDialog = UpgradeAppDialog(requireContext())
|
||||
}
|
||||
upgradeAppDialog!!.showUpgradeAppDialog(name, url)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.mogo.eagle.core.function.hmi.ui.bindingcar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import com.mogo.eagle.core.function.call.bindingcar.CallerBindingcarManager
|
||||
import com.mogo.eagle.core.function.call.devatools.CallerDevaToolsManager
|
||||
import com.mogo.eagle.core.function.hmi.R
|
||||
import com.mogo.eagle.core.utilcode.mogo.toast.TipToast
|
||||
import com.mogo.module.common.dialog.BaseFloatDialog
|
||||
import com.mogo.service.IMogoServiceApis
|
||||
import com.mogo.service.statusmanager.IMogoStatusChangedListener
|
||||
import com.mogo.service.statusmanager.StatusDescriptor
|
||||
|
||||
/**
|
||||
* @brief APP升级提示弹框
|
||||
* @author lixiaopeng
|
||||
*/
|
||||
class UpgradeAppDialog(context: Context) : BaseFloatDialog(context), LifecycleObserver{
|
||||
|
||||
private val TAG = "UpgradeAppDialog"
|
||||
private var confirmTv: TextView? = null
|
||||
private var cancleTv: TextView? = null
|
||||
private var tag: String? = null
|
||||
private var downloarUrl: String? = null
|
||||
|
||||
private var mServiceApis: IMogoServiceApis? = null
|
||||
private val statusChangedListenerForCheckNotice = IMogoStatusChangedListener { descriptor, isTrue ->
|
||||
if (descriptor == StatusDescriptor.MAIN_PAGE_IS_BACKGROUND) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
setContentView(R.layout.dialog_upgrade_app)
|
||||
setCanceledOnTouchOutside(true)
|
||||
|
||||
confirmTv = findViewById(R.id.tv_upgrade_confirm)
|
||||
cancleTv = findViewById(R.id.tv_upgrade_cancel)
|
||||
|
||||
confirmTv?.setOnClickListener {
|
||||
downloadApp()
|
||||
}
|
||||
|
||||
cancleTv?.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 去下载 TODO 成功或者失败
|
||||
*/
|
||||
fun downloadApp() {
|
||||
tag?.let { downloarUrl?.let { it1 -> CallerDevaToolsManager.downLoadPackage(it, it1) } }
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
}
|
||||
|
||||
fun showUpgradeAppDialog(name: String, url: String) {
|
||||
if (isShowing) {
|
||||
return
|
||||
}
|
||||
tag = name
|
||||
downloarUrl = url
|
||||
Log.d("liyz", "tag = $tag ---- downloarUrl = $downloarUrl")
|
||||
show()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import android.text.Html
|
||||
import android.util.AttributeSet
|
||||
@@ -63,6 +64,8 @@ import com.mogo.eagle.core.utilcode.util.*
|
||||
import com.mogo.map.MogoMap
|
||||
import com.mogo.map.uicontroller.VisualAngleMode
|
||||
import com.mogo.map.uicontroller.VisualAngleMode.*
|
||||
import com.mogo.module.service.routeoverlay.*
|
||||
import com.tencent.liteav.basic.datareport.a.B
|
||||
import com.zhidao.easysocket.utils.L
|
||||
import kotlinx.android.synthetic.main.view_debug_setting.view.*
|
||||
import mogo.telematics.pad.MessagePad
|
||||
@@ -137,7 +140,7 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
private var clickListener: ClickListener? = null
|
||||
|
||||
//剪切板
|
||||
private var clipboardManager: ClipboardManager ?= null
|
||||
private var clipboardManager: ClipboardManager? = null
|
||||
|
||||
private val simpleDateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
@@ -291,10 +294,10 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
* 开发者模式
|
||||
*/
|
||||
swDevelopMode.setOnCheckedChangeListener { _, isChecked ->
|
||||
if(isChecked){
|
||||
if (isChecked) {
|
||||
controlCenterLayout.visibility = View.VISIBLE
|
||||
commonLayout.visibility = View.GONE
|
||||
}else{
|
||||
} else {
|
||||
controlCenterLayout.visibility = View.GONE
|
||||
commonLayout.visibility = View.VISIBLE
|
||||
}
|
||||
@@ -339,12 +342,12 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
buttonView.setCompoundDrawables(null, null, iconDown, null)
|
||||
//展示OBU控制中心
|
||||
obuControllerLayout.visibility = View.VISIBLE
|
||||
tbVehicleStateController.isChecked = true
|
||||
tbVehicleStateController.isChecked = true
|
||||
} else {
|
||||
buttonView.setCompoundDrawables(null, null, iconRight, null)
|
||||
//隐藏OBU控制中心
|
||||
obuControllerLayout.visibility = View.GONE
|
||||
tbVehicleStateController.isChecked = false
|
||||
tbVehicleStateController.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,17 +552,17 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
FunctionBuildConfig.isRainMode = isChecked
|
||||
}
|
||||
//雨天模式按钮只在司机屏生效,乘客屏不显示
|
||||
if(AppIdentityModeUtils.isPassenger(FunctionBuildConfig.appIdentityMode)){
|
||||
if (AppIdentityModeUtils.isPassenger(FunctionBuildConfig.appIdentityMode)) {
|
||||
tbIsRainMode.visibility = View.GONE
|
||||
}
|
||||
|
||||
//重启工控机所有节点
|
||||
btnIpcReboot.onClick{
|
||||
btnIpcReboot.onClick {
|
||||
CallerAutoPilotManager.sendIpcReboot()
|
||||
ToastUtils.showLong("重启命令已发送")
|
||||
}
|
||||
//只在司机端设置工控机节点重启功能
|
||||
if(AppIdentityModeUtils.isPassenger(FunctionBuildConfig.appIdentityMode)){
|
||||
if (AppIdentityModeUtils.isPassenger(FunctionBuildConfig.appIdentityMode)) {
|
||||
btnIpcReboot.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -635,15 +638,15 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
//设置点云大小
|
||||
btnPointCloudSize.setOnClickListener{
|
||||
btnPointCloudSize.setOnClickListener {
|
||||
val cloudSize = etPointCloudSize.text.toString()
|
||||
if(cloudSize.isEmpty()){
|
||||
if (cloudSize.isEmpty()) {
|
||||
ToastUtils.showShort("请输入正确的点云大小")
|
||||
}else{
|
||||
} else {
|
||||
try {
|
||||
val cloudSizeFloat = cloudSize.toFloat()
|
||||
CallerHDMapManager.setPointCloudSize(cloudSizeFloat)
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showShort("点云大小格式输入不正确")
|
||||
}
|
||||
}
|
||||
@@ -651,12 +654,12 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
//设置点云颜色
|
||||
btnPointCloudColor.setOnClickListener {
|
||||
val cloudColor = etPointCloudColor.text.toString()
|
||||
if(cloudColor.isEmpty()){
|
||||
if (cloudColor.isEmpty()) {
|
||||
ToastUtils.showShort("请输入正确的点云颜色")
|
||||
}else{
|
||||
} else {
|
||||
try {
|
||||
CallerHDMapManager.setPointCloudColor(cloudColor)
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showShort("点云大小颜色输入不正确")
|
||||
}
|
||||
}
|
||||
@@ -694,34 +697,47 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
|
||||
//SN复制按钮
|
||||
tvPadSnClip.setOnClickListener {
|
||||
if(clipboardManager==null){
|
||||
if (clipboardManager == null) {
|
||||
//获取剪贴板管理器:
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
}
|
||||
// 创建普通字符型ClipData ,将ClipData内容放到系统剪贴板里
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("MoGoSN",AppConfigInfo.mogoSN))
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("MoGoSN", AppConfigInfo.mogoSN))
|
||||
ToastUtils.showLong("SN复制成功")
|
||||
}
|
||||
|
||||
//工控机镜像复制按钮
|
||||
tvIpcVersionInfoClip.setOnClickListener{
|
||||
if(clipboardManager==null){
|
||||
tvIpcVersionInfoClip.setOnClickListener {
|
||||
if (clipboardManager == null) {
|
||||
//获取剪贴板管理器:
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
}
|
||||
// 创建普通字符型ClipData ,将ClipData内容放到系统剪贴板里
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("DockVersion",mAutoPilotStatusInfo?.dockVersion))
|
||||
clipboardManager?.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
"DockVersion",
|
||||
mAutoPilotStatusInfo?.dockVersion
|
||||
)
|
||||
)
|
||||
ToastUtils.showLong("docker版本复制成功")
|
||||
}
|
||||
|
||||
//经纬度复制按钮
|
||||
tvCarInfoCopyClip.setOnClickListener{
|
||||
if(clipboardManager==null){
|
||||
tvCarInfoCopyClip.setOnClickListener {
|
||||
if (clipboardManager == null) {
|
||||
//获取剪贴板管理器:
|
||||
clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
}
|
||||
// 创建普通字符型ClipData ,将ClipData内容放到系统剪贴板里
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("LonAndLat","${mGnssInfo?.longitude},${mGnssInfo?.latitude}"))
|
||||
clipboardManager?.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
"LonAndLat",
|
||||
"${mGnssInfo?.longitude},${mGnssInfo?.latitude}"
|
||||
)
|
||||
)
|
||||
ToastUtils.showLong("经纬度复制成功")
|
||||
}
|
||||
|
||||
@@ -764,6 +780,13 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
CallerAutoPilotManager.connectSpecifiedServer(ip)
|
||||
}
|
||||
}
|
||||
|
||||
//是否开启异常上报
|
||||
tbReportWarning.isChecked = FunctionBuildConfig.isReportWarning
|
||||
tbReportWarning.setOnCheckedChangeListener { _, isChecked ->
|
||||
FunctionBuildConfig.isReportWarning = isChecked
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -944,6 +967,16 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
accelerationIsShow = isChecked
|
||||
}
|
||||
|
||||
tbRouteDynamicEffect.isChecked = AppIdentityModeUtils.isTaxi(FunctionBuildConfig.appIdentityMode)
|
||||
|
||||
tbRouteDynamicEffect.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
RouteStrategy.enable(true)
|
||||
} else {
|
||||
RouteStrategy.enable(false)
|
||||
}
|
||||
}
|
||||
|
||||
btnThresholdDefine.setOnClickListener {
|
||||
try {
|
||||
accelerationThresholdNum = etThreshold.text.toString().toDouble()
|
||||
@@ -1127,7 +1160,7 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
if (logTimeStr.isNullOrEmpty()) {
|
||||
logTimeStr = "10"
|
||||
}
|
||||
try{
|
||||
try {
|
||||
val logCatchTime = logTimeStr.toInt()
|
||||
if (logCatchTime > 60) {
|
||||
tbLogCatch.isChecked = false
|
||||
@@ -1135,7 +1168,7 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
return@setOnCheckedChangeListener
|
||||
}
|
||||
CallerDevaToolsManager.startCatchLog(logCatchTime)
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showLong("输入格式错误,请重新输入正确时间数字,最长抓取时间为60分钟")
|
||||
etLogCatch.setText("")
|
||||
}
|
||||
@@ -1259,7 +1292,7 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* ADAS车辆地盘数据
|
||||
* ADAS车辆底盘数据
|
||||
*/
|
||||
cbAdasVehicle.setOnCheckedChangeListener { _, isChecked ->
|
||||
val map = CallerDevaToolsManager.getTraceInfo()
|
||||
@@ -1270,6 +1303,20 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
CallerDevaToolsManager.refreshTraceInfo(map)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ADAS PLANNING OBJ 感知障碍物
|
||||
*/
|
||||
cbAdasPlanningObj.setOnCheckedChangeListener { _, isChecked ->
|
||||
val map = CallerDevaToolsManager.getTraceInfo()
|
||||
val param = map[ChainConstant.CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS]
|
||||
param?.let {
|
||||
it.record = isChecked
|
||||
map[ChainConstant.CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS] = param
|
||||
CallerDevaToolsManager.refreshTraceInfo(map)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun refreshTraceInfo() {
|
||||
@@ -1516,9 +1563,9 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
}"
|
||||
)
|
||||
|
||||
if(AppConfigInfo.isConnectAutopilot){
|
||||
if (AppConfigInfo.isConnectAutopilot) {
|
||||
tvIpcConnectStatus.minLines = 1
|
||||
}else{
|
||||
} else {
|
||||
tvIpcConnectStatus.minLines = 4
|
||||
}
|
||||
|
||||
@@ -1538,7 +1585,7 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
}"
|
||||
)
|
||||
//如果是乘客端,则不显示工控机连接状态
|
||||
if(AppIdentityModeUtils.isPassenger(FunctionBuildConfig.appIdentityMode)){
|
||||
if (AppIdentityModeUtils.isPassenger(FunctionBuildConfig.appIdentityMode)) {
|
||||
tvIpcConnectStatus.visibility = View.GONE
|
||||
tvAutopilotConnectStatus.visibility = View.GONE
|
||||
}
|
||||
@@ -1848,8 +1895,8 @@ class DebugSettingView @JvmOverloads constructor(
|
||||
/**
|
||||
* 初始化上报
|
||||
*/
|
||||
fun reportInit(reportList: ArrayList<ReportEntity>){
|
||||
if(reportList.size > 0){
|
||||
fun reportInit(reportList: ArrayList<ReportEntity>) {
|
||||
if (reportList.size > 0) {
|
||||
reportMsgLayout.visibility = View.VISIBLE
|
||||
reportList[0].let {
|
||||
tvReportSrc.text = "src:${it.src}"
|
||||
|
||||
@@ -74,7 +74,8 @@ class AutoPilotAndCheckView @JvmOverloads constructor(
|
||||
ToastUtils.showShort("超过最大限速值60,设置失败")
|
||||
}
|
||||
else -> {
|
||||
llSpeedPosition.background = resources.getDrawable(R.drawable.pilot_speed_bg)
|
||||
llSpeedPosition.background =
|
||||
resources.getDrawable(R.drawable.pilot_speed_bg)
|
||||
keyBoardUtil?.hideKeyboard()
|
||||
etInputSpeed.clearFocus()
|
||||
// 设置自动驾驶速度
|
||||
@@ -107,13 +108,15 @@ class AutoPilotAndCheckView @JvmOverloads constructor(
|
||||
etInputSpeed.setOnFocusChangeListener { v, hasFocus ->
|
||||
when {
|
||||
hasFocus -> {
|
||||
llSpeedPosition.background = resources.getDrawable(R.drawable.pilot_speed_high_light_bg)
|
||||
llSpeedPosition.background =
|
||||
resources.getDrawable(R.drawable.pilot_speed_high_light_bg)
|
||||
if (keyBoardUtil == null) {
|
||||
keyBoardUtil = KeyBoardUtil(sKeyBoardView, etInputSpeed)
|
||||
}
|
||||
keyBoardUtil?.showKeyboard()
|
||||
}
|
||||
else -> llSpeedPosition.background = resources.getDrawable(R.drawable.pilot_speed_bg)
|
||||
else -> llSpeedPosition.background =
|
||||
resources.getDrawable(R.drawable.pilot_speed_bg)
|
||||
}
|
||||
}
|
||||
etInputSpeed.setOnTouchListener { v, event ->
|
||||
@@ -150,28 +153,16 @@ class AutoPilotAndCheckView @JvmOverloads constructor(
|
||||
this.clickListener = clickListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Bus不可设置自动驾驶速度,而Taxi可以
|
||||
*/
|
||||
private fun updateSpeedSettingViews() {
|
||||
when {
|
||||
AppIdentityModeUtils.isBus(FunctionBuildConfig.appIdentityMode) -> {
|
||||
tvSpeedTitle.visibility = View.GONE
|
||||
llSpeedPosition.visibility = View.GONE
|
||||
}
|
||||
else -> {
|
||||
tvSpeedTitle.visibility = View.VISIBLE
|
||||
llSpeedPosition.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
tvSpeedTitle.visibility = View.VISIBLE
|
||||
llSpeedPosition.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun showAdUpgradeStatus(ipcUpgradeStateInfo: IPCUpgradeStateInfo){
|
||||
fun showAdUpgradeStatus(ipcUpgradeStateInfo: IPCUpgradeStateInfo) {
|
||||
systemVersionView?.showAdUpgradeStatus(ipcUpgradeStateInfo)
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
CallerAutoPilotStatusListenerManager.addListener(TAG, this)
|
||||
|
||||
@@ -5,16 +5,19 @@ import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.mogo.cloud.passport.MoGoAiCloudClientConfig
|
||||
import com.mogo.eagle.core.data.autopilot.AutopilotStatusInfo
|
||||
import com.mogo.eagle.core.data.bindingcar.AdUpgradeStateHelper
|
||||
import com.mogo.eagle.core.data.bindingcar.IPCUpgradeStateInfo
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.Companion.M_HMI
|
||||
import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotStatusListener
|
||||
import com.mogo.eagle.core.function.api.bindingcar.IMoGoBindingCarListener
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutoPilotStatusListenerManager
|
||||
import com.mogo.eagle.core.function.call.bindingcar.CallerBindingCarListenerManager
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
|
||||
import com.mogo.eagle.core.function.call.bindingcar.CallerBindingcarManager
|
||||
import com.mogo.eagle.core.function.hmi.R
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.Logger
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.Companion.M_HMI
|
||||
import com.mogo.eagle.core.utilcode.util.AppUtils
|
||||
import com.mogo.eagle.core.utilcode.util.ThreadUtils
|
||||
import com.mogo.eagle.core.utilcode.util.ToastUtils
|
||||
@@ -58,7 +61,13 @@ class SystemVersionView @JvmOverloads constructor(
|
||||
//鹰眼版本视图点击事件
|
||||
ivPadVersion.setOnClickListener {
|
||||
CallerLogger.i("$M_HMI$$TAG", "pad version view clicked")
|
||||
// CallerBindingcarManager.getBindingcarProvider().queryAppUpgrade()
|
||||
|
||||
Logger.d("liyz", "ivPadVersion --click ")
|
||||
|
||||
}
|
||||
|
||||
|
||||
//工控机版本视图点击事件
|
||||
ivAdVersion.setOnClickListener {
|
||||
CallerLogger.i("$M_HMI$$TAG", "ad version view clicked")
|
||||
|
||||
@@ -53,7 +53,6 @@ class V2XWarningView @JvmOverloads constructor(
|
||||
* @param closeTime 倒计时
|
||||
*/
|
||||
fun showWarning(direction: WarningDirectionEnum, closeTime: Long) {
|
||||
// CallerLogger.d("$M_HMI$TAG", "预警红边:预警方向->$direction 预警倒计时->$closeTime")
|
||||
Log.d(TAG, "预警红边:预警方向->$direction 预警倒计时->$closeTime")
|
||||
UiThreadHandler.post {
|
||||
// 如果传入的不是关闭显示,则设置倒计时,定时关闭红框警示
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.mogo.eagle.core.widget.RoundConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="840px"
|
||||
android:layout_height="470px"
|
||||
android:background="@color/dialog_bg_color"
|
||||
app:roundLayoutRadius="32px">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_upgrade_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="50px"
|
||||
android:text="@string/application_upgrade"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="56px"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_upgrade_tips"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="50px"
|
||||
android:text="@string/application_upgrade_confirm"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="43px"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_bindingcar_title" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view_horizontal_line"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2px"
|
||||
android:layout_marginTop="80px"
|
||||
android:background="#66B8BFE8"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_bindingcar_tips" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view_vertical_line"
|
||||
android:layout_width="3px"
|
||||
android:layout_height="0dp"
|
||||
android:background="#66B8BFE8"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view_horizontal_line" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_upgrade_confirm"
|
||||
android:layout_width="400px"
|
||||
android:layout_height="100px"
|
||||
android:gravity="center"
|
||||
android:text="@string/confirm"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="46px"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/view_vertical_line"
|
||||
app:layout_constraintTop_toBottomOf="@id/view_horizontal_line" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_upgrade_cancel"
|
||||
android:layout_width="400px"
|
||||
android:layout_height="100px"
|
||||
android:gravity="center"
|
||||
android:text="@string/cancel"
|
||||
android:textColor="#FFFFFFFF"
|
||||
android:textSize="46px"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toRightOf="@id/view_vertical_line"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view_horizontal_line" />
|
||||
|
||||
|
||||
</com.mogo.eagle.core.widget.RoundConstraintLayout>
|
||||
@@ -1111,6 +1111,18 @@
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/btnConnectServerIp" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/tbReportWarning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/dp_10"
|
||||
android:textColor="#000"
|
||||
android:textOff="开启异常上报提示"
|
||||
android:textOn="关闭异常上报提示"
|
||||
android:textSize="@dimen/dp_24"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnConnectServerIp"
|
||||
/>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -1245,6 +1257,17 @@
|
||||
android:textOn="关闭「加速度面板」"
|
||||
android:textSize="@dimen/dp_24" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/tbRouteDynamicEffect"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_columnWeight="1"
|
||||
android:layout_margin="2dp"
|
||||
android:gravity="center"
|
||||
android:textOff="开启「引导线动态效果」"
|
||||
android:textOn="关闭「引导线动态效果」"
|
||||
android:textSize="@dimen/dp_24" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/etThreshold"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -1955,6 +1978,16 @@
|
||||
android:text="ADAS底盘数据"
|
||||
android:textColor="#000"
|
||||
android:textSize="@dimen/dp_24" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/cbAdasPlanningObj"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false"
|
||||
android:padding="@dimen/dp_10"
|
||||
android:text="ADAS PLANNING 感知障碍物"
|
||||
android:textColor="#000"
|
||||
android:textSize="@dimen/dp_24" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package com.mogo.eagle.core.function.map;
|
||||
|
||||
import static com.mogo.eagle.core.utilcode.mogo.logger.scene.SceneConstant.M_HMI;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import com.mogo.commons.AbsMogoApplication;
|
||||
import com.mogo.eagle.core.data.config.FunctionBuildConfig;
|
||||
import com.mogo.eagle.core.data.enums.TrafficTypeEnum;
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger;
|
||||
import com.mogo.map.MogoMap;
|
||||
import com.mogo.map.MogoMarkerManager;
|
||||
import com.mogo.module.common.MogoApisHandler;
|
||||
import com.mogo.module.common.constants.AdasRecognizedType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import mogo.telematics.pad.MessagePad;
|
||||
|
||||
/**
|
||||
* @author xiaoyuzhou
|
||||
* @date 2021/10/19 10:45 上午
|
||||
* 域控制器识别信息绘制
|
||||
*/
|
||||
public class IdentifyDataDrawer {
|
||||
private static final String TAG = "IdentifyDataDrawer";
|
||||
|
||||
protected final Context mContext;
|
||||
private static volatile IdentifyDataDrawer sInstance;
|
||||
|
||||
/**
|
||||
* 上一帧数据的缓存
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, MessagePad.TrackedObject> mMarkersCaches = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* kalman缓存数据
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, KalmanFilter> algoCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 记录每次实际绘制的交通元素UUID
|
||||
*/
|
||||
private final ArrayList<String> trafficDataUuidList = new ArrayList<>();
|
||||
/**
|
||||
* 过滤后的数据集合
|
||||
*/
|
||||
private final ArrayList<MessagePad.TrackedObject> mFilterTrafficData = new ArrayList<>();
|
||||
|
||||
private IdentifyDataDrawer() {
|
||||
mContext = AbsMogoApplication.getApp();
|
||||
}
|
||||
|
||||
public static IdentifyDataDrawer getInstance() {
|
||||
if (sInstance == null) {
|
||||
synchronized (IdentifyDataDrawer.class) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new IdentifyDataDrawer();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public synchronized void release() {
|
||||
sInstance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 adas 识别的数据
|
||||
*
|
||||
* @param resultList adas感知融合数据
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public void renderAdasRecognizedResult(List<MessagePad.TrackedObject> resultList) {
|
||||
if (resultList == null || resultList.isEmpty()) {
|
||||
clearOldMarker();
|
||||
CallerLogger.INSTANCE.w(TAG, "感知数据为空无需渲染……");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MogoApisHandler.getInstance().getApis().getStatusManagerApi().isVrMode()) {
|
||||
clearOldMarker();
|
||||
CallerLogger.INSTANCE.w(TAG, "渲染 adas 识别的数据 当前不是VR模式");
|
||||
return;
|
||||
}
|
||||
|
||||
//清除缓存
|
||||
for (MessagePad.TrackedObject data : resultList) {
|
||||
if (trafficDataUuidList.size() > 0 && trafficDataUuidList.contains("" + data.getUuid())) {
|
||||
trafficDataUuidList.remove("" + data.getUuid());
|
||||
}
|
||||
}
|
||||
trafficDataUuidList.forEach(uuid -> {
|
||||
mMarkersCaches.remove(uuid);
|
||||
algoCache.remove(uuid);
|
||||
});
|
||||
|
||||
ArrayList<MessagePad.TrackedObject> filterList = filterTrafficData(resultList);
|
||||
|
||||
if (filterList.size() > 0) {
|
||||
// 绘制新数据
|
||||
MogoMarkerManager.getInstance(mContext)
|
||||
.updateBatchMarkerPosition(filterList);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据过滤器
|
||||
*
|
||||
* @return 过滤后的数据集合
|
||||
*/
|
||||
private ArrayList<MessagePad.TrackedObject> filterTrafficData(List<MessagePad.TrackedObject> trafficData) {
|
||||
mFilterTrafficData.clear();
|
||||
trafficDataUuidList.clear();
|
||||
for (MessagePad.TrackedObject data : trafficData) {
|
||||
// 过滤掉未知感知数据
|
||||
if (!FunctionBuildConfig.isDrawUnknownIdentifyData && data.getType() == TrafficTypeEnum.TYPE_TRAFFIC_ID_WEI_ZHI.getType()) {
|
||||
//CallerLogger.INSTANCE.w(TAG, "未知感知类型数据,丢弃,不渲染");
|
||||
continue;
|
||||
}
|
||||
String uuid = "" + data.getUuid();
|
||||
//首次过来的数据不添加,首次未添加的感知物在调用完绘制方法后再塞入cache map
|
||||
MessagePad.TrackedObject cacheData = mMarkersCaches.get(uuid);
|
||||
if (cacheData != null) {
|
||||
if (data.getSpeed() < 0.5) {
|
||||
data = data.toBuilder().setHeading(cacheData.getHeading()).setLongitude(cacheData.getLongitude()).setLatitude(cacheData.getLatitude()).build();
|
||||
}
|
||||
mFilterTrafficData.add(data);
|
||||
//更新已存在的感知物体数据
|
||||
}
|
||||
mMarkersCaches.put(uuid, data);
|
||||
trafficDataUuidList.add(uuid);
|
||||
}
|
||||
return mFilterTrafficData;
|
||||
}
|
||||
|
||||
//todo 相信滤波的定位点做验证,将原始data修改经纬度和航向角返回
|
||||
private MessagePad.TrackedObject kalmanCorrectData(MessagePad.TrackedObject data) {
|
||||
String uuid = "" + data.getUuid();
|
||||
if (algoCache.containsKey(uuid)) {
|
||||
Object o = algoCache.get(uuid);
|
||||
KalmanFilter kf = (KalmanFilter) o;
|
||||
assert kf != null;
|
||||
algoCache.put(uuid, kf);
|
||||
MessagePad.TrackedObject cacheTrackObj = mMarkersCaches.get(uuid);
|
||||
assert cacheTrackObj != null;
|
||||
if (data.getSpeed() < 0.5) {
|
||||
return data.toBuilder().setHeading(cacheTrackObj.getHeading()).setLongitude(cacheTrackObj.getLongitude()).setLatitude(cacheTrackObj.getLatitude()).build();
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
double r = 0.000005;
|
||||
if (AdasRecognizedType.valueFrom(data.getType()) == AdasRecognizedType.classIdTrafficBus || AdasRecognizedType.valueFrom(data.getType()) == AdasRecognizedType.classIdTrafficTruck) {
|
||||
r = 0.00001;
|
||||
}
|
||||
algoCache.put(uuid, new KalmanFilter(data.getLongitude(), data.getLatitude(), r));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除旧的 marker 数据
|
||||
*/
|
||||
public void clearOldMarker() {
|
||||
for (String uuid : trafficDataUuidList) {
|
||||
MogoMarkerManager.getInstance(mContext)
|
||||
.removeMarker(uuid);
|
||||
}
|
||||
trafficDataUuidList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.mogo.eagle.core.data.config.FunctionBuildConfig
|
||||
import com.mogo.eagle.core.function.api.autopilot.IMoGoAutopilotIdentifyListener
|
||||
import com.mogo.eagle.core.function.api.base.IMoGoSubscriber
|
||||
import com.mogo.eagle.core.function.call.autopilot.CallerAutopilotIdentifyListenerManager
|
||||
import com.mogo.eagle.core.function.map.identify.IdentifyFactory
|
||||
import com.mogo.eagle.core.utilcode.util.ThreadUtils
|
||||
import mogo.telematics.pad.MessagePad
|
||||
import mogo.telematics.pad.MessagePad.TrackedObject
|
||||
@@ -41,9 +42,19 @@ class MapIdentifySubscriber private constructor() : IMoGoSubscriber, IMoGoAutopi
|
||||
override fun onAutopilotIdentifyDataUpdate(trafficData: List<TrackedObject>?) {
|
||||
try {
|
||||
if (FunctionBuildConfig.isDrawIdentifyData) {
|
||||
ThreadUtils.getSinglePool().execute { IdentifyDataDrawer.getInstance().renderAdasRecognizedResult(trafficData) }
|
||||
ThreadUtils.getSinglePool().execute { IdentifyFactory.renderAdasRecognizedResult(trafficData) }
|
||||
} else {
|
||||
IdentifyDataDrawer.getInstance().clearOldMarker()
|
||||
IdentifyFactory.clearOldMarker()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAutopilotIdentifyPlanningObj(planningObjects: List<MessagePad.PlanningObject>?) {
|
||||
try {
|
||||
if (FunctionBuildConfig.isDrawIdentifyData) {
|
||||
ThreadUtils.getSinglePool().execute { IdentifyFactory.renderPlanningWarningObj(planningObjects) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.mogo.eagle.core.function.map.identify;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
public class CircleQueue {
|
||||
|
||||
private final Vector<ObjQueue> objQueue;
|
||||
private final int maxSize;
|
||||
|
||||
public CircleQueue(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
objQueue = new Vector<>(maxSize);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return objQueue.size();
|
||||
}
|
||||
|
||||
public void addQueue(ObjQueue obj) {
|
||||
if (objQueue.size() == maxSize) {
|
||||
objQueue.remove(0);
|
||||
}
|
||||
objQueue.add(obj);
|
||||
}
|
||||
|
||||
public void deleteObj(ObjQueue obj) {
|
||||
objQueue.remove(obj);
|
||||
}
|
||||
|
||||
public List<ObjQueue> getLastThreeFrame() {
|
||||
return objQueue.subList(objQueue.size() - 3, objQueue.size());
|
||||
}
|
||||
|
||||
public List<ObjQueue> getLastFiveFrame() {
|
||||
return objQueue.subList(objQueue.size() - 5, objQueue.size());
|
||||
}
|
||||
|
||||
public List<ObjQueue> getPreFrame() {
|
||||
return objQueue.subList(0, objQueue.size());
|
||||
}
|
||||
|
||||
public ObjQueue getLastFrame() {
|
||||
return objQueue.lastElement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CircleQueue{" +
|
||||
"objQueue=" + objQueue +
|
||||
"size=" + objQueue.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.mogo.eagle.core.function.map.identify
|
||||
|
||||
import mogo.telematics.pad.MessagePad
|
||||
import mogo.telematics.pad.MessagePad.TrackedObject
|
||||
|
||||
interface Identify {
|
||||
|
||||
fun renderPlanningWarningObj(planningObjects: List<MessagePad.PlanningObject>?)
|
||||
|
||||
fun renderAdasRecognizedResult(resultList: List<TrackedObject>?)
|
||||
|
||||
fun clearOldMarker()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.mogo.eagle.core.function.map.identify
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.mogo.commons.AbsMogoApplication
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger.w
|
||||
import com.mogo.map.MogoMarkerManager
|
||||
import com.mogo.module.common.MogoApisHandler
|
||||
import mogo.telematics.pad.MessagePad
|
||||
import mogo.telematics.pad.MessagePad.TrackedObject
|
||||
|
||||
/**
|
||||
* 域控制器识别信息绘制
|
||||
*/
|
||||
class IdentifyBeautifyDataDrawer : Identify {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IdentifyDataDrawer"
|
||||
}
|
||||
|
||||
override fun renderPlanningWarningObj(planningObjects: List<MessagePad.PlanningObject>?) {
|
||||
TrackManager.getInstance().filterWarningData(planningObjects)
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 adas 识别的数据
|
||||
*
|
||||
* @param resultList adas感知融合数据
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
override fun renderAdasRecognizedResult(resultList: List<TrackedObject>?) {
|
||||
if (resultList == null || resultList.isEmpty()) {
|
||||
TrackManager.getInstance().clearAll()
|
||||
w(TAG, "感知数据为空无需渲染……")
|
||||
return
|
||||
}
|
||||
if (!MogoApisHandler.getInstance().apis.statusManagerApi.isVrMode) {
|
||||
TrackManager.getInstance().clearAll()
|
||||
w(TAG, "渲染 adas 识别的数据 当前不是VR模式")
|
||||
return
|
||||
}
|
||||
|
||||
//清除缓存
|
||||
TrackManager.getInstance().clearCache(resultList)
|
||||
// val cost = System.nanoTime()
|
||||
val filterList = TrackManager.getInstance().filterTrafficData(resultList)
|
||||
// Log.d(
|
||||
// "time cost",
|
||||
// " " + (System.nanoTime() - cost) / 1000000 + " , 处理了" + resultList.size + "条数据"
|
||||
// )
|
||||
if (filterList.size > 0) {
|
||||
// 绘制新数据
|
||||
MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
.updateBatchMarkerPosition(filterList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除旧的 marker 数据
|
||||
*/
|
||||
override fun clearOldMarker() {
|
||||
TrackManager.getInstance().clearAll()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.mogo.eagle.core.function.map.identify
|
||||
|
||||
import com.mogo.eagle.core.data.config.FunctionBuildConfig
|
||||
import com.mogo.eagle.core.utilcode.mogo.AppIdentityModeUtils
|
||||
import mogo.telematics.pad.MessagePad
|
||||
import mogo.telematics.pad.MessagePad.TrackedObject
|
||||
|
||||
object IdentifyFactory : Identify {
|
||||
object DriverIdentify {
|
||||
internal val originDataDrawer = IdentifyOriginDataDrawer()
|
||||
}
|
||||
|
||||
object UserIdentify {
|
||||
internal val beautifyDataDrawer = IdentifyBeautifyDataDrawer()
|
||||
}
|
||||
|
||||
override fun renderPlanningWarningObj(planningObjects: List<MessagePad.PlanningObject>?) {
|
||||
identify!!.renderPlanningWarningObj(planningObjects)
|
||||
}
|
||||
|
||||
override fun renderAdasRecognizedResult(resultList: List<TrackedObject>?) {
|
||||
identify!!.renderAdasRecognizedResult(resultList)
|
||||
}
|
||||
|
||||
override fun clearOldMarker() {
|
||||
identify!!.clearOldMarker()
|
||||
}
|
||||
|
||||
private var identify: Identify? = null
|
||||
|
||||
init { //todo 还得加开关做判断
|
||||
identify = if (AppIdentityModeUtils.isBus(FunctionBuildConfig.appIdentityMode)) {
|
||||
UserIdentify.beautifyDataDrawer
|
||||
} else {
|
||||
DriverIdentify.originDataDrawer
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.mogo.eagle.core.function.map.identify
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.mogo.commons.AbsMogoApplication
|
||||
import com.mogo.eagle.core.data.config.FunctionBuildConfig
|
||||
import com.mogo.eagle.core.data.enums.TrafficTypeEnum
|
||||
import com.mogo.eagle.core.utilcode.mogo.logger.CallerLogger.w
|
||||
import com.mogo.map.MogoMarkerManager
|
||||
import com.mogo.module.common.MogoApisHandler
|
||||
import mogo.telematics.pad.MessagePad
|
||||
import mogo.telematics.pad.MessagePad.TrackedObject
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* @author xiaoyuzhou
|
||||
* @date 2021/10/19 10:45 上午
|
||||
* 域控制器识别信息绘制
|
||||
*/
|
||||
class IdentifyOriginDataDrawer : Identify {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IdentifyDataDrawer"
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一帧数据的缓存
|
||||
*/
|
||||
private val mMarkersCaches = ConcurrentHashMap<String, TrackedObject>()
|
||||
|
||||
/**
|
||||
* kalman缓存数据
|
||||
*/
|
||||
private val algoCache = ConcurrentHashMap<String, KalmanFilter>()
|
||||
|
||||
/**
|
||||
* 记录每次实际绘制的交通元素UUID
|
||||
*/
|
||||
private val trafficDataUuidList = ArrayList<String>()
|
||||
|
||||
/**
|
||||
* 过滤后的数据集合
|
||||
*/
|
||||
private val mFilterTrafficData = ArrayList<TrackedObject>()
|
||||
|
||||
/**
|
||||
* planning 感知物预警缓存,用于重置color状态
|
||||
*/
|
||||
private val colorTrafficData = ArrayList<String>()
|
||||
|
||||
//todo reset color
|
||||
override fun renderPlanningWarningObj(planningObjects: List<MessagePad.PlanningObject>?) {
|
||||
// if (planningObjects == null) {
|
||||
// if (colorTrafficData.size == 0) {
|
||||
// return
|
||||
// }
|
||||
// colorTrafficData.forEach {
|
||||
// val cacheData = mMarkersCaches[it] //todo 是否要直接绘制 还是等下一帧
|
||||
// if (cacheData != null) {
|
||||
// mMarkersCaches[it] = cacheData.toBuilder().setColor("#D8D8D8FF").build()
|
||||
// }
|
||||
// }
|
||||
// colorTrafficData.clear()
|
||||
// return
|
||||
// }
|
||||
// val tempTrafficData = ArrayList<TrackedObject>()
|
||||
// planningObjects.forEach {
|
||||
// val trackId = it.uuid.toString()
|
||||
// val cacheData = mMarkersCaches[trackId]
|
||||
// if (cacheData != null) {
|
||||
// colorTrafficData.add(trackId)
|
||||
// when (it.type) {
|
||||
// 0 -> {
|
||||
// tempTrafficData.add(cacheData.toBuilder().setColor("#FF3C45FF").build())
|
||||
// }
|
||||
// 1 -> {
|
||||
// tempTrafficData.add(cacheData.toBuilder().setColor("#FFD53EFF").build())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
// .updateBatchMarkerPosition(tempTrafficData)
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染 adas 识别的数据
|
||||
*
|
||||
* @param resultList adas感知融合数据
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
override fun renderAdasRecognizedResult(resultList: List<TrackedObject>?) {
|
||||
if (resultList == null || resultList.isEmpty()) {
|
||||
clearOldMarker()
|
||||
w(TAG, "感知数据为空无需渲染……")
|
||||
return
|
||||
}
|
||||
if (!MogoApisHandler.getInstance().apis.statusManagerApi.isVrMode) {
|
||||
clearOldMarker()
|
||||
w(TAG, "渲染 adas 识别的数据 当前不是VR模式")
|
||||
return
|
||||
}
|
||||
|
||||
//清除缓存
|
||||
for (data in resultList) {
|
||||
if (trafficDataUuidList.size > 0 && trafficDataUuidList.contains("" + data.uuid)) {
|
||||
trafficDataUuidList.remove("" + data.uuid)
|
||||
}
|
||||
}
|
||||
trafficDataUuidList.forEach(Consumer { uuid: String ->
|
||||
mMarkersCaches.remove(uuid)
|
||||
algoCache.remove(uuid)
|
||||
MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
.removeMarker(uuid)
|
||||
})
|
||||
val filterList = filterTrafficData(resultList)
|
||||
if (filterList.size > 0) {
|
||||
// 绘制新数据
|
||||
MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
.updateBatchMarkerPosition(filterList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据过滤器
|
||||
*
|
||||
* @return 过滤后的数据集合
|
||||
*/
|
||||
private fun filterTrafficData(trafficData: List<TrackedObject>): ArrayList<TrackedObject> {
|
||||
mFilterTrafficData.clear()
|
||||
trafficDataUuidList.clear()
|
||||
for (data in trafficData) {
|
||||
// 过滤掉未知感知数据
|
||||
if (!FunctionBuildConfig.isDrawUnknownIdentifyData && data.type == TrafficTypeEnum.TYPE_TRAFFIC_ID_WEI_ZHI.type) {
|
||||
//CallerLogger.INSTANCE.w(TAG, "未知感知类型数据,丢弃,不渲染");
|
||||
continue
|
||||
}
|
||||
val uuid = "" + data.uuid
|
||||
//首次过来的数据不添加,首次未添加的感知物在调用完绘制方法后再塞入cache map
|
||||
val cacheData = mMarkersCaches[uuid]
|
||||
if (cacheData != null) {
|
||||
if (data.speed < 0.5) {
|
||||
data.toBuilder().setHeading(cacheData.heading).setLongitude(cacheData.longitude)
|
||||
.setLatitude(cacheData.latitude).build()
|
||||
}
|
||||
mFilterTrafficData.add(data)
|
||||
//更新已存在的感知物体数据
|
||||
}
|
||||
mMarkersCaches[uuid] = data
|
||||
trafficDataUuidList.add(uuid)
|
||||
}
|
||||
return mFilterTrafficData
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除旧的 marker 数据
|
||||
*/
|
||||
override fun clearOldMarker() {
|
||||
for (uuid in trafficDataUuidList) {
|
||||
MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
.removeMarker(uuid)
|
||||
}
|
||||
trafficDataUuidList.clear()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.mogo.eagle.core.function.map;
|
||||
package com.mogo.eagle.core.function.map.identify;
|
||||
|
||||
public class KalmanFilter {
|
||||
private final double q = 1.0E-6D;
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.mogo.eagle.core.function.map.identify;
|
||||
|
||||
public class ObjQueue {
|
||||
|
||||
private double heading;
|
||||
private double speed;
|
||||
private int type;
|
||||
|
||||
public ObjQueue(double heading, double speed, int type) {
|
||||
this.heading = heading;
|
||||
this.speed = speed;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public double getHeading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
public void setHeading(double heading) {
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
public double getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public void setSpeed(double speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ObjQueue{" +
|
||||
"heading=" + heading +
|
||||
", speed=" + speed +
|
||||
", type=" + type +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.mogo.eagle.core.function.map.identify;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
import com.google.common.collect.BiMap;
|
||||
import com.google.common.collect.HashBiMap;
|
||||
import com.mogo.commons.AbsMogoApplication;
|
||||
import com.mogo.eagle.core.data.config.FunctionBuildConfig;
|
||||
import com.mogo.eagle.core.data.enums.TrafficTypeEnum;
|
||||
import com.mogo.map.MogoMarkerManager;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import mogo.telematics.pad.MessagePad;
|
||||
|
||||
public class TrackManager {
|
||||
|
||||
private static final class TrackOwner {
|
||||
private static final TrackManager trackManager = new TrackManager();
|
||||
}
|
||||
|
||||
public static TrackManager getInstance() {
|
||||
return TrackOwner.trackManager;
|
||||
}
|
||||
|
||||
public static final DecimalFormat DF = new DecimalFormat("0.000000");
|
||||
public static final int DISTANCE = 6371000;
|
||||
public static double LIMIT_SPEED = 0.5;
|
||||
|
||||
/**
|
||||
* marker缓存队列
|
||||
*/
|
||||
private final ArrayMap<String, TrackObj> mMarkersCaches = new ArrayMap<>();
|
||||
|
||||
/**
|
||||
* marker s2 cellId缓存队列,空间换时间
|
||||
*/
|
||||
private final BiMap<String, Long> cellIdCaches = HashBiMap.create();
|
||||
|
||||
private final ArrayMap<String, TrackObj> recentCaches = new ArrayMap<>();
|
||||
|
||||
/**
|
||||
* 记录每次实际绘制的交通元素UUID
|
||||
*/
|
||||
private final Set<String> trafficDataUuidList = new ArraySet<>();
|
||||
|
||||
/**
|
||||
* 过滤后的数据集合
|
||||
*/
|
||||
private final ArrayList<MessagePad.TrackedObject> mFilterTrafficData = new ArrayList<>();
|
||||
|
||||
public ArrayList<MessagePad.TrackedObject> filterTrafficData(List<MessagePad.TrackedObject> trafficData) {
|
||||
//清空上次返回数据,做到缓存复用
|
||||
mFilterTrafficData.clear();
|
||||
trafficDataUuidList.clear();
|
||||
//进入过滤机制的感知物体,首先从缓存队列中进行查找 uuid
|
||||
for (MessagePad.TrackedObject data : trafficData) {
|
||||
|
||||
// todo 过滤掉未知感知数据,后面会依据危险等级显示
|
||||
if (!FunctionBuildConfig.isDrawUnknownIdentifyData && data.getType() == TrafficTypeEnum.TYPE_TRAFFIC_ID_WEI_ZHI.getType()) {
|
||||
//CallerLogger.INSTANCE.w(TAG, "未知感知类型数据,丢弃,不渲染");
|
||||
continue;
|
||||
}
|
||||
|
||||
String uuid = "" + data.getUuid();
|
||||
TrackObj trackObj = mMarkersCaches.get(uuid);
|
||||
if (trackObj != null) {
|
||||
data = trackObj.updateObj(data);
|
||||
mFilterTrafficData.add(data);
|
||||
} else {
|
||||
trackObj = new TrackObj(data);
|
||||
// 判断是否有重合元素 google s2
|
||||
if (cellIdCaches.containsValue(trackObj.getCellIdPos())) {
|
||||
String findSameValue = cellIdCaches.inverse().get(trackObj.getCellIdPos());
|
||||
if (data.getUuid() - Integer.parseInt(findSameValue) > 0) {
|
||||
// Log.d("0609", "uuid : " + findSameValue + " 与新感知物 : " + uuid + " , 出现相同pos : " + trackObj.getCellIdPos());
|
||||
uuid = findSameValue;
|
||||
data = data.toBuilder().setUuid(Integer.parseInt(findSameValue)).build();
|
||||
data = trackObj.updateObj(data);
|
||||
mFilterTrafficData.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
cellIdCaches.forcePut(uuid, trackObj.getCellIdPos());
|
||||
mMarkersCaches.put(uuid, trackObj);
|
||||
Log.d("hy uuid : " + uuid, " 显示物体,塞入set");
|
||||
trafficDataUuidList.add(uuid);
|
||||
}
|
||||
//todo 将上次没被删除掉物体加入集合,造成延迟删除,对运动物体不友好
|
||||
// Iterator it = recentCaches.keySet().iterator();
|
||||
// while (it.hasNext()) {
|
||||
// String key = (String) it.next();
|
||||
// TrackObj trackObj = recentCaches.get(key);
|
||||
// if(trackObj == null){
|
||||
// continue;
|
||||
// }
|
||||
// if(!trackObj.relativeStatic()){
|
||||
// continue;
|
||||
// }
|
||||
// mFilterTrafficData.add(trackObj.getCache());
|
||||
// }
|
||||
return mFilterTrafficData;
|
||||
}
|
||||
|
||||
//todo reset color
|
||||
public void filterWarningData(List<MessagePad.PlanningObject> planningObjects) {
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public void clearCache(List<MessagePad.TrackedObject> resultList) {
|
||||
//清除缓存
|
||||
for (MessagePad.TrackedObject data : resultList) {
|
||||
if (trafficDataUuidList.size() > 0 && trafficDataUuidList.contains("" + data.getUuid())) {
|
||||
trafficDataUuidList.remove("" + data.getUuid());
|
||||
}
|
||||
}
|
||||
trafficDataUuidList.forEach(uuid -> {
|
||||
Log.d("hy uuid : " + uuid, " 移除物体");
|
||||
mMarkersCaches.remove(uuid);
|
||||
cellIdCaches.remove(uuid);
|
||||
MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
.removeMarker(uuid);
|
||||
});
|
||||
//todo bus存在时间回溯,将id重置,会有id复用问题,导致鹰眼展示元素缺少
|
||||
// Iterator it = mMarkersCaches.keySet().iterator();
|
||||
// while (it.hasNext()) {
|
||||
// String key = (String) it.next();
|
||||
// TrackObj trackObj = mMarkersCaches.get(key);
|
||||
// if (trackObj != null && Math.abs(CallerAutoPilotStatusListenerManager.INSTANCE.getCurWgs84SatelliteTime() - trackObj.getRecentlyTime()) >= 2000) {
|
||||
//// Log.d("track", "clearCache uuid : " + key + " , time : " + (CallerAutoPilotStatusListenerManager.INSTANCE.getCurWgs84SatelliteTime() - trackObj.getRecentlyTime()));
|
||||
// mMarkersCaches.remove(key);
|
||||
// cellIdCaches.remove(key);
|
||||
// recentCaches.remove(key);
|
||||
// MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
// .removeMarker(key);
|
||||
// } else {
|
||||
// recentCaches.put(key, trackObj);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
public void clearAll() {
|
||||
trafficDataUuidList.clear();
|
||||
Iterator it = mMarkersCaches.keySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
String key = (String) it.next();
|
||||
mMarkersCaches.remove(key);
|
||||
cellIdCaches.remove(key);
|
||||
recentCaches.remove(key);
|
||||
MogoMarkerManager.getInstance(AbsMogoApplication.getApp())
|
||||
.removeMarker(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.mogo.eagle.core.function.map.identify;
|
||||
|
||||
import static com.mogo.eagle.core.function.map.identify.TrackManager.DISTANCE;
|
||||
import static com.mogo.eagle.core.function.map.identify.TrackManager.LIMIT_SPEED;
|
||||
|
||||
import com.mogo.eagle.core.function.call.map.CallerHDMapManager;
|
||||
import com.mogo.eagle.core.utilcode.geometry.S2CellId;
|
||||
import com.mogo.eagle.core.utilcode.geometry.S2LatLng;
|
||||
import com.mogo.module.service.Utils;
|
||||
import com.mogo.eagle.core.data.map.CenterLine;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import mogo.telematics.pad.MessagePad;
|
||||
|
||||
public class TrackObj {
|
||||
|
||||
private final CircleQueue circleQueue = new CircleQueue(10);
|
||||
private final KalmanFilter kalmanFilter; //卡尔曼结果
|
||||
private S2CellId s2CellId; //s2 id权重
|
||||
private S2LatLng s2LatLng; //s2 经纬度
|
||||
private long recentlyTime; //用于缓存帧数判断,暂定缓存1秒数据,中间如果有物体未出现,1秒后删除
|
||||
private double roadAngle; //道路航向
|
||||
private double headingDelta; //航向角德尔塔
|
||||
private double typeWeight; //类型权重
|
||||
private double lat;
|
||||
private double lon;
|
||||
private double speedAverage;
|
||||
// private SinglePointRoadInfo singlePointRoadInfo;
|
||||
// private double[] matchedPoint;
|
||||
|
||||
public TrackObj(MessagePad.TrackedObject data) {
|
||||
kalmanFilter = new KalmanFilter(data.getLongitude(), data.getLatitude(), 0.0000005);
|
||||
circleQueue.addQueue(new ObjQueue(data.getHeading(), data.getSpeed(), data.getType()));
|
||||
recentlyTime = Double.valueOf(data.getSatelliteTime() * 1000).longValue();
|
||||
lat = data.getLatitude();
|
||||
lon = data.getLongitude();
|
||||
s2LatLng = S2LatLng.fromDegrees(data.getLatitude(), data.getLongitude());
|
||||
s2CellId = S2CellId.fromLatLng(s2LatLng).parent(22); //需要验证22前后
|
||||
CenterLine centerLine = CallerHDMapManager.INSTANCE.getCenterLineInfo(lon, lat, -1);
|
||||
if (centerLine != null && centerLine.getAngle() != 0) {
|
||||
roadAngle = centerLine.getAngle();
|
||||
}
|
||||
}
|
||||
|
||||
private MessagePad.TrackedObject cacheData;
|
||||
|
||||
//先处理kalman数据,将经纬度校准后,放入缓存队列,然后基于后序策略将各个项进行校准
|
||||
public MessagePad.TrackedObject updateObj(MessagePad.TrackedObject data) {
|
||||
cacheData = data;
|
||||
|
||||
correct();
|
||||
|
||||
recentlyTime = Double.valueOf(data.getSatelliteTime() * 1000).longValue();
|
||||
// Log.d("calHeading uuid : " + cacheData.getUuid(), "result heading : " + cacheData.getHeading() + " speed : " + cacheData.getSpeed());
|
||||
circleQueue.addQueue(new ObjQueue(cacheData.getHeading(), cacheData.getSpeed(), cacheData.getType()));
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
private void correct() {
|
||||
calAverageSpeed();
|
||||
calLoc();
|
||||
calHeading();
|
||||
calType();
|
||||
}
|
||||
|
||||
private void calAverageSpeed() {
|
||||
//计算平均速度
|
||||
if (circleQueue.size() >= 5) {
|
||||
List<ObjQueue> objQueueList = circleQueue.getLastFiveFrame();
|
||||
speedAverage = (objQueueList.get(0).getSpeed() + objQueueList.get(1).getSpeed() + objQueueList.get(2).getSpeed() + objQueueList.get(3).getSpeed() + objQueueList.get(4).getSpeed()) / 5;
|
||||
} else {
|
||||
double cal = 0;
|
||||
List<ObjQueue> objQueueList = circleQueue.getPreFrame();
|
||||
for (ObjQueue obj : objQueueList) {
|
||||
cal += obj.getSpeed();
|
||||
}
|
||||
speedAverage = cal / objQueueList.size();
|
||||
// speedAverage = circleQueue.getLastFrame().getSpeed();
|
||||
}
|
||||
}
|
||||
|
||||
private void calLoc() {
|
||||
//距离计算,位置修正
|
||||
//todo bus250 taxi上测试下面注释掉内容
|
||||
//double[] lonLat = kalmanFilter.filter(cacheData.getLongitude(), cacheData.getLatitude());
|
||||
// double distance = s2LatLng.getDistance(S2LatLng.fromDegrees(lonLat[1], lonLat[0])).distance(DISTANCE);
|
||||
// double distance = s2LatLng.getDistance(S2LatLng.fromDegrees(cacheData.getLatitude(), cacheData.getLongitude())).distance(DISTANCE);
|
||||
//todo 重新计算速度值(如果连续几帧distance累加到一定值,速度没变化,需要重新计算速度,防止锁死)
|
||||
// if (relativeStatic()) {
|
||||
// double tempDis = distance;
|
||||
// if (distance >= 4) { //(150km/h) 41.6m/s x 0.1s = 4.16m 约等于 4
|
||||
// tempDis = 4;
|
||||
// }
|
||||
// double calSpeed = cacheData.getSpeed();
|
||||
// if (cacheData.getSpeed() != 0.0) {
|
||||
// calSpeed = tempDis / ((Double.valueOf(cacheData.getSatelliteTime() * 1000).longValue() - recentlyTime) / 1000.0);
|
||||
//// Log.d("calSpeed uuid : " + cacheData.getUuid(), " tempDis : " + tempDis + " , 重新赋值 calSpeed : " + DF.format(calSpeed) + " , time : " + (Double.valueOf(cacheData.getSatelliteTime() * 1000).longValue() - recentlyTime) + " , 原速度 : " + cacheData.getSpeed());
|
||||
// if (calSpeed > cacheData.getSpeed()) {
|
||||
// calSpeed = cacheData.getSpeed();
|
||||
//// Log.d("calSpeed uuid : " + cacheData.getUuid(), " 二次重新赋值 calSpeed : " + DF.format(calSpeed));
|
||||
// }
|
||||
//// if (calSpeed > 2) {
|
||||
//// calSpeed = 2;
|
||||
////// Log.d("calSpeed uuid : " + cacheData.getUuid(), " 三次重新赋值 calSpeed : " + DF.format(calSpeed));
|
||||
//// }
|
||||
// }
|
||||
// cacheData = cacheData.toBuilder().setSpeed(calSpeed).build();
|
||||
// }
|
||||
//todo 等后序速度优化结果值可用,使用计算结果
|
||||
// double calDistance = (cacheData.getSpeed() * (Double.valueOf(cacheData.getSatelliteTime() * 1000).longValue() - recentlyTime)) / 1000.0;
|
||||
// double calDistance = Utils.calculateLineDistance(lon, lat, cacheData.getLongitude(), cacheData.getLatitude());
|
||||
// Log.d("calLoc uuid : " + cacheData.getUuid() + " calDistance : " + DF.format(calDistance), (calDistance * 2 < distance) ? "超出范围" : "正常值");
|
||||
//速度小于0.5m/s,并且距离在计算合理范围内超出2倍,则认为是相对静止状态(注意调整阈值),不更新缓存点信息
|
||||
// if (cacheData.getSpeed() < LIMIT_SPEED || relativeStatic() || calDistance * 2 < distance) {
|
||||
if (relativeStatic()) {
|
||||
// if (singlePointRoadInfo == null) {
|
||||
// double angle = roadAngle != 0 ? roadAngle : cacheData.getHeading();
|
||||
// long cost = System.nanoTime();
|
||||
// singlePointRoadInfo = MapDataApi.INSTANCE.getSinglePointMatchRoad(lon, lat, (float) angle, true, true);
|
||||
// Log.d("hy create cost", " " + (System.nanoTime() - cost) / 1000000);
|
||||
// }
|
||||
// if (singlePointRoadInfo != null && singlePointRoadInfo.getCoords() != null && !singlePointRoadInfo.getCoords().isEmpty()) {
|
||||
// if(matchedPoint == null || matchedPoint.length == 0){
|
||||
// long cost = System.nanoTime();
|
||||
// matchedPoint = PointInterpolatorUtil.mergeToRoad(cacheData.getLongitude(), cacheData.getLatitude(), singlePointRoadInfo.getCoords());
|
||||
// Log.d("hy matchedPoint cost", " " + (System.nanoTime() - cost) / 1000000);
|
||||
// Log.d("hy uuid : " + cacheData.getUuid(), "道路经纬度 lon : " + matchedPoint[0] + " lat : " + matchedPoint[1] + " distance : " + matchedPoint[2] + " , 原数据 lon : " + lon + " lat : " + lat);
|
||||
// }else{
|
||||
// if(matchedPoint[0] == 0 || matchedPoint[1] == 0){
|
||||
// cacheData = cacheData.toBuilder().setLongitude(lon).setLatitude(lat).build();
|
||||
// }else{
|
||||
// cacheData = cacheData.toBuilder().setLongitude(matchedPoint[0]).setLatitude(matchedPoint[1]).build();
|
||||
// }
|
||||
// }
|
||||
// lat = matchedPoint[1];
|
||||
// lon = matchedPoint[0];
|
||||
// } else {
|
||||
// Log.d("hy uuid : " + cacheData.getUuid(), "未匹配到道路数据,使用原数据 lon : " + lon + " lat : " + lat);
|
||||
// }
|
||||
cacheData = cacheData.toBuilder().setLongitude(lon).setLatitude(lat).build();
|
||||
} else {
|
||||
//不在阈值内则更新,代表物体移动,使用卡尔曼滤波经纬度数据
|
||||
lat = cacheData.getLatitude();
|
||||
lon = cacheData.getLongitude();
|
||||
s2LatLng = S2LatLng.fromDegrees(cacheData.getLatitude(), cacheData.getLongitude());
|
||||
s2CellId = S2CellId.fromLatLng(s2LatLng).parent(22);
|
||||
// cacheData = cacheData.toBuilder().setLongitude(lonLat[0]).setLatitude(lonLat[1]).build();
|
||||
}
|
||||
}
|
||||
|
||||
private void calHeading() {
|
||||
double newDelta;
|
||||
ObjQueue lastObj;
|
||||
if (circleQueue.size() >= 3) {
|
||||
//计算差量
|
||||
List<ObjQueue> objQueueList = circleQueue.getLastThreeFrame();
|
||||
lastObj = objQueueList.get(2);
|
||||
|
||||
double firstDelta = objQueueList.get(1).getHeading() - objQueueList.get(0).getHeading();
|
||||
double secondDelta = objQueueList.get(2).getHeading() - objQueueList.get(1).getHeading();
|
||||
newDelta = Math.abs(cacheData.getHeading() - lastObj.getHeading());
|
||||
//按帧与帧之间的顺序变化
|
||||
double abs = Math.abs(firstDelta - secondDelta);
|
||||
//存在180度转向(有一帧出现错误)
|
||||
if (Math.abs(abs - 180) < 5) {
|
||||
headingDelta = firstDelta - secondDelta;
|
||||
} else if (abs < 5) { //两帧之间差量比较均匀
|
||||
headingDelta = firstDelta - secondDelta;
|
||||
} else if (Math.abs(abs - 180) > 5 && newDelta < 5) { //前两帧数据中出现异常值,相信后序帧
|
||||
headingDelta = newDelta;
|
||||
}
|
||||
} else {
|
||||
lastObj = circleQueue.getLastFrame();
|
||||
newDelta = Math.abs(cacheData.getHeading() - lastObj.getHeading());
|
||||
headingDelta = newDelta;
|
||||
}
|
||||
//更正数据,速度小于LIMIT_SPEED使用上一帧数据
|
||||
if (relativeStatic()) {
|
||||
if (roadAngle != 0.0) {
|
||||
CenterLine centerLine = CallerHDMapManager.INSTANCE.getCenterLineInfo(lon, lat, -1);
|
||||
if (centerLine != null && centerLine.getAngle() != 0) {
|
||||
cacheData = cacheData.toBuilder().setHeading(centerLine.getAngle()).build();
|
||||
} else {
|
||||
// Log.d("hy uuid : " + cacheData.getUuid(), "未获取到道路航向,使用上一帧 : " + circleQueue.getLastFrame().getHeading());
|
||||
cacheData = cacheData.toBuilder().setHeading(circleQueue.getLastFrame().getHeading()).build();
|
||||
}
|
||||
} else {
|
||||
// Log.d("hy uuid : " + cacheData.getUuid(), "未获取到道路航向,使用上一帧 : " + circleQueue.getLastFrame().getHeading());
|
||||
cacheData = cacheData.toBuilder().setHeading(circleQueue.getLastFrame().getHeading()).build();
|
||||
}
|
||||
|
||||
}
|
||||
//速度大于LIMIT_SPEED并出现大幅度转向使用缓存帧和delta数据
|
||||
if (cacheData.getSpeed() >= LIMIT_SPEED && newDelta > 10 && headingDelta != 0.0) {
|
||||
// Log.i("0609", "uuid : " + cacheData.getUuid() + " 修正航向角 last : " + lastObj.getHeading() + " , 增益 : " + headingDelta);
|
||||
cacheData = cacheData.toBuilder().setHeading(lastObj.getHeading() + headingDelta).build();
|
||||
}
|
||||
}
|
||||
|
||||
private void calType() {
|
||||
|
||||
}
|
||||
|
||||
public long getRecentlyTime() {
|
||||
return recentlyTime;
|
||||
}
|
||||
|
||||
public long getCellIdPos() {
|
||||
return s2CellId.pos();
|
||||
}
|
||||
|
||||
public MessagePad.TrackedObject getCache() {
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
public boolean relativeStatic() {
|
||||
return speedAverage < LIMIT_SPEED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TrackObj{" +
|
||||
"circleQueue=" + circleQueue +
|
||||
", s2CellId=" + s2CellId +
|
||||
", recentlyTime=" + recentlyTime +
|
||||
", cacheData=" + cacheData +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -166,13 +166,17 @@ public class SmallMapDirectionView
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
if (mCarMarker == null){
|
||||
mCarMarker = mAMap.addMarker(new MarkerOptions()
|
||||
.icon(BitmapDescriptorFactory.fromResource(R.drawable.module_small_map_view_my_location_logo))
|
||||
.anchor(0.5f, 0.5f));
|
||||
}
|
||||
if(mCarMarker == null){
|
||||
return;
|
||||
}
|
||||
LatLng currentLatLng = new LatLng(location.getLatitude(), location.getLongitude());
|
||||
//更新车辆位置
|
||||
if (mCarMarker != null) {
|
||||
// mCarMarker.setRotateAngle(location.getBearing());
|
||||
mCarMarker.setPosition(currentLatLng);
|
||||
// mCarMarker.setToTop();
|
||||
}
|
||||
mCarMarker.setPosition(currentLatLng);
|
||||
|
||||
CameraPosition cameraPosition;
|
||||
if (mCoordinatesLatLng.size() > 1) {
|
||||
|
||||
@@ -466,7 +466,6 @@ object V2XEventManager : IMoGoMapLocationListener, IMoGoTokenCallback, IV2XCallb
|
||||
l3.location = V2XMarkerLocation().also { l4 ->
|
||||
l4.lat = this.roadwork?.center?.point?.lat ?: 0.0
|
||||
l4.lon = this.roadwork?.center?.point?.lon ?: 0.0
|
||||
l4.angle = -1.0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class V2XAlarmServer {
|
||||
// 因为集合是按照距离排序后的所以这里检索出来第一个就发出警告
|
||||
for (V2XRoadEventEntity v2XRoadEventEntity : v2XRoadEventEntityList) {
|
||||
// 0、道路事件必须有朝向,角度>=0;
|
||||
boolean ignoreAngle = v2XRoadEventEntity.getLocation().getAngle() < 0;
|
||||
boolean ignoreAngle = EventTypeEnum.AI_ROAD_WORK.getPoiType().equals(v2XRoadEventEntity.getPoiType());
|
||||
if (v2XRoadEventEntity.getLocation().getAngle() >= 0 || ignoreAngle) {
|
||||
// 计算车辆距离指定气泡的距离
|
||||
MarkerLocation eventLocation = v2XRoadEventEntity.getLocation();
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.mogo.eagle.core.data.bindingcar;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author lixiaopeng
|
||||
* @description
|
||||
* @since 6/21/22
|
||||
*/
|
||||
public class AppInfo implements Serializable {
|
||||
private IdInfo _id;
|
||||
private String bk_inst_id;
|
||||
private String bk_inst_name;
|
||||
private String bk_supplier_account;
|
||||
private int screen_type; //1,司机屏,2乘客屏
|
||||
private String sn;
|
||||
private long last_time;
|
||||
private String app_url;
|
||||
private String bk_obj_id;
|
||||
private String version_code;
|
||||
private String version_name;
|
||||
private long create_time;
|
||||
|
||||
public IdInfo get_id() {
|
||||
return _id;
|
||||
}
|
||||
|
||||
public void set_id(IdInfo _id) {
|
||||
this._id = _id;
|
||||
}
|
||||
|
||||
public String getBk_inst_id() {
|
||||
return bk_inst_id;
|
||||
}
|
||||
|
||||
public void setBk_inst_id(String bk_inst_id) {
|
||||
this.bk_inst_id = bk_inst_id;
|
||||
}
|
||||
|
||||
public String getBk_inst_name() {
|
||||
return bk_inst_name;
|
||||
}
|
||||
|
||||
public void setBk_inst_name(String bk_inst_name) {
|
||||
this.bk_inst_name = bk_inst_name;
|
||||
}
|
||||
|
||||
public String getBk_supplier_account() {
|
||||
return bk_supplier_account;
|
||||
}
|
||||
|
||||
public void setBk_supplier_account(String bk_supplier_account) {
|
||||
this.bk_supplier_account = bk_supplier_account;
|
||||
}
|
||||
|
||||
public int getScreen_type() {
|
||||
return screen_type;
|
||||
}
|
||||
|
||||
public void setScreen_type(int screen_type) {
|
||||
this.screen_type = screen_type;
|
||||
}
|
||||
|
||||
public String getSn() {
|
||||
return sn;
|
||||
}
|
||||
|
||||
public void setSn(String sn) {
|
||||
this.sn = sn;
|
||||
}
|
||||
|
||||
public long getLast_time() {
|
||||
return last_time;
|
||||
}
|
||||
|
||||
public void setLast_time(long last_time) {
|
||||
this.last_time = last_time;
|
||||
}
|
||||
|
||||
public String getApp_url() {
|
||||
return app_url;
|
||||
}
|
||||
|
||||
public void setApp_url(String app_url) {
|
||||
this.app_url = app_url;
|
||||
}
|
||||
|
||||
public String getBk_obj_id() {
|
||||
return bk_obj_id;
|
||||
}
|
||||
|
||||
public void setBk_obj_id(String bk_obj_id) {
|
||||
this.bk_obj_id = bk_obj_id;
|
||||
}
|
||||
|
||||
public String getVersion_code() {
|
||||
return version_code;
|
||||
}
|
||||
|
||||
public void setVersion_code(String version_code) {
|
||||
this.version_code = version_code;
|
||||
}
|
||||
|
||||
public String getVersion_name() {
|
||||
return version_name;
|
||||
}
|
||||
|
||||
public void setVersion_name(String version_name) {
|
||||
this.version_name = version_name;
|
||||
}
|
||||
|
||||
public long getCreate_time() {
|
||||
return create_time;
|
||||
}
|
||||
|
||||
public void setCreate_time(long create_time) {
|
||||
this.create_time = create_time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AppInfo{" +
|
||||
"_id=" + _id +
|
||||
", bk_inst_id='" + bk_inst_id + '\'' +
|
||||
", bk_inst_name='" + bk_inst_name + '\'' +
|
||||
", bk_supplier_account='" + bk_supplier_account + '\'' +
|
||||
", screen_type=" + screen_type +
|
||||
", sn='" + sn + '\'' +
|
||||
", last_time=" + last_time +
|
||||
", app_url='" + app_url + '\'' +
|
||||
", bk_obj_id='" + bk_obj_id + '\'' +
|
||||
", version_code='" + version_code + '\'' +
|
||||
", version_name='" + version_name + '\'' +
|
||||
", create_time=" + create_time +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.mogo.eagle.core.data.bindingcar;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author lixiaopeng
|
||||
* @description
|
||||
* @since 6/21/22
|
||||
*/
|
||||
public class IdInfo implements Serializable {
|
||||
private String timestamp;
|
||||
private String date;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.mogo.eagle.core.data.bindingcar;
|
||||
|
||||
import com.mogo.eagle.core.data.BaseData;
|
||||
|
||||
/**
|
||||
* @author lixiaopeng
|
||||
* @description app升级管理
|
||||
* @since: 6/21/22
|
||||
*/
|
||||
public class UpgradeAppInfo extends BaseData {
|
||||
public AppInfo data;
|
||||
|
||||
public AppInfo getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(AppInfo data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UpgradeAppInfo{" +
|
||||
"data=" + data +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -117,4 +117,11 @@ object FunctionBuildConfig {
|
||||
@JvmField
|
||||
var skinMode = 0
|
||||
|
||||
/**
|
||||
* 是否进行异常上报提示
|
||||
*/
|
||||
@Volatile
|
||||
@JvmField
|
||||
var isReportWarning = true
|
||||
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class ChainConstant {
|
||||
const val CHAIN_LINK_LOG_WEB_SOCKET_TRAJECTORY = 4
|
||||
const val CHAIN_LINK_LOG_WEB_SOCKET_VEHICLE = 5
|
||||
const val CHAIN_LINK_LOG_WEB_SOCKET_TRAFFIC_LIGHT = 6
|
||||
const val CHAIN_LINK_LOG_WEB_SOCKET_PLANNING_OBJECTS = 7
|
||||
|
||||
const val CHAIN_LINK_LOG_ADAS_INIT = "-eagleInitStatus"
|
||||
const val CHAIN_LINK_LOG_ADAS_GNSS = "-adasWsGnssInfo"
|
||||
@@ -26,6 +27,7 @@ class ChainConstant {
|
||||
const val CHAIN_LINK_LOG_ADAS_TRAJECTORY = "-adasWsTrajectory"
|
||||
const val CHAIN_LINK_LOG_ADAS_VEHICLE = "-adasWsVehicle"
|
||||
const val CHAIN_LINK_LOG_ADAS_TRAFFIC_LIGHT = "-adasWsTrafficLight"
|
||||
const val CHAIN_LINK_LOG_ADAS_PLANNING_OBJECTS = "-adasWsPlanningObj"
|
||||
|
||||
const val CHAIN_ALIAS_CODE_MULTI_CONNECT = "CHAIN_ALIAS_CODE_MULTI_CONNECT"
|
||||
const val CHAIN_ALIAS_CODE_ADAS_MESSAGE_CAR_CONFIG = "CHAIN_ALIAS_CODE_CAR_CONFIG"
|
||||
@@ -39,6 +41,7 @@ class ChainConstant {
|
||||
const val CHAIN_ALIAS_CODE_ADAS_MESSAGE_AUTOPILOT_RECORD = "PAD_ADAS_MESSAGE_AUTOPILOT_RECORD"
|
||||
const val CHAIN_ALIAS_CODE_ADAS_MESSAGE_AUTOPILOT_VEHICLE = "PAD_ADAS_MESSAGE_AUTOPILOT_VEHICLE"
|
||||
const val CHAIN_ALIAS_CODE_ADAS_MESSAGE_AUTOPILOT_TRAFFIC_LIGHT = "PAD_ADAS_MESSAGE_AUTOPILOT_TRAFFIC_LIGHT"
|
||||
const val CHAIN_ALIAS_CODE_ADAS_MESSAGE_PLANNING_OBJECTS = "CHAIN_ALIAS_CODE_ADAS_MESSAGE_PLANNING_OBJECTS"
|
||||
const val CHAIN_ALIAS_CODE_ADAS_MESSAGE_AUTOPILOT_WARN = "PAD_ADAS_MESSAGE_AUTOPILOT_WARN"
|
||||
|
||||
const val CHAIN_ALIAS_CODE_CLOUD_CONNECT_FAIL = "CHAIN_ALIAS_CODE_CLOUD_CONNECT_FAIL"
|
||||
|
||||
@@ -19,9 +19,9 @@ enum class TrafficTypeEnum(
|
||||
TYPE_TRAFFIC_ID_WEI_ZHI(
|
||||
100,
|
||||
"未知数据",
|
||||
R.raw.fangkuang,
|
||||
R.raw.fangkuang,
|
||||
R.raw.fangkuang
|
||||
R.raw.traffic_xiankuang,
|
||||
R.raw.traffic_xiankuang,
|
||||
R.raw.traffic_xiankuang
|
||||
),
|
||||
TYPE_TRAFFIC_ID_PEOPLE(
|
||||
1,
|
||||
|
||||
@@ -17,6 +17,11 @@ interface IMoGoAutopilotIdentifyListener {
|
||||
*/
|
||||
fun onAutopilotIdentifyDataUpdate(trafficData: List<MessagePad.TrackedObject>?) {}
|
||||
|
||||
/**
|
||||
* planning识别感知预警物体
|
||||
*/
|
||||
fun onAutopilotIdentifyPlanningObj(planningObjects: List<MessagePad.PlanningObject>?){}
|
||||
|
||||
/**
|
||||
* 报警信息
|
||||
*
|
||||
|
||||
@@ -10,7 +10,17 @@ import java.util.List;
|
||||
* @since: 3/15/22
|
||||
*/
|
||||
public interface IMoGoBindingcarProvider extends IMoGoFunctionServerProvider {
|
||||
/**
|
||||
* 修改工控机的绑定关系
|
||||
* @param callBack
|
||||
*/
|
||||
void modifyCarInfo(BindingcarCallBack callBack);
|
||||
|
||||
/**
|
||||
* 获取车辆的信息
|
||||
* @param macAddress
|
||||
* @param widevineIDWithMd5
|
||||
*/
|
||||
void getBindingcarInfo(String macAddress, String widevineIDWithMd5);
|
||||
|
||||
/**
|
||||
@@ -27,4 +37,10 @@ public interface IMoGoBindingcarProvider extends IMoGoFunctionServerProvider {
|
||||
* @param dockerVersion 当前工控机版本
|
||||
*/
|
||||
void queryContainers(String padSn,String dockerVersion);
|
||||
|
||||
/**
|
||||
* 查询app是否有更新
|
||||
*/
|
||||
void queryAppUpgrade();
|
||||
|
||||
}
|
||||
|
||||
@@ -205,6 +205,11 @@ interface IMoGoWaringProvider : IMoGoHmiViewProxy {
|
||||
*/
|
||||
fun showModifyBindingcarDialog()
|
||||
|
||||
/**
|
||||
* 展示升级app弹框
|
||||
*/
|
||||
fun showUpgradeDialog(name: String, url: String)
|
||||
|
||||
/**
|
||||
* 呈现工控机升级确认框
|
||||
*/
|
||||
|
||||
@@ -68,6 +68,15 @@ object CallerAutopilotIdentifyListenerManager : CallerBase() {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun invokeAutopilotIdentifyPlanningObj(planningObjects: List<MessagePad.PlanningObject>?){
|
||||
M_AUTOPILOT_IDENTIFY_LISTENERS.forEach {
|
||||
val tag = it.key
|
||||
val listener = it.value
|
||||
listener.onAutopilotIdentifyPlanningObj(planningObjects)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报警信息 回调
|
||||
*/
|
||||
|
||||
@@ -269,6 +269,10 @@ object CallerHmiManager : CallerBase() {
|
||||
waringProviderApi?.showModifyBindingcarDialog()
|
||||
}
|
||||
|
||||
fun showUpgradeDialog(name: String, url: String) {
|
||||
waringProviderApi?.showUpgradeDialog(name, url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 呈现工控机升级确认框
|
||||
*/
|
||||
|
||||
BIN
core/mogo-core-res/src/main/res/raw/traffic_xiankuang.nt3d
Normal file
BIN
core/mogo-core-res/src/main/res/raw/traffic_xiankuang.nt3d
Normal file
Binary file not shown.
@@ -57,6 +57,7 @@ dependencies {
|
||||
implementation rootProject.ext.dependencies.kotlinstdlibjdk7
|
||||
implementation rootProject.ext.dependencies.androidxannotation
|
||||
implementation rootProject.ext.dependencies.material
|
||||
implementation rootProject.ext.dependencies.guava
|
||||
|
||||
implementation rootProject.ext.dependencies.gson
|
||||
implementation rootProject.ext.dependencies.glideanno
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2018 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.base.Objects;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** A point consisting of BigDecimal coordinates. */
|
||||
@GwtCompatible
|
||||
final strictfp class BigPoint implements Comparable<BigPoint> {
|
||||
final BigDecimal x;
|
||||
final BigDecimal y;
|
||||
final BigDecimal z;
|
||||
|
||||
/** Creates a point of BigDecimal coordinates from a point of double coordinates. */
|
||||
BigPoint(S2Point p) {
|
||||
this(Platform.newBigDecimal(p.x), Platform.newBigDecimal(p.y), Platform.newBigDecimal(p.z));
|
||||
}
|
||||
|
||||
/** Creates a point from the given BigDecimal coordinates. */
|
||||
BigPoint(BigDecimal x, BigDecimal y, BigDecimal z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/** Returns an S2Point by rounding 'this' to double precision. */
|
||||
S2Point toS2Point() {
|
||||
return new S2Point(x.doubleValue(), y.doubleValue(), z.doubleValue());
|
||||
}
|
||||
|
||||
/** Returns the vector cross product of 'this' with 'that'. */
|
||||
BigPoint crossProd(BigPoint that) {
|
||||
return new BigPoint(
|
||||
y.multiply(that.z).subtract(z.multiply(that.y)),
|
||||
z.multiply(that.x).subtract(x.multiply(that.z)),
|
||||
x.multiply(that.y).subtract(y.multiply(that.x)));
|
||||
}
|
||||
|
||||
/** Returns the vector dot product of 'this' with 'that'. */
|
||||
BigDecimal dotProd(BigPoint that) {
|
||||
return x.multiply(that.x).add(y.multiply(that.y)).add(z.multiply(that.z));
|
||||
}
|
||||
|
||||
/** Returns the vector dot product of 'this' with 'that'. */
|
||||
BigDecimal dotProd(S2Point that) {
|
||||
return dotProd(new BigPoint(that));
|
||||
}
|
||||
|
||||
/** Returns true iff this and 'p' are exactly parallel or anti-parallel. */
|
||||
boolean isLinearlyDependent(BigPoint p) {
|
||||
BigPoint n = crossProd(p);
|
||||
return n.x.signum() == 0 && n.y.signum() == 0 && n.z.signum() == 0;
|
||||
}
|
||||
|
||||
/** Returns true iff this and 'p' are exactly anti-parallel, antipodal points. */
|
||||
boolean isAntipodal(BigPoint p) {
|
||||
return isLinearlyDependent(p) && dotProd(p).signum() < 0;
|
||||
}
|
||||
|
||||
/** Returns the square of the magnitude of this vector. */
|
||||
BigDecimal norm2() {
|
||||
return this.dotProd(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BigPoint p) {
|
||||
int result = x.compareTo(p.x);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = y.compareTo(p.y);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return z.compareTo(p.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (!(that instanceof BigPoint)) {
|
||||
return false;
|
||||
}
|
||||
BigPoint thatPoint = (BigPoint) that;
|
||||
return x.equals(thatPoint.x) && y.equals(thatPoint.y) && z.equals(thatPoint.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(x, y, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright 2018 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.primitives.UnsignedInts;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/** Utilities for encoding and decoding integers. */
|
||||
@GwtCompatible
|
||||
public class EncodedInts {
|
||||
|
||||
/**
|
||||
* Reads a variable-encoded signed long.
|
||||
*
|
||||
* <p>Note that if you frequently read/write negative numbers, you should consider zigzag-encoding
|
||||
* your values before storing them as varints. See {@link EncodedInts#encodeZigZag32} and {@link
|
||||
* #decodeZigZag32(int)}.
|
||||
*
|
||||
* @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF),
|
||||
* or if the variable-encoded signed long is malformed.
|
||||
*/
|
||||
public static long readVarint64(InputStream input) throws IOException {
|
||||
long result = 0;
|
||||
for (int shift = 0; shift < 64; shift += 7) {
|
||||
final byte b = InputStreams.readByte(input);
|
||||
result |= (long) (b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
throw new IOException("Malformed varint.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a signed long using variable encoding.
|
||||
*
|
||||
* <p>Note that if you frequently read/write negative numbers, you should consider zigzag-encoding
|
||||
* your values before storing them as varints. See {@link EncodedInts#encodeZigZag32} and {@link
|
||||
* #decodeZigZag32(int)}.
|
||||
*
|
||||
* @throws IOException if {@code output.write(int)} throws an {@link IOException}.
|
||||
*/
|
||||
public static void writeVarint64(OutputStream output, long value) throws IOException {
|
||||
while (true) {
|
||||
if ((value & ~0x7FL) == 0) {
|
||||
output.write((byte) value);
|
||||
return;
|
||||
} else {
|
||||
output.write((byte) (((int) value & 0x7F) | 0x80));
|
||||
value >>>= 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a unsigned integer consisting of {@code bytesPerWord} bytes from {@code supplier} in
|
||||
* little-endian format as an unsigned 64-bit integer.
|
||||
*
|
||||
* <p>This method is not compatible with {@link #readVarint64(InputStream)} or {@link
|
||||
* #writeVarint64(OutputStream, long)}.
|
||||
*
|
||||
* @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF).
|
||||
*/
|
||||
public static long decodeUintWithLength(InputStream input, int bytesPerWord) throws IOException {
|
||||
long x = 0;
|
||||
for (int i = 0; i < bytesPerWord; i++) {
|
||||
x += (InputStreams.readByte(input) & 0xffL) << (8 * i);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes an unsigned integer to {@code consumer} in little-endian format using {@code
|
||||
* bytesPerWord} bytes. (The client must ensure that the encoder's buffer is large enough).
|
||||
*
|
||||
* <p>This method is not compatible with {@link #readVarint64(InputStream)} or {@link
|
||||
* #writeVarint64(OutputStream, long)}.
|
||||
*
|
||||
* @throws IOException if {@code output.write(int)} throws an {@link IOException}.
|
||||
*/
|
||||
public static void encodeUintWithLength(OutputStream output, long value, int bytesPerWord)
|
||||
throws IOException {
|
||||
while (--bytesPerWord >= 0) {
|
||||
output.write((byte) value);
|
||||
value >>>= 8;
|
||||
}
|
||||
assert value == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a ZigZag-encoded 32-bit value. ZigZag encodes signed integers into values that can be
|
||||
* efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits
|
||||
* to be varint encoded, thus always taking 10 bytes on the wire.)
|
||||
*
|
||||
* @param n A signed 32-bit integer.
|
||||
* @return An unsigned 32-bit integer, stored in a signed int because Java has no explicit
|
||||
* unsigned support.
|
||||
*/
|
||||
public static int encodeZigZag32(final int n) {
|
||||
// Note: the right-shift must be arithmetic
|
||||
return (n << 1) ^ (n >> 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a ZigZag-encoded 64-bit value. ZigZag encodes signed integers into values that can be
|
||||
* efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits
|
||||
* to be varint encoded, thus always taking 10 bytes on the wire.)
|
||||
*
|
||||
* @param n A signed 64-bit integer.
|
||||
* @return An unsigned 64-bit integer, stored in a signed int because Java has no explicit
|
||||
* unsigned support.
|
||||
*/
|
||||
public static long encodeZigZag64(final long n) {
|
||||
// Note: the right-shift must be arithmetic
|
||||
return (n << 1) ^ (n >> 63);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a ZigZag-encoded 32-bit signed value. ZigZag encodes signed integers into values that
|
||||
* can be efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64
|
||||
* bits to be varint encoded, thus always taking 10 bytes on the wire.)
|
||||
*
|
||||
* @param n A 32-bit integer, stored in a signed int because Java has no explicit unsigned
|
||||
* support.
|
||||
* @return A signed 32-bit integer.
|
||||
*/
|
||||
public static int decodeZigZag32(final int n) {
|
||||
return (n >>> 1) ^ -(n & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a ZigZag-encoded 64-bit signed value. ZigZag encodes signed integers into values that
|
||||
* can be efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64
|
||||
* bits to be varint encoded, thus always taking 10 bytes on the wire.)
|
||||
*
|
||||
* @param n A 64-bit integer, stored in a signed long because Java has no explicit unsigned
|
||||
* support.
|
||||
* @return A signed 64-bit integer.
|
||||
*/
|
||||
public static long decodeZigZag64(final long n) {
|
||||
return (n >>> 1) ^ -(n & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interleaving of bits of val1 and val2, where the LSB of val1 is the LSB of the
|
||||
* result, and the MSB of val2 is the MSB of the result.
|
||||
*/
|
||||
public static long interleaveBits(int val1, int val2) {
|
||||
return insertBlankBits(val1) | (insertBlankBits(val2) << 1);
|
||||
}
|
||||
|
||||
/** Returns the first int de-interleaved from the result of {@link #interleaveBits}. */
|
||||
public static int deinterleaveBits1(long bits) {
|
||||
return removeBlankBits(bits);
|
||||
}
|
||||
|
||||
/** Returns the second int de-interleaved from the result of {@link #interleaveBits}. */
|
||||
public static int deinterleaveBits2(long bits) {
|
||||
return removeBlankBits(bits >>> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts blank bits between the bits of 'value' such that the MSB is blank and the LSB is
|
||||
* unchanged.
|
||||
*/
|
||||
private static final long insertBlankBits(int value) {
|
||||
long bits = UnsignedInts.toLong(value);
|
||||
bits = (bits | (bits << 16)) & 0x0000ffff0000ffffL;
|
||||
bits = (bits | (bits << 8)) & 0x00ff00ff00ff00ffL;
|
||||
bits = (bits | (bits << 4)) & 0x0f0f0f0f0f0f0f0fL;
|
||||
bits = (bits | (bits << 2)) & 0x3333333333333333L;
|
||||
bits = (bits | (bits << 1)) & 0x5555555555555555L;
|
||||
return bits;
|
||||
}
|
||||
|
||||
/** Reverses {@link #insertBlankBits} by extracting the even bits (bit 0, 2, ...). */
|
||||
private static int removeBlankBits(long bits) {
|
||||
bits &= 0x5555555555555555L;
|
||||
bits |= bits >>> 1;
|
||||
bits &= 0x3333333333333333L;
|
||||
bits |= bits >>> 2;
|
||||
bits &= 0x0f0f0f0f0f0f0f0fL;
|
||||
bits |= bits >>> 4;
|
||||
bits &= 0x00ff00ff00ff00ffL;
|
||||
bits |= bits >>> 8;
|
||||
bits &= 0x0000ffff0000ffffL;
|
||||
bits |= bits >>> 16;
|
||||
return (int) bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #interleaveBits} but interleaves bit pairs rather than individual bits. This format
|
||||
* is faster to decode than the fully interleaved format, and produces the same results for our
|
||||
* use case.
|
||||
*
|
||||
* <p>This code is about 10% faster than {@link #interleaveBits}.
|
||||
*/
|
||||
public static long interleaveBitPairs(int val1, int val2) {
|
||||
return insertBlankPairs(val1) | (insertBlankPairs(val2) << 2);
|
||||
}
|
||||
|
||||
/** Returns the first int de-interleaved from the result of {@link #interleaveBitPairs}. */
|
||||
public static int deinterleaveBitPairs1(long pairs) {
|
||||
return removeBlankPairs(pairs);
|
||||
}
|
||||
|
||||
/** Returns the second int de-interleaved from the result of {@link #interleaveBitPairs}. */
|
||||
public static int deinterleaveBitPairs2(long pairs) {
|
||||
return removeBlankPairs(pairs >>> 2);
|
||||
}
|
||||
|
||||
/** Inserts 00 pairs in between the pairs from 'value'. */
|
||||
private static final long insertBlankPairs(int value) {
|
||||
long bits = UnsignedInts.toLong(value);
|
||||
bits = (bits | (bits << 16)) & 0x0000ffff0000ffffL;
|
||||
bits = (bits | (bits << 8)) & 0x00ff00ff00ff00ffL;
|
||||
bits = (bits | (bits << 4)) & 0x0f0f0f0f0f0f0f0fL;
|
||||
bits = (bits | (bits << 2)) & 0x3333333333333333L;
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses {#link #insertBitPairs} by selecting the two LSB bits, dropping the next two,
|
||||
* selecting the next two, etc.
|
||||
*/
|
||||
private static int removeBlankPairs(long pairs) {
|
||||
pairs &= 0x3333333333333333L;
|
||||
pairs |= pairs >>> 2;
|
||||
pairs &= 0x0f0f0f0f0f0f0f0fL;
|
||||
pairs |= pairs >>> 4;
|
||||
pairs &= 0x00ff00ff00ff00ffL;
|
||||
pairs |= pairs >>> 8;
|
||||
pairs &= 0x0000ffff0000ffffL;
|
||||
pairs |= pairs >>> 16;
|
||||
return (int) pairs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2018 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/** Utilities for handling {@link InputStream}s. */
|
||||
@GwtCompatible
|
||||
final class InputStreams {
|
||||
|
||||
/**
|
||||
* Reads a byte from {@code input}.
|
||||
*
|
||||
* @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF).
|
||||
*/
|
||||
static byte readByte(InputStream input) throws IOException {
|
||||
int result = input.read();
|
||||
if (result < 0) {
|
||||
throw new IOException("EOF");
|
||||
}
|
||||
return (byte) (result & 0xFF);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/** Simple utility for reading little endian primitives from a stream. */
|
||||
@GwtCompatible
|
||||
public final class LittleEndianInput {
|
||||
private final InputStream input;
|
||||
|
||||
/** Constructs a little-endian input that reads from the given stream. */
|
||||
public LittleEndianInput(InputStream input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a byte.
|
||||
*
|
||||
* @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF).
|
||||
*/
|
||||
public byte readByte() throws IOException {
|
||||
return InputStreams.readByte(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a fixed size of bytes from the input.
|
||||
*
|
||||
* @param size the number of bytes to read.
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public byte[] readBytes(final int size) throws IOException {
|
||||
byte[] result = new byte[size];
|
||||
int numRead = input.read(result);
|
||||
if (numRead < size) {
|
||||
throw new IOException("EOF");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed integer.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public int readInt() throws IOException {
|
||||
return (readByte() & 0xFF)
|
||||
| ((readByte() & 0xFF) << 8)
|
||||
| ((readByte() & 0xFF) << 16)
|
||||
| ((readByte() & 0xFF) << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a little-endian signed long.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public long readLong() throws IOException {
|
||||
return (readByte() & 0xFFL)
|
||||
| ((readByte() & 0xFFL) << 8)
|
||||
| ((readByte() & 0xFFL) << 16)
|
||||
| ((readByte() & 0xFFL) << 24)
|
||||
| ((readByte() & 0xFFL) << 32)
|
||||
| ((readByte() & 0xFFL) << 40)
|
||||
| ((readByte() & 0xFFL) << 48)
|
||||
| ((readByte() & 0xFFL) << 56);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a little-endian IEEE754 32-bit float.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public float readFloat() throws IOException {
|
||||
return Float.intBitsToFloat(readInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a little-endian IEEE754 64-bit double.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public double readDouble() throws IOException {
|
||||
return Double.longBitsToDouble(readLong());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a variable-encoded signed integer with {@link #readVarint64()}.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public int readVarint32() throws IOException {
|
||||
return (int) readVarint64();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a variable-encoded signed long with {@link EncodedInts#readVarint64(InputStream)}
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public long readVarint64() throws IOException {
|
||||
return EncodedInts.readVarint64(input);
|
||||
}
|
||||
|
||||
/** Closes the underlying stream. */
|
||||
public void close() throws IOException {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2016 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/** Simple utility for writing little endian primitives to a stream. */
|
||||
@GwtCompatible
|
||||
public final class LittleEndianOutput {
|
||||
private final OutputStream output;
|
||||
|
||||
/** Constructs a little-endian output that writes to the given stream. */
|
||||
public LittleEndianOutput(OutputStream output) {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
/** Writes a byte. */
|
||||
public void writeByte(byte value) throws IOException {
|
||||
output.write((int) value);
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] bytes) throws IOException {
|
||||
output.write(bytes);
|
||||
}
|
||||
|
||||
/** Writes a little-endian signed integer. */
|
||||
public void writeInt(int value) throws IOException {
|
||||
output.write(value & 0xFF);
|
||||
output.write((value >> 8) & 0xFF);
|
||||
output.write((value >> 16) & 0xFF);
|
||||
output.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
/** Writes a little-endian signed long. */
|
||||
public void writeLong(long value) throws IOException {
|
||||
output.write((int) (value & 0xFF));
|
||||
output.write((int) (value >> 8) & 0xFF);
|
||||
output.write((int) (value >> 16) & 0xFF);
|
||||
output.write((int) (value >> 24) & 0xFF);
|
||||
output.write((int) (value >> 32) & 0xFF);
|
||||
output.write((int) (value >> 40) & 0xFF);
|
||||
output.write((int) (value >> 48) & 0xFF);
|
||||
output.write((int) (value >> 56) & 0xFF);
|
||||
}
|
||||
|
||||
/** Writes a little-endian IEEE754 32-bit float. */
|
||||
public void writeFloat(float value) throws IOException {
|
||||
writeInt(Float.floatToIntBits(value));
|
||||
}
|
||||
|
||||
/** Writes a little-endian IEEE754 64-bit double. */
|
||||
public void writeDouble(double value) throws IOException {
|
||||
writeLong(Double.doubleToLongBits(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a signed integer using variable encoding with {@link #writeVarint64(long)}.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public void writeVarint32(int value) throws IOException {
|
||||
writeVarint64(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a signed long using variable encoding with {@link
|
||||
* EncodedInts#writeVarint64(OutputStream, long)}.
|
||||
*
|
||||
* @throws IOException if past end of input or error in underlying stream
|
||||
*/
|
||||
public void writeVarint64(long value) throws IOException {
|
||||
EncodedInts.writeVarint64(output, value);
|
||||
}
|
||||
|
||||
/** Closes the underlying output stream. */
|
||||
public void close() throws IOException {
|
||||
output.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.util.List;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/** A simple 3x3 matrix. */
|
||||
// TODO(eengle): Rename this to Matrix as it is not necessarily 3x3, and make Matrix3x3 a subclass.
|
||||
@GwtCompatible
|
||||
final class Matrix3x3 {
|
||||
private final double[] values;
|
||||
private final int rows;
|
||||
private final int cols;
|
||||
|
||||
/** Constructs a matrix from a series of column vectors. */
|
||||
public static Matrix3x3 fromCols(S2Point... columns) {
|
||||
Matrix3x3 result = new Matrix3x3(3, columns.length);
|
||||
for (int row = 0; row < result.rows; row++) {
|
||||
for (int col = 0; col < result.cols; col++) {
|
||||
result.set(row, col, columns[col].get(row));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Constructs a matrix from a series of column vectors. */
|
||||
public static Matrix3x3 fromCols(List<S2Point> frame) {
|
||||
return fromCols(frame.toArray(new S2Point[frame.size()]));
|
||||
}
|
||||
|
||||
/** Constructs a 2D matrix of the given width and values. */
|
||||
public Matrix3x3(int cols, double... values) {
|
||||
Preconditions.checkArgument(cols >= 0, "Negative rows not allowed.");
|
||||
rows = values.length / cols;
|
||||
this.cols = cols;
|
||||
Preconditions.checkArgument(
|
||||
rows * cols == values.length, "Values not an even multiple of 'cols'");
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
/** Constructs a 2D matrix of a fixed size. */
|
||||
public Matrix3x3(int rows, int cols) {
|
||||
Preconditions.checkArgument(rows >= 0, "Negative rows not allowed.");
|
||||
Preconditions.checkArgument(cols >= 0, "Negative cols not allowed.");
|
||||
this.rows = rows;
|
||||
this.cols = cols;
|
||||
this.values = new double[rows * cols];
|
||||
}
|
||||
|
||||
/** Returns the number of rows in this matrix. */
|
||||
public int rows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Returns the number of columns in this matrix. */
|
||||
public int cols() {
|
||||
return cols;
|
||||
}
|
||||
|
||||
/** Sets a value. */
|
||||
public void set(int row, int col, double value) {
|
||||
values[row * cols + col] = value;
|
||||
}
|
||||
|
||||
/** Gets a value. */
|
||||
public double get(int row, int col) {
|
||||
return values[row * cols + col];
|
||||
}
|
||||
|
||||
/** Returns the transpose of this. */
|
||||
@CheckReturnValue
|
||||
public Matrix3x3 transpose() {
|
||||
Matrix3x3 result = new Matrix3x3(cols, rows);
|
||||
for (int row = 0; row < result.rows; row++) {
|
||||
for (int col = 0; col < result.cols; col++) {
|
||||
result.set(row, col, get(col, row));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns the result of multiplying this x m. */
|
||||
@CheckReturnValue
|
||||
public Matrix3x3 mult(Matrix3x3 m) {
|
||||
Preconditions.checkArgument(cols == m.rows);
|
||||
Matrix3x3 result = new Matrix3x3(rows, m.cols);
|
||||
for (int row = 0; row < result.rows; row++) {
|
||||
for (int col = 0; col < result.cols; col++) {
|
||||
double sum = 0;
|
||||
for (int i = 0; i < cols; i++) {
|
||||
sum += get(row, i) * m.get(i, col);
|
||||
}
|
||||
result.set(row, col, sum);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return the vector of the given column. */
|
||||
public S2Point getCol(int col) {
|
||||
Preconditions.checkState(rows == 3);
|
||||
Preconditions.checkArgument(0 <= col && col < cols);
|
||||
return new S2Point(values[col], values[cols + col], values[2 * cols + col]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Matrix3x3)) {
|
||||
return false;
|
||||
}
|
||||
Matrix3x3 m = (Matrix3x3) o;
|
||||
if (rows != m.rows || cols != m.cols) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i] != m.values[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long hash = 37L * cols;
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
hash = 37L * hash + Platform.doubleHash(values[i]);
|
||||
}
|
||||
return (int) hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2006 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* An S2Point that also has a parameter associated with it, which corresponds to a time-like order
|
||||
* on the points.
|
||||
*
|
||||
* @author kirilll@google.com (Kirill Levin)
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final class ParametrizedS2Point implements Comparable<ParametrizedS2Point>, Serializable {
|
||||
|
||||
private final double time;
|
||||
private final S2Point point;
|
||||
|
||||
@SuppressWarnings("GoodTime") // should accept a java.time.Duration (?)
|
||||
public ParametrizedS2Point(double time, S2Point point) {
|
||||
this.time = time;
|
||||
this.point = point;
|
||||
}
|
||||
|
||||
@SuppressWarnings("GoodTime") // should return a java.time.Duration (?)
|
||||
public double getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public S2Point getPoint() {
|
||||
return point;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ParametrizedS2Point o) {
|
||||
int compareTime = Double.compare(time, o.time);
|
||||
if (compareTime != 0) {
|
||||
return compareTime;
|
||||
}
|
||||
return point.compareTo(o.point);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other instanceof ParametrizedS2Point) {
|
||||
ParametrizedS2Point x = (ParametrizedS2Point) other;
|
||||
return time == x.time && point.equalsPoint(x.point);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// TODO(jrosenstock): Use Objects.hash when API 19 (2014-06) is allowed. Current min is 14
|
||||
// (2011-10). Double.hashCode requires an even higher API level, so just hash the point.
|
||||
return point.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.PrintStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Contains utility methods which require different GWT client and server implementations. This
|
||||
* contains the server side implementations.
|
||||
*/
|
||||
@GwtCompatible(emulated = true)
|
||||
final class Platform {
|
||||
|
||||
private Platform() {}
|
||||
|
||||
/** @see Math#IEEEremainder(double, double) */
|
||||
static double IEEEremainder(double f1, double f2) {
|
||||
return Math.IEEEremainder(f1, f2);
|
||||
}
|
||||
|
||||
/** @see Math#getExponent(double) */
|
||||
static int getExponent(double d) {
|
||||
return Math.getExponent(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Logger} for the class.
|
||||
*
|
||||
* @see Logger#getLogger(String)
|
||||
*/
|
||||
static Logger getLoggerForClass(Class<?> clazz) {
|
||||
return Logger.getLogger(clazz.getCanonicalName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@code stream.printf} with the arguments. The GWT client just prints the format string
|
||||
* and the arguments separately. Using this method is not recommended; you should instead
|
||||
* construct strings with normal string concatenation whenever possible, so it will work the same
|
||||
* way in normal Java and GWT client versions.
|
||||
*/
|
||||
static void printf(PrintStream stream, String format, Object... params) {
|
||||
stream.printf(format, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code String.format} with the arguments. The GWT client just returns a string
|
||||
* consisting of the format string with the parameters concatenated to the end of it. Using this
|
||||
* method is not recommended; you should instead construct strings with normal string
|
||||
* concatenation whenever possible, so it will work the same way in normal Java and GWT client
|
||||
* versions.
|
||||
*/
|
||||
static String formatString(String format, Object... params) {
|
||||
return String.format(format, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the double as a string and removes unneeded trailing zeros, to behave the same as
|
||||
* printf("%.15g",d) in C++. The Javascript implementation does NOT have identical behavior.
|
||||
*/
|
||||
static String formatDouble(double d) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
if (d == 0d) {
|
||||
return "0";
|
||||
}
|
||||
// Style 'g' uses either 'e' or 'f', depending on the magnitude of the number.
|
||||
out.append(String.format(Locale.US, "%.15g", d));
|
||||
|
||||
// If formatted with style 'e', the 'e' is always in the same place relative to the length,
|
||||
// and the string will be at least five chars long, like "1e-20".
|
||||
if ((out.length() >= 5) && (out.charAt(out.length() - 4) == 'e')) {
|
||||
// Remove trailing zeros before the 'e'.
|
||||
while ((out.length() >= 5) && out.charAt(out.length() - 5) == '0') {
|
||||
out.deleteCharAt(out.length() - 5);
|
||||
}
|
||||
// Remove trailing decimal point.
|
||||
if (out.charAt(out.length() - 5) == '.') {
|
||||
out.deleteCharAt(out.length() - 5);
|
||||
}
|
||||
} else {
|
||||
// Otherwise, it was formatted with style 'f'. Remove trailing zeros.
|
||||
while (out.length() > 0 && out.charAt(out.length() - 1) == '0') {
|
||||
out.setLength(out.length() - 1);
|
||||
}
|
||||
// Remove trailing decimal point.
|
||||
if (out.length() > 0 && out.charAt(out.length() - 1) == '.') {
|
||||
out.setLength(out.length() - 1);
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/** A portable way to hash a double value. */
|
||||
public static long doubleHash(double value) {
|
||||
return Double.doubleToLongBits(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sign of the determinant of the matrix constructed from the three column vectors
|
||||
* {@code a}, {@code b}, and {@code c}. This operation is very robust for small determinants, but
|
||||
* is extremely slow and should only be used if performance is not a concern or all faster
|
||||
* techniques have been exhausted.
|
||||
*/
|
||||
public static int sign(S2Point a, S2Point b, S2Point c) {
|
||||
Real bycz = Real.mul(b.y, c.z);
|
||||
Real bzcy = Real.mul(b.z, c.y);
|
||||
Real bzcx = Real.mul(b.z, c.x);
|
||||
Real bxcz = Real.mul(b.x, c.z);
|
||||
Real bxcy = Real.mul(b.x, c.y);
|
||||
Real bycx = Real.mul(b.y, c.x);
|
||||
Real bcx = bycz.sub(bzcy);
|
||||
Real bcy = bzcx.sub(bxcz);
|
||||
Real bcz = bxcy.sub(bycx);
|
||||
Real x = bcx.mul(a.x);
|
||||
Real y = bcy.mul(a.y);
|
||||
Real z = bcz.mul(a.z);
|
||||
return x.add(y).add(z).signum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of an ulp of the argument. An ulp of a double value is the positive distance
|
||||
* between this floating-point value and the double next larger in magnitude.
|
||||
*/
|
||||
public static double ulp(double x) {
|
||||
return Math.ulp(x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next representable value in the direction of 'dir' starting from 'x', emulating the
|
||||
* behavior of {@link Math#nextAfter}.
|
||||
*/
|
||||
public static double nextAfter(double x, double dir) {
|
||||
return Math.nextAfter(x, dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@code BigDecimal} instance whose value is the exact decimal representation of
|
||||
* {@code x}, emulating the behavior of {@link BigDecimal#BigDecimal(double)}.
|
||||
*/
|
||||
static BigDecimal newBigDecimal(double x) {
|
||||
return new BigDecimal(x);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Copyright 2019 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.annotations.GwtIncompatible;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Doubles;
|
||||
import com.google.common.primitives.ImmutableLongArray;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** A set of interfaces for describing primitive arrays. */
|
||||
@GwtCompatible
|
||||
public final class PrimitiveArrays {
|
||||
|
||||
/**
|
||||
* An array of {@code byte}s.
|
||||
*
|
||||
* <p>Implementations will be thread-safe if the underlying data is not mutated. Users should
|
||||
* ensure the underlying data is not mutated in order to get predictable behaviour. Any buffering
|
||||
* should be done internally.
|
||||
*
|
||||
* <p>Implementations may support arrays > 2GB in size like so:
|
||||
*
|
||||
* <pre>{@code
|
||||
* new Bytes() {
|
||||
* byte get(long position) {
|
||||
* if (position < b1.length) {
|
||||
* return b1[Ints.checkedCast(position)];
|
||||
* }
|
||||
* return b2[Ints.checkedCast(position - b2.length)];
|
||||
* }
|
||||
* long length() { return b1.length + b2.length; }
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public interface Bytes {
|
||||
/**
|
||||
* Returns the {@code byte} at position {@code position}.
|
||||
*
|
||||
* <p>Throws an {@link IndexOutOfBoundsException} if the absolute get on the underlying
|
||||
* implementation fails.
|
||||
*/
|
||||
byte get(long position);
|
||||
|
||||
/** Returns the length of this array. */
|
||||
long length();
|
||||
|
||||
/** Returns a {@link Cursor} with the given {@code position} and {@code limit}. */
|
||||
default Cursor cursor(long position, long limit) {
|
||||
Preconditions.checkArgument(position <= limit && position <= length());
|
||||
return new Cursor(position, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Cursor} with the given {@code position}.
|
||||
*
|
||||
* <p>The {@code limit} of the returned cursor is the {@link #length()} of this array.
|
||||
*/
|
||||
default Cursor cursor(long position) {
|
||||
return cursor(position, length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Cursor}.
|
||||
*
|
||||
* <p>The {@code position} of the returned cursor is 0, and the {@code limit} is the {@link
|
||||
* #length()} of this array.
|
||||
*/
|
||||
default Cursor cursor() {
|
||||
return cursor(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Bytes} wrapping {@code buffer}.
|
||||
*
|
||||
* <p>The returned array starts from index 0 of buffer, and its length is {@code
|
||||
* buffer.limit()}.
|
||||
*/
|
||||
@GwtIncompatible("ByteBuffer")
|
||||
static Bytes fromByteBuffer(ByteBuffer buffer) {
|
||||
// TODO(eengle): Buffer positions > 0 trip bugs in the various methods. Exclude this case.
|
||||
Preconditions.checkState(buffer.position() == 0);
|
||||
return new Bytes() {
|
||||
@Override
|
||||
public byte get(long position) {
|
||||
return buffer.get(Ints.checkedCast(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return (long) buffer.limit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns a {@link Bytes} wrapping {@code bytes}. */
|
||||
static Bytes fromByteArray(byte[] bytes) {
|
||||
return new Bytes() {
|
||||
@Override
|
||||
public byte get(long position) {
|
||||
return bytes[Ints.checkedCast(position)];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long length() {
|
||||
return bytes.length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns an {@link InputStream} wrapping this array starting at {@code offset}. */
|
||||
default InputStream toInputStream(long offset) {
|
||||
Preconditions.checkArgument(offset >= 0 && offset <= length());
|
||||
return new InputStream() {
|
||||
long position = offset;
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
if (position == length()) {
|
||||
return -1;
|
||||
}
|
||||
return get(position++) & 0xFF;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link InputStream} wrapping this array starting at {@code cursor.position}.
|
||||
*
|
||||
* <p>{@code cursor.position} is incremented for each byte read from the returned {@link
|
||||
* InputStream}.
|
||||
*/
|
||||
default InputStream toInputStream(Cursor cursor) {
|
||||
Preconditions.checkArgument(cursor.position >= 0 && cursor.position <= length());
|
||||
return new InputStream() {
|
||||
@Override
|
||||
public int read() {
|
||||
if (cursor.position == length()) {
|
||||
return -1;
|
||||
}
|
||||
return get(cursor.position++) & 0xFF;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns an {@link InputStream} wrapping this array starting at the 0th byte. */
|
||||
default InputStream toInputStream() {
|
||||
return toInputStream(0);
|
||||
}
|
||||
|
||||
/** Writes this array to {@code output}. */
|
||||
default void writeTo(OutputStream output) throws IOException {
|
||||
for (long i = 0; i < length(); i++) {
|
||||
output.write(get(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unsigned integer consisting of {@code numBytes} bytes read from this array at
|
||||
* {@code cursor.position} in little-endian format as an unsigned 64-bit integer.
|
||||
*
|
||||
* <p>{@code cursor.position} is updated to the index of the first byte following the varint64.
|
||||
*/
|
||||
default long readVarint64(Cursor cursor) {
|
||||
long result = 0;
|
||||
for (int shift = 0; shift < 64; shift += 7) {
|
||||
byte b = get(cursor.position++);
|
||||
result |= (long) (b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Malformed varint.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #readVarint64(Cursor)}, but throws an {@link IllegalArgumentException} if the
|
||||
* read varint64 is greater than {@link Integer#MAX_VALUE}.
|
||||
*/
|
||||
default int readVarint32(Cursor cursor) {
|
||||
return Ints.checkedCast(readVarint64(cursor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unsigned integer consisting of {@code numBytes} bytes read from this array at
|
||||
* {@code cursor.position} in little-endian format as an unsigned 64-bit integer.
|
||||
*
|
||||
* <p>{@code cursor.position} is updated to the index of the first byte following the uint.
|
||||
*
|
||||
* <p>This method is not compatible with {@link #readVarint64(Cursor)}.
|
||||
*/
|
||||
default long readUintWithLength(Cursor cursor, int numBytes) {
|
||||
long result = readUintWithLength(cursor.position, numBytes);
|
||||
cursor.position += numBytes;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Same as {@link #readUintWithLength(Cursor, int)}, but does not require a {@link Cursor}. */
|
||||
default long readUintWithLength(long position, int numBytes) {
|
||||
long x = 0;
|
||||
for (int i = 0; i < numBytes; i++) {
|
||||
x += (get(position++) & 0xffL) << (8 * i);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/** Returns a little-endian double read from this array at {@code position}. */
|
||||
default double readLittleEndianDouble(long position) {
|
||||
return Double.longBitsToDouble(readUintWithLength(position, Doubles.BYTES));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of {@code long}s.
|
||||
*
|
||||
* <p>Implementations will be thread-safe if the underlying data is not mutated. Users should
|
||||
* ensure the underlying data is not mutated in order to get predictable behaviour. Any buffering
|
||||
* should be done internally.
|
||||
*/
|
||||
interface Longs {
|
||||
/**
|
||||
* Returns the {@code long} at position {@code position}.
|
||||
*
|
||||
* <p>Throws an {@link IndexOutOfBoundsException} if the absolute get on the underlying
|
||||
* implementation fails.
|
||||
*/
|
||||
long get(int position);
|
||||
|
||||
/** Returns the length of this array. */
|
||||
int length();
|
||||
|
||||
/** Returns a {@link Longs} wrapping {@code immutableLongArray}. */
|
||||
static Longs fromImmutableLongArray(ImmutableLongArray immutableLongArray) {
|
||||
return new Longs() {
|
||||
@Override
|
||||
public long get(int position) {
|
||||
return immutableLongArray.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return immutableLongArray.length();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes and returns this array as an {@code int[]}.
|
||||
*
|
||||
* <p>Throws an {@link IllegalArgumentException} if any value in this array is < {@link
|
||||
* Integer#MIN_VALUE} or > {@link Integer#MAX_VALUE}.
|
||||
*/
|
||||
default int[] toIntArray() {
|
||||
int[] result = new int[length()];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = Ints.checkedCast(get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** A cursor storing a position and a limit. */
|
||||
public static class Cursor {
|
||||
public long position;
|
||||
public long limit;
|
||||
|
||||
Cursor(long position, long limit) {
|
||||
Preconditions.checkArgument(position >= 0);
|
||||
Preconditions.checkArgument(position <= limit);
|
||||
this.position = position;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
/** Returns the number of remaining elements ({@code limit - position}). */
|
||||
public long remaining() {
|
||||
return limit - position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
* An R1Interval represents a closed, bounded interval on the real line. It is capable of
|
||||
* representing the empty interval (containing no points) and zero-length intervals (containing a
|
||||
* single point).
|
||||
*
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class R1Interval implements Serializable {
|
||||
private double lo;
|
||||
private double hi;
|
||||
|
||||
/**
|
||||
* Default constructor, contains the empty interval.
|
||||
*
|
||||
* <p>Package private since only the S2 library needs to mutate R1Intervals. External code that
|
||||
* needs an empty interval should call {@link #empty()}.
|
||||
*/
|
||||
R1Interval() {
|
||||
lo = 1;
|
||||
hi = 0;
|
||||
}
|
||||
|
||||
/** Interval constructor. If lo > hi, the interval is empty. */
|
||||
public R1Interval(double lo, double hi) {
|
||||
this.lo = lo;
|
||||
this.hi = hi;
|
||||
}
|
||||
|
||||
/** Copy constructor. */
|
||||
public R1Interval(R1Interval interval) {
|
||||
this.lo = interval.lo;
|
||||
this.hi = interval.hi;
|
||||
}
|
||||
|
||||
/** Returns an empty interval. (Any interval where lo > hi is considered empty.) */
|
||||
public static R1Interval empty() {
|
||||
return new R1Interval(1, 0);
|
||||
}
|
||||
|
||||
/** Convenience method to construct an interval containing a single point. */
|
||||
public static R1Interval fromPoint(double p) {
|
||||
return new R1Interval(p, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to construct the minimal interval containing the two given points. This is
|
||||
* equivalent to starting with an empty interval and calling AddPoint() twice, but it is more
|
||||
* efficient.
|
||||
*/
|
||||
public static R1Interval fromPointPair(double p1, double p2) {
|
||||
R1Interval result = new R1Interval();
|
||||
result.initFromPointPair(p1, p2);
|
||||
return result;
|
||||
}
|
||||
|
||||
void initFromPointPair(double p1, double p2) {
|
||||
if (p1 <= p2) {
|
||||
lo = p1;
|
||||
hi = p2;
|
||||
} else {
|
||||
lo = p2;
|
||||
hi = p1;
|
||||
}
|
||||
}
|
||||
|
||||
public double lo() {
|
||||
return lo;
|
||||
}
|
||||
|
||||
public double hi() {
|
||||
return hi;
|
||||
}
|
||||
|
||||
/** Designates which end of the interval to work with. */
|
||||
enum Endpoint {
|
||||
/** The low end of the interval. */
|
||||
LO {
|
||||
@Override
|
||||
public double getValue(R1Interval interval) {
|
||||
return interval.lo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(R1Interval interval, double value) {
|
||||
interval.lo = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Endpoint opposite() {
|
||||
return HI;
|
||||
}
|
||||
},
|
||||
/** The high end of the interval. */
|
||||
HI {
|
||||
@Override
|
||||
public double getValue(R1Interval interval) {
|
||||
return interval.hi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(R1Interval interval, double value) {
|
||||
interval.hi = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Endpoint opposite() {
|
||||
return LO;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract double getValue(R1Interval interval);
|
||||
|
||||
public abstract void setValue(R1Interval interval, double value);
|
||||
|
||||
public abstract Endpoint opposite();
|
||||
}
|
||||
|
||||
/** Returns the value at the given Endpoint, which must not be null. */
|
||||
double getValue(Endpoint endpoint) {
|
||||
return endpoint.getValue(this);
|
||||
}
|
||||
|
||||
/** Sets the value of the given Endpoint, which must not be null. */
|
||||
void setValue(Endpoint endpoint, double value) {
|
||||
endpoint.setValue(this, value);
|
||||
}
|
||||
|
||||
/** Returns true if the interval is empty, i.e. it contains no points. */
|
||||
public boolean isEmpty() {
|
||||
return lo > hi;
|
||||
}
|
||||
|
||||
/** Returns the center of the interval. For empty intervals, the result is arbitrary. */
|
||||
public double getCenter() {
|
||||
return 0.5 * (lo + hi);
|
||||
}
|
||||
|
||||
/** Returns the length of the interval. The length of an empty interval is negative. */
|
||||
public double getLength() {
|
||||
return hi - lo;
|
||||
}
|
||||
|
||||
public boolean contains(double p) {
|
||||
return p >= lo && p <= hi;
|
||||
}
|
||||
|
||||
public boolean interiorContains(double p) {
|
||||
return p > lo && p < hi;
|
||||
}
|
||||
|
||||
/** Returns true if this interval contains the interval {@code y}. */
|
||||
public boolean contains(R1Interval y) {
|
||||
if (y.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return y.lo >= lo && y.hi <= hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the interior of this interval contains the entire interval {@code y} (including
|
||||
* its boundary).
|
||||
*/
|
||||
public boolean interiorContains(R1Interval y) {
|
||||
if (y.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return y.lo > lo && y.hi < hi;
|
||||
}
|
||||
|
||||
/** Returns true if this interval intersects {@code y}, i.e. if they have any points in common. */
|
||||
public boolean intersects(R1Interval y) {
|
||||
if (lo <= y.lo) {
|
||||
return y.lo <= hi && y.lo <= y.hi;
|
||||
} else {
|
||||
return lo <= y.hi && lo <= hi;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the interior of this interval intersects any point of {@code y} (including its
|
||||
* boundary).
|
||||
*/
|
||||
public boolean interiorIntersects(R1Interval y) {
|
||||
return y.lo < hi && lo < y.hi && lo < hi && y.lo <= y.hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Hausdorff distance to the given interval {@code y}. For two R1Intervals x and y,
|
||||
* this distance is defined as h(x, y) = max_{p in x} min_{q in y} d(p, q).
|
||||
*/
|
||||
public double getDirectedHausdorffDistance(R1Interval y) {
|
||||
if (isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
if (y.isEmpty()) {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
return Math.max(0.0, Math.max(hi() - y.hi(), y.lo() - lo()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum and maximum value of this interval. If {@code lo} is greater than {@code hi}
|
||||
* this interval will become empty.
|
||||
*
|
||||
* <p>Package private since only the S2 libraries have a current need to mutate R1Intervals.
|
||||
*/
|
||||
void set(double lo, double hi) {
|
||||
this.lo = lo;
|
||||
this.hi = hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum value of this interval. If {@code lo} is greater than {@code hi()} this
|
||||
* interval will become empty.
|
||||
*
|
||||
* <p>Package private since only the S2 libraries have a current need to mutate R1Intervals.
|
||||
*/
|
||||
void setLo(double lo) {
|
||||
this.lo = lo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum value of this interval. If {@code hi} is less than {@code lo()} this interval
|
||||
* will become empty.
|
||||
*
|
||||
* <p>Package private since only the S2 libraries have a current need to mutate R1Intervals.
|
||||
*/
|
||||
void setHi(double hi) {
|
||||
this.hi = hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current interval to the empty interval.
|
||||
*
|
||||
* <p>Package private since only the S2 libraries have a current need to mutate R1Intervals.
|
||||
*/
|
||||
void setEmpty() {
|
||||
this.lo = 1;
|
||||
this.hi = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands this interval so that it contains the point {@code p}.
|
||||
*
|
||||
* <p>Package private since only the S2 library needs to mutate R1Intervals.
|
||||
*/
|
||||
void unionInternal(double p) {
|
||||
if (isEmpty()) {
|
||||
lo = p;
|
||||
hi = p;
|
||||
} else if (p < lo) {
|
||||
lo = p;
|
||||
} else if (p > hi) {
|
||||
hi = p;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest point in the interval to the point {@code p}. The interval must be
|
||||
* non-empty.
|
||||
*/
|
||||
public double clampPoint(double p) {
|
||||
// assert (!isEmpty());
|
||||
return Math.max(lo, Math.min(hi, p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an interval that contains all points with a distance "radius" of a point in this
|
||||
* interval. Note that the expansion of an empty interval is always empty.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public R1Interval expanded(double radius) {
|
||||
// assert (radius >= 0);
|
||||
if (isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return new R1Interval(lo - radius, hi + radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands this interval to contain all points within a distance "radius" of a point in this
|
||||
* interval.
|
||||
*
|
||||
* <p>Package private since only S2 classes are intended to mutate R1Intervals for now.
|
||||
*/
|
||||
void expandedInternal(double radius) {
|
||||
lo -= radius;
|
||||
hi += radius;
|
||||
}
|
||||
|
||||
/** Returns the smallest interval that contains this interval and {@code y}. */
|
||||
@CheckReturnValue
|
||||
public R1Interval union(R1Interval y) {
|
||||
if (isEmpty()) {
|
||||
return y;
|
||||
}
|
||||
if (y.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return new R1Interval(Math.min(lo, y.lo), Math.max(hi, y.hi));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this interval to the union of this interval and {@code y}.
|
||||
*
|
||||
* <p>Package private since only S2 classes are intended to mutate R11Intervals for now.
|
||||
*/
|
||||
void unionInternal(R1Interval y) {
|
||||
if (isEmpty()) {
|
||||
lo = y.lo;
|
||||
hi = y.hi;
|
||||
} else if (!y.isEmpty()) {
|
||||
lo = Math.min(lo, y.lo);
|
||||
hi = Math.max(hi, y.hi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intersection of this interval with {@code y}. Empty intervals do not need to be
|
||||
* special-cased.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public R1Interval intersection(R1Interval y) {
|
||||
return new R1Interval(Math.max(lo, y.lo), Math.min(hi, y.hi));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this interval to the intersection of the current interval and {@code y}.
|
||||
*
|
||||
* <p>Package private since only S2 classes are intended to mutate R1 intervals for now.
|
||||
*/
|
||||
void intersectionInternal(R1Interval y) {
|
||||
lo = Math.max(lo, y.lo);
|
||||
hi = Math.min(hi, y.hi);
|
||||
}
|
||||
|
||||
/** Returns the smallest interval that contains this interval and the point {@code p}. */
|
||||
@CheckReturnValue
|
||||
public R1Interval addPoint(double p) {
|
||||
if (isEmpty()) {
|
||||
return R1Interval.fromPoint(p);
|
||||
} else if (p < lo) {
|
||||
return new R1Interval(p, hi);
|
||||
} else if (p > hi) {
|
||||
return new R1Interval(lo, p);
|
||||
} else {
|
||||
return new R1Interval(lo, hi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (that instanceof R1Interval) {
|
||||
R1Interval y = (R1Interval) that;
|
||||
// Return true if two intervals contain the same set of points.
|
||||
return (lo == y.lo && hi == y.hi) || (isEmpty() && y.isEmpty());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (isEmpty()) {
|
||||
return 17;
|
||||
}
|
||||
|
||||
long value = 17;
|
||||
value = 37 * value + Double.doubleToLongBits(lo);
|
||||
value = 37 * value + Double.doubleToLongBits(hi);
|
||||
return (int) (value ^ (value >>> 32));
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #approxEquals(R1Interval, double)}, with a default value for maxError just larger
|
||||
* than typical rounding errors in computing intervals.
|
||||
*/
|
||||
public boolean approxEquals(R1Interval y) {
|
||||
return approxEquals(y, 1e-15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this interval can be transformed into {@code y} by moving each endpoint by at
|
||||
* most {@code maxError}. The empty interval is considered to be positioned arbitrarily on the
|
||||
* real line, thus any interval for which {@code length <= 2*maxError} is true matches the empty
|
||||
* interval.
|
||||
*/
|
||||
public boolean approxEquals(R1Interval y, double maxError) {
|
||||
if (isEmpty()) {
|
||||
return y.getLength() <= maxError;
|
||||
}
|
||||
if (y.isEmpty()) {
|
||||
return getLength() <= maxError;
|
||||
}
|
||||
return Math.abs(y.lo - lo) <= maxError && Math.abs(y.hi - hi) <= maxError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + lo + ", " + hi + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
* An R2Rect represents a closed axis-aligned rectangle in the (x,y) plane. This class is mutable to
|
||||
* allow iteratively constructing bounds via e.g. {@link #addPoint(R2Vector)}.
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class R2Rect implements Serializable {
|
||||
private final R1Interval x;
|
||||
private final R1Interval y;
|
||||
|
||||
/** Creates an empty R2Rect. */
|
||||
public R2Rect() {
|
||||
// The default R1Interval constructor creates an empty interval.
|
||||
this.x = new R1Interval();
|
||||
this.y = new R1Interval();
|
||||
// assert (isValid());
|
||||
}
|
||||
|
||||
/** Constructs a rectangle from the given lower-left and upper-right points. */
|
||||
public R2Rect(R2Vector lo, R2Vector hi) {
|
||||
x = new R1Interval(lo.x(), hi.x());
|
||||
y = new R1Interval(lo.y(), hi.y());
|
||||
// assert (isValid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a rectangle from the given intervals in x and y. The two intervals must either be
|
||||
* both empty or both non-empty.
|
||||
*/
|
||||
public R2Rect(R1Interval x, R1Interval y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
// assert (isValid());
|
||||
}
|
||||
|
||||
/** Copy constructor. */
|
||||
public R2Rect(R2Rect rect) {
|
||||
this.x = new R1Interval(rect.x);
|
||||
this.y = new R1Interval(rect.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the canonical empty rectangle. Use isEmpty() to test for empty
|
||||
* rectangles, since they have more than one representation.
|
||||
*/
|
||||
public static R2Rect empty() {
|
||||
return new R2Rect(R1Interval.empty(), R1Interval.empty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new rectangle from a center point and size in each dimension. Both components of size
|
||||
* should be non-negative, i.e. this method cannot be used to create an empty rectangle.
|
||||
*/
|
||||
public static R2Rect fromCenterSize(R2Vector center, R2Vector size) {
|
||||
return new R2Rect(
|
||||
new R1Interval(center.x() - 0.5 * size.x(), center.x() + 0.5 * size.x()),
|
||||
new R1Interval(center.y() - 0.5 * size.y(), center.y() + 0.5 * size.y()));
|
||||
}
|
||||
|
||||
/** Returns a rectangle containing a single point. */
|
||||
public static R2Rect fromPoint(R2Vector p) {
|
||||
return new R2Rect(p, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimal bounding rectangle containing the two given points. This is equivalent to
|
||||
* starting with an empty rectangle and calling addPoint() twice. Note that it is different than
|
||||
* the R2Rect(lo, hi) constructor, where the first point is always used as the lower-left corner
|
||||
* of the resulting rectangle.
|
||||
*/
|
||||
public static R2Rect fromPointPair(R2Vector p1, R2Vector p2) {
|
||||
return new R2Rect(
|
||||
R1Interval.fromPointPair(p1.x(), p2.x()), R1Interval.fromPointPair(p1.y(), p2.y()));
|
||||
}
|
||||
|
||||
/** Returns the interval along the x-axis. */
|
||||
public R1Interval x() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/** Returns the interval along the y-axis. */
|
||||
public R1Interval y() {
|
||||
return y;
|
||||
}
|
||||
|
||||
/** Returns the point in this rectangle with the minimum x and y values. */
|
||||
public R2Vector lo() {
|
||||
return new R2Vector(x().lo(), y().lo());
|
||||
}
|
||||
|
||||
/** Returns the point in this rectangle with the maximum x and y values. */
|
||||
public R2Vector hi() {
|
||||
return new R2Vector(x().hi(), y().hi());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this rectangle is valid, which essentially just means that if the bound for
|
||||
* either axis is empty then both must be.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
// The x/y ranges must either be both empty or both non-empty.
|
||||
return x().isEmpty() == y().isEmpty();
|
||||
}
|
||||
|
||||
/** Return true if this rectangle is empty, i.e. it contains no points at all. */
|
||||
public boolean isEmpty() {
|
||||
return x().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the k<super>th</super> vertex of this rectangle (k = 0,1,2,3) in CCW order. Vertex 0 is
|
||||
* in the lower-left corner.
|
||||
*/
|
||||
public R2Vector getVertex(int k) {
|
||||
// Twiddle bits to return the points in CCW order (lower left, lower right,
|
||||
// upper right, upper left).
|
||||
// assert (k >= 0 && k <= 3);
|
||||
return getVertex((k >> 1) ^ (k & 1), k >> 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the vertex in direction "i" along the x-axis (0=left, 1=right) and direction "j" along
|
||||
* the y-axis (0=down, 1=up). Equivalently, returns the vertex constructed by selecting endpoint
|
||||
* "i" of the x-interval (0=lo, 1=hi) and vertex "j" of the y-interval.
|
||||
*/
|
||||
public R2Vector getVertex(int i, int j) {
|
||||
return new R2Vector(i == 0 ? x.lo() : x.hi(), j == 0 ? y.lo() : y.hi());
|
||||
}
|
||||
|
||||
/** Returns the center of this rectangle in (x,y)-space. */
|
||||
public R2Vector getCenter() {
|
||||
return new R2Vector(x().getCenter(), y().getCenter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width and height of this rectangle in (x,y)-space. Empty rectangles have a negative
|
||||
* width and height.
|
||||
*/
|
||||
public R2Vector getSize() {
|
||||
return new R2Vector(x().getLength(), y().getLength());
|
||||
}
|
||||
|
||||
/** Valid axes. */
|
||||
public enum Axis {
|
||||
X {
|
||||
@Override
|
||||
public R1Interval getInterval(R2Rect rect) {
|
||||
return rect.x;
|
||||
}
|
||||
},
|
||||
Y {
|
||||
@Override
|
||||
public R1Interval getInterval(R2Rect rect) {
|
||||
return rect.y;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract R1Interval getInterval(R2Rect rect);
|
||||
}
|
||||
|
||||
/** Returns the interval for the given axis, which must not be null. */
|
||||
public R1Interval getInterval(Axis axis) {
|
||||
return axis.getInterval(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this rectangle contains the given point. Note that rectangles are closed
|
||||
* regions, i.e. they contain their boundary.
|
||||
*/
|
||||
public boolean contains(R2Vector p) {
|
||||
return x().contains(p.x()) && y().contains(p.y());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the given point is contained in the interior of the region (i.e.
|
||||
* the region excluding its boundary).
|
||||
*/
|
||||
public boolean interiorContains(R2Vector p) {
|
||||
return x().interiorContains(p.x()) && y().interiorContains(p.y());
|
||||
}
|
||||
|
||||
/** Returns true if and only if this rectangle contains the given other rectangle. */
|
||||
public boolean contains(R2Rect other) {
|
||||
return x().contains(other.x()) && y().contains(other.y());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if and only if the interior of this rectangle contains all points of the given
|
||||
* other rectangle (including its boundary).
|
||||
*/
|
||||
public boolean interiorContains(R2Rect other) {
|
||||
return x().interiorContains(other.x()) && y().interiorContains(other.y());
|
||||
}
|
||||
|
||||
/** Returns true if this rectangle and the given other rectangle have any points in common. */
|
||||
public boolean intersects(R2Rect other) {
|
||||
return x().intersects(other.x()) && y().intersects(other.y());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if and only if the interior of this rectangle intersects any point (including the
|
||||
* boundary) of the given other rectangle.
|
||||
*/
|
||||
public boolean interiorIntersects(R2Rect other) {
|
||||
return x().interiorIntersects(other.x()) && y().interiorIntersects(other.y());
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the size of the bounding rectangle to include the given point. This rectangle is
|
||||
* expanded by the minimum amount possible.
|
||||
*/
|
||||
public void addPoint(R2Vector p) {
|
||||
x.unionInternal(p.x());
|
||||
y.unionInternal(p.y());
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the rectangle to include the given other rectangle. This is the same as replacing the
|
||||
* rectangle by the union of the two rectangles, but is somewhat more efficient.
|
||||
*/
|
||||
public void addRect(R2Rect other) {
|
||||
x.unionInternal(other.x);
|
||||
y.unionInternal(other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the closest point in this rectangle to the given point "p". This rectangle must be
|
||||
* non-empty.
|
||||
*/
|
||||
public R2Vector clampPoint(R2Vector p) {
|
||||
return new R2Vector(x().clampPoint(p.x()), y().clampPoint(p.y()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a rectangle that has been expanded on each side in the x-direction by margin.x(), and on
|
||||
* each side in the y-direction by margin.y(). If either margin is empty, then shrink the interval
|
||||
* on the corresponding sides instead. The resulting rectangle may be empty. Any expansion of an
|
||||
* empty rectangle remains empty.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public R2Rect expanded(R2Vector margin) {
|
||||
R1Interval xx = x().expanded(margin.x());
|
||||
R1Interval yy = y().expanded(margin.y());
|
||||
if (xx.isEmpty() || yy.isEmpty()) {
|
||||
return empty();
|
||||
} else {
|
||||
return new R2Rect(xx, yy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rectangle that has been expanded on both sides by the given margin. Any expansion of
|
||||
* an empty rectangle remains empty.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public R2Rect expanded(double margin) {
|
||||
return expanded(new R2Vector(margin, margin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands this rectangle on both axes by the given margin.
|
||||
*
|
||||
* <p>Package private since only S2 classes are intended to mutate R2Rects for now.
|
||||
*/
|
||||
void expand(double margin) {
|
||||
if (!isEmpty()) {
|
||||
x.expandedInternal(margin);
|
||||
y.expandedInternal(margin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest rectangle containing the union of this rectangle and the given rectangle.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public R2Rect union(R2Rect other) {
|
||||
return new R2Rect(x().union(other.x()), y().union(other.y()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest rectangle containing the intersection of this rectangle and the given
|
||||
* rectangle.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public R2Rect intersection(R2Rect other) {
|
||||
R1Interval xx = x().intersection(other.x());
|
||||
R1Interval yy = y().intersection(other.y());
|
||||
if (xx.isEmpty() || yy.isEmpty()) {
|
||||
return empty();
|
||||
}
|
||||
return new R2Rect(xx, yy);
|
||||
}
|
||||
|
||||
/** Returns a simple convolution hashcodes from the x and y internals. */
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return x.hashCode() * 701 + y.hashCode();
|
||||
}
|
||||
|
||||
/** Returns true if two rectangles contains the same set of points. */
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other instanceof R2Rect) {
|
||||
R2Rect r2 = (R2Rect) other;
|
||||
return x().equals(r2.x()) && y().equals(r2.y());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the x- and y-intervals of the two rectangles are the same up to the given
|
||||
* tolerance. See {@link R1Interval} for details on approximate interval equality.
|
||||
*/
|
||||
public boolean approxEquals(R2Rect other) {
|
||||
return approxEquals(other, 1e-15);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given rectangles are equal to within {@code maxError}. See {@link
|
||||
* R1Interval} for details on approximate interval equality.
|
||||
*/
|
||||
public boolean approxEquals(R2Rect other, double maxError) {
|
||||
return x().approxEquals(other.x(), maxError) && y().approxEquals(other.y(), maxError);
|
||||
}
|
||||
|
||||
/** Returns a simple string representation of this rectangle's lower and upper corners. */
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[Lo" + lo() + ", Hi" + hi() + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* R2Vector represents a vector in the two-dimensional space. It defines the basic geometrical
|
||||
* operations for 2D vectors, e.g. cross product, addition, norm, comparison, etc.
|
||||
*
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class R2Vector implements Serializable {
|
||||
double x;
|
||||
double y;
|
||||
|
||||
/** Constructs a new R2Vector at the origin [0,0] of the R2 coordinate system. */
|
||||
public R2Vector() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
/** Constructs a new R2 vector from the given x and y coordinates. */
|
||||
public R2Vector(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/** Constructs a new R2 vector from the given coordinates array, which must have length 2. */
|
||||
public R2Vector(double[] coord) {
|
||||
if (coord.length != 2) {
|
||||
throw new IllegalStateException("Points must have exactly 2 coordinates");
|
||||
}
|
||||
x = coord[0];
|
||||
y = coord[1];
|
||||
}
|
||||
|
||||
/** Returns the x coordinate of this R2 vector. */
|
||||
public double x() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/** Returns the y coordinate of this R2 vector. */
|
||||
public double y() {
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coordinate of the given axis, which will be the x axis if index is 0, and the y
|
||||
* axis if index is 1.
|
||||
*
|
||||
* @throws ArrayIndexOutOfBoundsException Thrown if the given index is not 0 or 1.
|
||||
*/
|
||||
public double get(int index) {
|
||||
if (index < 0 || index > 1) {
|
||||
throw new ArrayIndexOutOfBoundsException(index);
|
||||
}
|
||||
return index == 0 ? this.x : this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of this vector from the given other vector. Package private since this is
|
||||
* only mutable for S2.
|
||||
*/
|
||||
void set(R2Vector v) {
|
||||
this.x = v.x();
|
||||
this.y = v.y();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of this vector from the given values. Package private since this is only
|
||||
* mutable for S2.
|
||||
*/
|
||||
void set(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/** Returns the vector result of {@code p1 - p2}. */
|
||||
public static R2Vector add(final R2Vector p1, final R2Vector p2) {
|
||||
return new R2Vector(p1.x + p2.x, p1.y + p2.y);
|
||||
}
|
||||
|
||||
/** Returns the vector result of {@code p1 - p2}. */
|
||||
public static R2Vector sub(final R2Vector p1, final R2Vector p2) {
|
||||
return new R2Vector(p1.x - p2.x, p1.y - p2.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element-wise multiplication of p1 and p2, e.g. {@code vector [p1.x*p2.x,
|
||||
* p1.y*p2.y]}.
|
||||
*/
|
||||
public static R2Vector mul(final R2Vector p, double m) {
|
||||
return new R2Vector(m * p.x, m * p.y);
|
||||
}
|
||||
|
||||
/** Returns the vector magnitude. */
|
||||
public double norm() {
|
||||
return Math.sqrt(norm2());
|
||||
}
|
||||
|
||||
/** Returns the square of the vector magnitude. */
|
||||
public double norm2() {
|
||||
return (x * x) + (y * y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new vector scaled to magnitude 1, or a copy of the original vector if magnitude was
|
||||
* 0.
|
||||
*/
|
||||
public static R2Vector normalize(R2Vector vector) {
|
||||
double n = vector.norm();
|
||||
if (n != 0) {
|
||||
return mul(vector, 1.0 / n);
|
||||
} else {
|
||||
return new R2Vector(vector.x, vector.y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new R2 vector orthogonal to the current one with the same norm and counterclockwise
|
||||
* to it.
|
||||
*/
|
||||
public R2Vector ortho() {
|
||||
return new R2Vector(-y, x);
|
||||
}
|
||||
|
||||
/** Returns the dot product of the given vectors. */
|
||||
public static double dotProd(final R2Vector p1, final R2Vector p2) {
|
||||
return (p1.x * p2.x) + (p1.y * p2.y);
|
||||
}
|
||||
|
||||
/** Returns the dot product of this vector with that vector. */
|
||||
public double dotProd(R2Vector that) {
|
||||
return dotProd(this, that);
|
||||
}
|
||||
|
||||
/** Returns the cross product of this vector with that vector. */
|
||||
public double crossProd(final R2Vector that) {
|
||||
return this.x * that.y - this.y * that.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this vector is less than that vector, with the x-axis as the primary sort key
|
||||
* and the y-axis as the secondary sort key.
|
||||
*/
|
||||
public boolean lessThan(R2Vector that) {
|
||||
if (x < that.x) {
|
||||
return true;
|
||||
}
|
||||
if (that.x < x) {
|
||||
return false;
|
||||
}
|
||||
if (y < that.y) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns true if that object is an R2Vector with exactly the same x and y coordinates. */
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (!(that instanceof R2Vector)) {
|
||||
return false;
|
||||
}
|
||||
R2Vector thatPoint = (R2Vector) that;
|
||||
return this.x == thatPoint.x && this.y == thatPoint.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcualates hashcode based on stored coordinates. Since we want +0.0 and -0.0 to be treated the
|
||||
* same, we ignore the sign of the coordinates.
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long value = 17;
|
||||
value += 37 * value + Double.doubleToLongBits(Math.abs(x));
|
||||
value += 37 * value + Double.doubleToLongBits(Math.abs(y));
|
||||
return (int) (value ^ (value >>> 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "(" + x + ", " + y + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Copyright 2014 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtIncompatible;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* This class provides portable support for several exact arithmetic operations on double values,
|
||||
* without loss of precision. It stores an array of double values, and operations that require
|
||||
* additional bits of precision return Reals with larger arrays.
|
||||
*
|
||||
* <p>Converting a sequence of a dozen strictfp arithmetic operations to use Real can take up to 20
|
||||
* times longer than the natural but imprecise approach of using built in double operators like +
|
||||
* and *. Compared to other approaches like BigDecimal that consume more memory and typically slow
|
||||
* operations down by a factor of 100, that's great, but use of this class should still be avoided
|
||||
* when imprecise results will suffice.
|
||||
*
|
||||
* <p>This class exists as a package private element of the geometry library for the predicates in
|
||||
* {@link S2Predicates}, that require arbitrary precision arithmetic. It could be made suitable for
|
||||
* general usage by adding robust implementations of multiplication and division between two Reals,
|
||||
* and a toString() implementation that prints the exact summation of all the components.
|
||||
*
|
||||
* <p>Many of the algorithms in this class were adapted from the multiple components technique for
|
||||
* extended 64-bit IEEE 754 floating point precision, as described in:
|
||||
*
|
||||
* <pre>
|
||||
* Robust Adaptive Floating-Point Geometric Predicates
|
||||
* Jonathan Richard Shewchuk
|
||||
* School of Computer Science
|
||||
* Carnegie Mellon University
|
||||
* </pre>
|
||||
*
|
||||
* <p>Faster adaptive techniques are also presented in that paper, but are not implemented here.
|
||||
*/
|
||||
@GwtIncompatible(value = "No javascript support for strictfp.")
|
||||
strictfp class Real extends Number {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Used to split doubles into two half-length values, for exact multiplication. The value should
|
||||
* be Math.pow(2, Math.ceil(mantissaBits / 2)) + 1.
|
||||
*/
|
||||
private static final double SPLITTER;
|
||||
|
||||
static {
|
||||
// Find half ulp(1). We could use Math.ulp but it's not supported on GWT.
|
||||
double epsilon = 1.0;
|
||||
do {
|
||||
epsilon *= 0.5;
|
||||
} while (1.0 + epsilon != 1.0);
|
||||
int mantissaBits = (int) Math.round(-Math.log(epsilon) / Math.log(2));
|
||||
SPLITTER = (1 << ((mantissaBits + 1) / 2)) + 1;
|
||||
}
|
||||
|
||||
/** Returns the result of a + b, without loss of precision. */
|
||||
public static Real add(double a, double b) {
|
||||
double x = a + b;
|
||||
double error = twoSumError(a, b, x);
|
||||
return new Real(error, x);
|
||||
}
|
||||
|
||||
/** Returns the result of a - b, without loss of precision. */
|
||||
public static Real sub(double a, double b) {
|
||||
double x = a - b;
|
||||
double error = twoDiffError(a, b, x);
|
||||
return new Real(error, x);
|
||||
}
|
||||
|
||||
/** Returns the result of a * b, without loss of precision. */
|
||||
public static Real mul(double a, double b) {
|
||||
double x = a * b;
|
||||
double bhi = splitHigh(b);
|
||||
double blo = splitLow(b, bhi);
|
||||
double error = twoProductError(a, bhi, blo, x);
|
||||
return new Real(error, x);
|
||||
}
|
||||
|
||||
/**
|
||||
* A sequence of ordinary double values, ordered by magnitude in ascending order, containing no
|
||||
* zeroes and with no overlapping base 2 digits.
|
||||
*/
|
||||
private final double[] values;
|
||||
|
||||
/** Creates a Real based on the given double value. */
|
||||
public Real(double value) {
|
||||
values = new double[] {value};
|
||||
}
|
||||
|
||||
private Real(double... values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
/** Returns the result of a + b, without loss of precision. */
|
||||
public Real add(Real that) {
|
||||
return add(this, that, false);
|
||||
}
|
||||
|
||||
/** Returns the result of a - b, without loss of precision. */
|
||||
public Real sub(Real that) {
|
||||
return add(this, that, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result of adding together the components of a and b, inverting each element of b if
|
||||
* negateB is true.
|
||||
*/
|
||||
private static Real add(Real a, Real b, boolean negateB) {
|
||||
double bSign = negateB ? -1 : 1;
|
||||
double[] result = new double[a.values.length + b.values.length];
|
||||
int aIndex = 0;
|
||||
int bIndex = 0;
|
||||
|
||||
double sum;
|
||||
double newSum;
|
||||
double error;
|
||||
if (smallerMagnitude(a.values[aIndex], b.values[bIndex])) {
|
||||
sum = a.values[aIndex++];
|
||||
} else {
|
||||
sum = bSign * b.values[bIndex++];
|
||||
}
|
||||
|
||||
int resultIndex = 0;
|
||||
double smaller;
|
||||
if ((aIndex < a.values.length) && (bIndex < b.values.length)) {
|
||||
if (smallerMagnitude(a.values[aIndex], b.values[bIndex])) {
|
||||
smaller = a.values[aIndex++];
|
||||
} else {
|
||||
smaller = bSign * b.values[bIndex++];
|
||||
}
|
||||
newSum = smaller + sum;
|
||||
error = fastTwoSumError(smaller, sum, newSum);
|
||||
sum = newSum;
|
||||
if (error != 0.0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
while ((aIndex < a.values.length) && (bIndex < b.values.length)) {
|
||||
if (smallerMagnitude(a.values[aIndex], b.values[bIndex])) {
|
||||
smaller = a.values[aIndex++];
|
||||
} else {
|
||||
smaller = bSign * b.values[bIndex++];
|
||||
}
|
||||
newSum = sum + smaller;
|
||||
error = twoSumError(sum, smaller, newSum);
|
||||
sum = newSum;
|
||||
if (error != 0.0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (aIndex < a.values.length) {
|
||||
smaller = a.values[aIndex++];
|
||||
newSum = sum + smaller;
|
||||
error = twoSumError(sum, smaller, newSum);
|
||||
sum = newSum;
|
||||
if (error != 0.0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
}
|
||||
while (bIndex < b.values.length) {
|
||||
smaller = bSign * b.values[bIndex++];
|
||||
newSum = sum + smaller;
|
||||
error = twoSumError(sum, smaller, newSum);
|
||||
sum = newSum;
|
||||
if (error != 0.0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
}
|
||||
if ((sum != 0.0) || (resultIndex == 0)) {
|
||||
result[resultIndex++] = sum;
|
||||
}
|
||||
|
||||
if (result.length > resultIndex) {
|
||||
result = copyOf(result, resultIndex);
|
||||
}
|
||||
return new Real(result);
|
||||
}
|
||||
|
||||
/** Returns true if the magnitude of a is less than the magnitude of b. */
|
||||
private static boolean smallerMagnitude(double a, double b) {
|
||||
return (b > a) == (b > -a);
|
||||
}
|
||||
|
||||
/** Returns the result of this * scale, without loss of precision. */
|
||||
public Real mul(double scale) {
|
||||
double[] result = new double[values.length * 2];
|
||||
double scaleHigh = splitHigh(scale);
|
||||
double scaleLow = splitLow(scale, scaleHigh);
|
||||
double quotient = values[0] * scale;
|
||||
double error = twoProductError(values[0], scaleHigh, scaleLow, quotient);
|
||||
int resultIndex = 0;
|
||||
if (error != 0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
for (int i = 1; i < values.length; i++) {
|
||||
double term = values[i] * scale;
|
||||
double termError = twoProductError(values[i], scaleHigh, scaleLow, term);
|
||||
double sum = quotient + termError;
|
||||
error = twoSumError(quotient, termError, sum);
|
||||
if (error != 0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
quotient = term + sum;
|
||||
error = fastTwoSumError(term, sum, quotient);
|
||||
if (error != 0) {
|
||||
result[resultIndex++] = error;
|
||||
}
|
||||
}
|
||||
if ((quotient != 0.0) || (resultIndex == 0)) {
|
||||
result[resultIndex++] = quotient;
|
||||
}
|
||||
if (result.length > resultIndex) {
|
||||
result = copyOf(result, resultIndex);
|
||||
}
|
||||
return new Real(result);
|
||||
}
|
||||
|
||||
/** Returns the negative of this number. */
|
||||
public Real negate() {
|
||||
double[] copy = new double[values.length];
|
||||
for (int i = values.length - 1; i >= 0; i--) {
|
||||
copy[i] = -values[i];
|
||||
}
|
||||
return new Real(copy);
|
||||
}
|
||||
|
||||
/** Returns the signum of this number more quickly than via Math.signum(doubleValue()). */
|
||||
public int signum() {
|
||||
double msb = values[values.length - 1];
|
||||
if (msb > 0) {
|
||||
return 1;
|
||||
} else if (msb < 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the string representation of the double value nearest this Real. */
|
||||
@Override
|
||||
public String toString() {
|
||||
return Double.toString(doubleValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int intValue() {
|
||||
return (int) longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long longValue() {
|
||||
return Math.round(doubleValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public float floatValue() {
|
||||
return (float) doubleValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double doubleValue() {
|
||||
// Since the components are guaranteed to have no overlapping digits, we
|
||||
// could simply sum them without loss of precision... but to return a double
|
||||
// we truncate to the 53 bits of the largest exponent.
|
||||
double sum = 0;
|
||||
for (double value : values) {
|
||||
sum += value;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/** Returns a BigDecimal representation of this extended precision real value. */
|
||||
public BigDecimal bigValue() {
|
||||
BigDecimal sum = new BigDecimal(values[0]);
|
||||
for (int i = 1; i < values.length; i++) {
|
||||
sum = sum.add(new BigDecimal(values[i]));
|
||||
}
|
||||
return sum.stripTrailingZeros();
|
||||
}
|
||||
|
||||
private static double[] copyOf(double[] array, int newLength) {
|
||||
double[] result = new double[newLength];
|
||||
for (int i = 0; i < newLength; i++) {
|
||||
result[i] = array[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns the error in the sum x=a+b, when |a|>=|b|. */
|
||||
private static double fastTwoSumError(double a, double b, double x) {
|
||||
return b - (x - a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error in the sum x=a+b, when the relative magnitudes of a and b are not known in
|
||||
* advance.
|
||||
*/
|
||||
private static double twoSumError(double a, double b, double x) {
|
||||
double error = x - a;
|
||||
return (a - (x - error)) + (b - error);
|
||||
}
|
||||
|
||||
/** Returns the error in the difference x=a-b. */
|
||||
private static double twoDiffError(double a, double b, double x) {
|
||||
double error = a - x;
|
||||
return (a - (x + error)) + (error - b);
|
||||
}
|
||||
|
||||
/** Returns the high split for the given value. */
|
||||
private static double splitHigh(double a) {
|
||||
double c = SPLITTER * a;
|
||||
return c - (c - a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the low split for the given value and previously-computed high split as returned by
|
||||
* {@link #splitHigh(double)}.
|
||||
*/
|
||||
private static double splitLow(double a, double ahi) {
|
||||
return a - ahi;
|
||||
}
|
||||
|
||||
/** Returns the error in the product x=a*b, with precomputed splits for b. */
|
||||
private static double twoProductError(double a, double bhi, double blo, double x) {
|
||||
double ahi = splitHigh(a);
|
||||
double alo = splitLow(a, ahi);
|
||||
double err1 = x - (ahi * bhi);
|
||||
double err2 = err1 - (alo * bhi);
|
||||
double err3 = err2 - (ahi * blo);
|
||||
return (alo * blo) - err3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class S1Angle implements Comparable<S1Angle>, Serializable {
|
||||
/** An angle larger than any finite angle. */
|
||||
public static final S1Angle INFINITY = new S1Angle(Double.POSITIVE_INFINITY);
|
||||
|
||||
/** An explicit shorthand for the default constructor. */
|
||||
public static final S1Angle ZERO = new S1Angle();
|
||||
|
||||
private final double radians;
|
||||
|
||||
/** Returns the angle in radians. */
|
||||
public double radians() {
|
||||
return radians;
|
||||
}
|
||||
|
||||
/** Returns the angle in degrees. */
|
||||
public double degrees() {
|
||||
return radians * (180 / Math.PI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns angle in tens of microdegrees, rounded to the nearest ten microdegrees.
|
||||
*
|
||||
* <p>Normalized angles will never overflow an int.
|
||||
*
|
||||
* @throws IllegalArgumentException if the result overflows an int
|
||||
*/
|
||||
public int e5() {
|
||||
return Ints.checkedCast(Math.round(degrees() * 1e5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns angle in microdegrees, rounded to the nearest microdegree.
|
||||
*
|
||||
* <p>Normalized angles will never overflow an int.
|
||||
*
|
||||
* @throws IllegalArgumentException if the result overflows an int
|
||||
*/
|
||||
public int e6() {
|
||||
return Ints.checkedCast(Math.round(degrees() * 1e6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns angle in tenths of a microdegree, rounded to the nearest tenth of a microdegree.
|
||||
*
|
||||
* <p>Normalized angles will never overflow an int.
|
||||
*
|
||||
* @throws IllegalArgumentException if the result overflows an int
|
||||
*/
|
||||
public int e7() {
|
||||
return Ints.checkedCast(Math.round(degrees() * 1e7));
|
||||
}
|
||||
|
||||
/** The default constructor yields a zero angle. */
|
||||
public S1Angle() {
|
||||
this.radians = 0;
|
||||
}
|
||||
|
||||
private S1Angle(double radians) {
|
||||
this.radians = radians;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the angle between two points, which is also equal to the distance between these points
|
||||
* on the unit sphere. The points do not need to be normalized.
|
||||
*/
|
||||
public S1Angle(S2Point x, S2Point y) {
|
||||
this.radians = x.angle(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (that instanceof S1Angle) {
|
||||
return this.radians == ((S1Angle) that).radians;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long value = Double.doubleToLongBits(radians);
|
||||
return (int) (value ^ (value >>> 32));
|
||||
}
|
||||
|
||||
public boolean lessThan(S1Angle that) {
|
||||
return this.radians < that.radians;
|
||||
}
|
||||
|
||||
public boolean greaterThan(S1Angle that) {
|
||||
return this.radians > that.radians;
|
||||
}
|
||||
|
||||
public boolean lessOrEquals(S1Angle that) {
|
||||
return this.radians <= that.radians;
|
||||
}
|
||||
|
||||
public boolean greaterOrEquals(S1Angle that) {
|
||||
return this.radians >= that.radians;
|
||||
}
|
||||
|
||||
public static S1Angle max(S1Angle left, S1Angle right) {
|
||||
return right.greaterThan(left) ? right : left;
|
||||
}
|
||||
|
||||
public static S1Angle min(S1Angle left, S1Angle right) {
|
||||
return right.greaterThan(left) ? left : right;
|
||||
}
|
||||
|
||||
/** Returns a new S1Angle specified in radians. */
|
||||
public static S1Angle radians(double radians) {
|
||||
return new S1Angle(radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new S1Angle converted from degrees. Note that <code>degrees(x).degrees() == x</code>
|
||||
* may not hold due to inexact arithmetic.
|
||||
*/
|
||||
public static S1Angle degrees(double degrees) {
|
||||
return new S1Angle(degrees * (Math.PI / 180));
|
||||
}
|
||||
|
||||
/** Returns a new S1Angle converted from tens of microdegrees. */
|
||||
public static S1Angle e5(int e5) {
|
||||
return degrees(e5 * 1e-5);
|
||||
}
|
||||
|
||||
/** Returns a new S1Angle converted from microdegrees. */
|
||||
public static S1Angle e6(int e6) {
|
||||
// Multiplying by 1e-6 isn't quite as accurate as dividing by 1e6,
|
||||
// but it's about 10 times faster and more than accurate enough.
|
||||
return degrees(e6 * 1e-6);
|
||||
}
|
||||
|
||||
/** Returns a new S1Angle converted from tenths of a microdegree. */
|
||||
public static S1Angle e7(int e7) {
|
||||
return degrees(e7 * 1e-7);
|
||||
}
|
||||
|
||||
/** Returns the distance along the surface of a sphere of the given radius. */
|
||||
public double distance(double radius) {
|
||||
return radians * radius;
|
||||
}
|
||||
|
||||
public S1Angle neg() {
|
||||
return new S1Angle(-radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this + a)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle add(S1Angle a) {
|
||||
return new S1Angle(radians + a.radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this - a)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle sub(S1Angle a) {
|
||||
return new S1Angle(radians - a.radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this * m)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle mul(double m) {
|
||||
return new S1Angle(radians * m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this / d)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle div(double d) {
|
||||
return new S1Angle(radians / d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the trigonometric cosine of the angle.
|
||||
*/
|
||||
public double cos() {
|
||||
return Math.cos(radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the trigonometric sine of the angle.
|
||||
*/
|
||||
public double sin() {
|
||||
return Math.sin(radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the trigonometric tangent of the angle.
|
||||
*/
|
||||
public double tan() {
|
||||
return Math.tan(radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the angle normalized to the range (-180, 180] degrees.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle normalize() {
|
||||
final boolean isNormalized = radians > -Math.PI && radians <= Math.PI;
|
||||
if (isNormalized) {
|
||||
return this;
|
||||
}
|
||||
double normalized = Platform.IEEEremainder(radians, 2.0 * Math.PI);
|
||||
if (normalized <= -Math.PI) {
|
||||
normalized = Math.PI;
|
||||
}
|
||||
assert normalized > -Math.PI;
|
||||
assert normalized <= Math.PI;
|
||||
return new S1Angle(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the angle in degrees with a "d" suffix, e.g. "17.3745d". By default 6 digits are
|
||||
* printed; this can be changed using setprecision(). Up to 17 digits are required to distinguish
|
||||
* one angle from another.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return degrees() + "d";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(S1Angle that) {
|
||||
return this.radians < that.radians ? -1 : this.radians > that.radians ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Creates a new Builder initialized to a copy of this angle. */
|
||||
public Builder toBuilder() {
|
||||
return new Builder().add(this);
|
||||
}
|
||||
|
||||
/** A builder of {@link S1Angle} instances. */
|
||||
public static final class Builder {
|
||||
private double radians;
|
||||
|
||||
/** Constructs a new builder initialized to {@link #ZERO}. */
|
||||
public Builder() {}
|
||||
|
||||
/** Adds angle. */
|
||||
public Builder add(S1Angle angle) {
|
||||
radians += angle.radians;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Adds radians. */
|
||||
public Builder add(double radians) {
|
||||
this.radians += radians;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a new {@link S1Angle} copied from the current state of this builder. */
|
||||
public S1Angle build() {
|
||||
return new S1Angle(radians);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
* Copyright 2014 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static java.lang.Math.asin;
|
||||
import static java.lang.Math.sqrt;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.primitives.Doubles;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* S1ChordAngle represents the angle subtended by a chord (i.e., the straight 3D Cartesian line
|
||||
* segment connecting two points on the unit sphere). Its representation makes it very efficient for
|
||||
* computing and comparing distances, but unlike S1Angle it is only capable of representing angles
|
||||
* between 0 and Pi radians. Generally, S1ChordAngle should only be used in loops where many angles
|
||||
* need to be calculated and compared. Otherwise it is simpler to use S1Angle.
|
||||
*
|
||||
* <p>S1ChordAngle also loses some accuracy as the angle approaches Pi radians. Specifically, the
|
||||
* representation of (Pi - x) radians can be expected to have an error of about (1e-15 / x), with a
|
||||
* maximum error of about 1e-7.
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class S1ChordAngle implements Comparable<S1ChordAngle>, Serializable {
|
||||
|
||||
/** Max value that can be returned from {@link #getLength2()}. */
|
||||
public static final double MAX_LENGTH2 = 4.0;
|
||||
|
||||
/** The zero chord angle. */
|
||||
public static final S1ChordAngle ZERO = new S1ChordAngle(0);
|
||||
|
||||
/** The chord angle of 90 degrees (a "right angle"). */
|
||||
public static final S1ChordAngle RIGHT = new S1ChordAngle(2);
|
||||
|
||||
/** The chord angle of 180 degrees (a "straight angle"). This is the max finite chord angle. */
|
||||
public static final S1ChordAngle STRAIGHT = new S1ChordAngle(MAX_LENGTH2);
|
||||
|
||||
/**
|
||||
* A chord angle larger than any finite chord angle. The only valid operations on {@code INFINITY}
|
||||
* are comparisons and {@link S1Angle} conversions.
|
||||
*/
|
||||
public static final S1ChordAngle INFINITY = new S1ChordAngle(Double.POSITIVE_INFINITY);
|
||||
|
||||
/**
|
||||
* A chord angle smaller than {@link #ZERO}. The only valid operations on {@code NEGATIVE} are
|
||||
* comparisons and {@link S1Angle} conversions.
|
||||
*/
|
||||
public static final S1ChordAngle NEGATIVE = new S1ChordAngle(-1);
|
||||
|
||||
private final double length2;
|
||||
|
||||
/**
|
||||
* Constructs the S1ChordAngle corresponding to the distance between the two given points. The
|
||||
* points must be unit length.
|
||||
*/
|
||||
public S1ChordAngle(S2Point x, S2Point y) {
|
||||
checkArgument(S2.isUnitLength(x));
|
||||
checkArgument(S2.isUnitLength(y));
|
||||
// The distance may slightly exceed 4.0 due to roundoff errors.
|
||||
length2 = Math.min(MAX_LENGTH2, x.getDistance2(y));
|
||||
checkArgument(isValid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new chord angle approximated from {@code angle} (see {@link
|
||||
* #getS1AngleConstructorMaxError()} for the max magnitude of the error).
|
||||
*
|
||||
* <p>Angles outside the range [0, Pi] are handled as follows:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link S1Angle#INFINITY} is mapped to {@link #INFINITY}
|
||||
* <li>negative angles are mapped to {@link #NEGATIVE}
|
||||
* <li>finite angles larger than Pi are mapped to {@link #STRAIGHT}
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that this operation is relatively expensive and should be avoided. To use {@link
|
||||
* S1ChordAngle} effectively, you should structure your code so that input arguments are converted
|
||||
* to S1ChordAngles at the beginning of your algorithm, and results are converted back to {@link
|
||||
* S1Angle}s only at the end.
|
||||
*/
|
||||
public static S1ChordAngle fromS1Angle(S1Angle angle) {
|
||||
if (angle.radians() < 0) {
|
||||
return NEGATIVE;
|
||||
} else if (angle.equals(S1Angle.INFINITY)) {
|
||||
return INFINITY;
|
||||
} else {
|
||||
// The chord length is 2 * sin(angle / 2).
|
||||
double length = 2 * Math.sin(0.5 * Math.min(Math.PI, angle.radians()));
|
||||
return new S1ChordAngle(length * length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* S1ChordAngles are represented by the squared chord length, which can range from 0 to {@code
|
||||
* MAX_LENGTH2}. {@link #INFINITY} uses an infinite squared length.
|
||||
*/
|
||||
private S1ChordAngle(double length2) {
|
||||
this.length2 = length2;
|
||||
checkArgument(isValid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an S1ChordAngle from the squared chord length. Note that the argument is
|
||||
* automatically clamped to a maximum of {@code MAX_LENGTH2} to handle possible roundoff errors.
|
||||
* The argument must be non-negative.
|
||||
*/
|
||||
public static S1ChordAngle fromLength2(double length2) {
|
||||
return new S1ChordAngle(Math.min(MAX_LENGTH2, length2));
|
||||
}
|
||||
|
||||
/** Returns whether the chord distance is exactly 0. */
|
||||
public boolean isZero() {
|
||||
return length2 == 0;
|
||||
}
|
||||
|
||||
/** Returns whether the chord distance is negative. */
|
||||
public boolean isNegative() {
|
||||
return length2 < 0;
|
||||
}
|
||||
|
||||
/** Returns whether the chord distance is exactly (positive) infinity. */
|
||||
public boolean isInfinity() {
|
||||
return length2 == Double.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
/** Returns true if the angle is negative or infinity. */
|
||||
public boolean isSpecial() {
|
||||
return isNegative() || isInfinity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if getLength2() is within the normal range of 0 to 4 (inclusive) or the angle is
|
||||
* special.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return (length2 >= 0 && length2 <= MAX_LENGTH2) || isNegative() || isInfinity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the chord angle to an {@link S1Angle}. {@link #INFINITY} is converted to {@link
|
||||
* S1Angle#INFINITY}, and {@link #NEGATIVE} is converted to a negative {@link S1Angle}. This
|
||||
* operation is relatively expensive.
|
||||
*/
|
||||
public S1Angle toAngle() {
|
||||
if (isNegative()) {
|
||||
return S1Angle.radians(-1);
|
||||
} else if (isInfinity()) {
|
||||
return S1Angle.INFINITY;
|
||||
} else {
|
||||
return S1Angle.radians(2 * asin(0.5 * sqrt(length2)));
|
||||
}
|
||||
}
|
||||
|
||||
/** The squared length of the chord. (Most clients will not need this.) */
|
||||
public double getLength2() {
|
||||
return length2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest representable S1ChordAngle larger than this object. This can be used to
|
||||
* convert a "<" comparison to a "<=" comparison.
|
||||
*
|
||||
* <p>Note the following special cases:
|
||||
*
|
||||
* <ul>
|
||||
* <li>NEGATIVE.successor() == ZERO
|
||||
* <li>STRAIGHT.successor() == INFINITY
|
||||
* <li>INFINITY.Successor() == INFINITY
|
||||
* </ul>
|
||||
*/
|
||||
public S1ChordAngle successor() {
|
||||
if (length2 >= MAX_LENGTH2) {
|
||||
return INFINITY;
|
||||
}
|
||||
if (length2 < 0.0) {
|
||||
return ZERO;
|
||||
}
|
||||
return new S1ChordAngle(Platform.nextAfter(length2, 10.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #successor}, but returns the largest representable S1ChordAngle less than this
|
||||
* object.
|
||||
*
|
||||
* <p>Note the following special cases:
|
||||
*
|
||||
* <ul>
|
||||
* <li>INFINITY.predecessor() == STRAIGHT
|
||||
* <li>ZERO.predecessor() == NEGATIVE
|
||||
* <li>NEGATIVE.predecessor() == NEGATIVE
|
||||
* </ul>
|
||||
*/
|
||||
public S1ChordAngle predecessor() {
|
||||
if (length2 <= 0.0) {
|
||||
return NEGATIVE;
|
||||
}
|
||||
if (length2 > MAX_LENGTH2) {
|
||||
return STRAIGHT;
|
||||
}
|
||||
return new S1ChordAngle(Platform.nextAfter(length2, -10.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new S1ChordAngle whose chord distance represents the sum of the angular distances
|
||||
* represented by the 'a' and 'b' chord angles.
|
||||
*
|
||||
* <p>Note that this method is much more efficient than converting the chord angles to S1Angles
|
||||
* and adding those. It requires only one square root plus a few additions and multiplications.
|
||||
*/
|
||||
public static S1ChordAngle add(S1ChordAngle a, S1ChordAngle b) {
|
||||
checkArgument(!a.isSpecial());
|
||||
checkArgument(!b.isSpecial());
|
||||
|
||||
// Optimization for the common case where "b" is an error tolerance parameter that happens to be
|
||||
// set to zero.
|
||||
double a2 = a.length2;
|
||||
double b2 = b.length2;
|
||||
if (b2 == 0) {
|
||||
return a;
|
||||
}
|
||||
|
||||
// Clamp the angle sum to at most 180 degrees.
|
||||
if (a2 + b2 >= MAX_LENGTH2) {
|
||||
return S1ChordAngle.STRAIGHT;
|
||||
}
|
||||
|
||||
// Let "a" and "b" be the (non-squared) chord lengths, and let c = a+b.
|
||||
// Let A, B, and C be the corresponding half-angles (a = 2*sin(A), etc).
|
||||
// Then the formula below can be derived from c = 2 * sin(A+B) and the relationships
|
||||
// sin(A+B) = sin(A)*cos(B) + sin(B)*cos(A)
|
||||
// cos(X) = sqrt(1 - sin^2(X)) .
|
||||
double x = a2 * (1 - 0.25 * b2); // isValid() => non-negative
|
||||
double y = b2 * (1 - 0.25 * a2); // isValid() => non-negative
|
||||
return new S1ChordAngle(Math.min(MAX_LENGTH2, x + y + 2 * sqrt(x * y)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract one S1ChordAngle from another.
|
||||
*
|
||||
* <p>Note that this method is much more efficient than converting the chord angles to S1Angles
|
||||
* and adding those. It requires only one square root plus a few additions and multiplications.
|
||||
*/
|
||||
public static S1ChordAngle sub(S1ChordAngle a, S1ChordAngle b) {
|
||||
// See comments in add(S1ChordAngle, S1ChordAngle).
|
||||
checkArgument(!a.isSpecial());
|
||||
checkArgument(!b.isSpecial());
|
||||
double a2 = a.length2;
|
||||
double b2 = b.length2;
|
||||
if (b2 == 0) {
|
||||
return a;
|
||||
}
|
||||
if (a2 <= b2) {
|
||||
return S1ChordAngle.ZERO;
|
||||
}
|
||||
double x = a2 * (1 - 0.25 * b2);
|
||||
double y = b2 * (1 - 0.25 * a2);
|
||||
return new S1ChordAngle(Math.max(0.0, x + y - 2 * sqrt(x * y)));
|
||||
}
|
||||
|
||||
/** Returns the smaller of the given instances. */
|
||||
public static S1ChordAngle min(S1ChordAngle a, S1ChordAngle b) {
|
||||
return a.length2 <= b.length2 ? a : b;
|
||||
}
|
||||
|
||||
/** Returns the larger of the given instances. */
|
||||
public static S1ChordAngle max(S1ChordAngle a, S1ChordAngle b) {
|
||||
return a.length2 > b.length2 ? a : b;
|
||||
}
|
||||
|
||||
/** Returns the square of Math.sin(toAngle().radians()), but computed more efficiently. */
|
||||
public static double sin2(S1ChordAngle a) {
|
||||
checkArgument(!a.isSpecial());
|
||||
// Let "a" be the (non-squared) chord length, and let A be the corresponding half-angle
|
||||
// (a = 2*sin(A)). The formula below can be derived from:
|
||||
// sin(2*A) = 2 * sin(A) * cos(A)
|
||||
// cos^2(A) = 1 - sin^2(A)
|
||||
// This is much faster than converting to an angle and computing its sine.
|
||||
return a.length2 * (1 - 0.25 * a.length2);
|
||||
}
|
||||
|
||||
/** Returns Math.sin(toAngle().radians()), but computed more efficiently. */
|
||||
public static double sin(S1ChordAngle a) {
|
||||
return sqrt(sin2(a));
|
||||
}
|
||||
|
||||
/** Returns Math.cos(toAngle().radians()), but computed more efficiently. */
|
||||
public static double cos(S1ChordAngle a) {
|
||||
// cos(2*A) = cos^2(A) - sin^2(A) = 1 - 2*sin^2(A)
|
||||
checkArgument(!a.isSpecial());
|
||||
return 1 - 0.5 * a.length2;
|
||||
}
|
||||
|
||||
/** Returns Math.tan(toAngle().radians()), but computed more efficiently. */
|
||||
public static double tan(S1ChordAngle a) {
|
||||
return sin(a) / cos(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new S1ChordAngle that has been adjusted by the given error bound (which can be
|
||||
* positive or negative). {@code error} should be the value returned by one of the error bound
|
||||
* methods below. For example:
|
||||
*
|
||||
* <pre>
|
||||
* {@code S1ChordAngle a = new S1ChordAngle(x, y);}
|
||||
* {@code S1ChordAngle a1 = a.plusError(a.getS2PointConstructorMaxError());}
|
||||
* </pre>
|
||||
*
|
||||
* <p>If this {@link #isSpecial}, we return {@code this}.
|
||||
*/
|
||||
public S1ChordAngle plusError(double error) {
|
||||
return isSpecial() ? this : fromLength2(Math.max(0.0, Math.min(MAX_LENGTH2, length2 + error)));
|
||||
}
|
||||
|
||||
/** Returns the error in {@link #fromS1Angle}. */
|
||||
public double getS1AngleConstructorMaxError() {
|
||||
return S2.DBL_EPSILON * length2;
|
||||
}
|
||||
|
||||
/**
|
||||
* There is a relative error of {@code 2.5 * DBL_EPSILON} when computing the squared distance,
|
||||
* plus a relative error of {@code 2 * DBL_EPSILON} and an absolute error of {@code 16 *
|
||||
* DBL_EPSILON^2} because the lengths of the input points may differ from 1 by up to {@code 2 *
|
||||
* DBL_EPSILON} each. (This is the maximum length error in {@link S2Point#normalize}).
|
||||
*/
|
||||
public double getS2PointConstructorMaxError() {
|
||||
return (4.5 * S2.DBL_EPSILON * length2) + (16 * S2.DBL_EPSILON * S2.DBL_EPSILON);
|
||||
}
|
||||
|
||||
/** Returns the string of the closest {@link S1Angle} to this chord distance. */
|
||||
@Override
|
||||
public String toString() {
|
||||
return toAngle().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(S1ChordAngle that) {
|
||||
return Double.compare(this.length2, that.length2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (other instanceof S1ChordAngle) && length2 == ((S1ChordAngle) other).length2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return length2 == 0.0 ? 0 : Doubles.hashCode(length2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,670 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
* An S1Interval represents a closed interval on a unit circle (also known as a 1-dimensional
|
||||
* sphere). It is capable of representing the empty interval (containing no points), the full
|
||||
* interval (containing all points), and zero-length intervals (containing a single point).
|
||||
*
|
||||
* <p>Points are represented by the angle they make with the positive x-axis in the range [-Pi, Pi].
|
||||
* An interval is represented by its lower and upper bounds (both inclusive, since the interval is
|
||||
* closed). The lower bound may be greater than the upper bound, in which case the interval is
|
||||
* "inverted" (i.e. it passes through the point (-1, 0)).
|
||||
*
|
||||
* <p>Note that the point (-1, 0) has two valid representations, Pi and -Pi. The normalized
|
||||
* representation of this point internally is Pi, so that endpoints of normal intervals are in the
|
||||
* range (-Pi, Pi]. However, we take advantage of the point -Pi to construct two special intervals:
|
||||
* the full() interval is [-Pi, Pi], and the Empty() interval is [Pi, -Pi].
|
||||
*
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class S1Interval implements Cloneable, Serializable {
|
||||
private double lo;
|
||||
private double hi;
|
||||
|
||||
public S1Interval() {
|
||||
setEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Both endpoints must be in the range -Pi to Pi inclusive. The value -Pi is converted internally
|
||||
* to Pi except for the full() and empty() intervals.
|
||||
*/
|
||||
public S1Interval(double lo, double hi) {
|
||||
this(lo, hi, false);
|
||||
}
|
||||
|
||||
/** Copy constructor. Assumes that the {@code interval} is valid. */
|
||||
public S1Interval(S1Interval interval) {
|
||||
this.lo = interval.lo;
|
||||
this.hi = interval.hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal constructor that just passes the arguments down to {@link #set(double, double,
|
||||
* boolean)}.
|
||||
*/
|
||||
private S1Interval(double lo, double hi, boolean checked) {
|
||||
set(lo, hi, checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the range of this interval, assuming both arguments are in the correct range, i.e.
|
||||
* normalization from -Pi to Pi is already done. If {@code checked} is false, endpoints at -Pi
|
||||
* will be moved to +Pi unless the other endpoint is already there.
|
||||
*
|
||||
* <p>Note that because S1Interval has invariants to maintain after each update, values cannot be
|
||||
* set singly, both endpoints must be set together.
|
||||
*/
|
||||
void set(double newLo, double newHi, boolean checked) {
|
||||
this.lo = newLo;
|
||||
this.hi = newHi;
|
||||
if (!checked) {
|
||||
if (newLo == -S2.M_PI && newHi != S2.M_PI) {
|
||||
lo = S2.M_PI;
|
||||
}
|
||||
if (newHi == -S2.M_PI && newLo != S2.M_PI) {
|
||||
hi = S2.M_PI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the range of this interval to the empty interval.
|
||||
*
|
||||
* <p>Package private since only S2 code needs to mutate S1Intervals for now.
|
||||
*/
|
||||
void setEmpty() {
|
||||
lo = S2.M_PI;
|
||||
hi = -S2.M_PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the range of this interval to the full interval.
|
||||
*
|
||||
* <p>Package private since only S2 code needs to mutate S1Intervals for now.
|
||||
*/
|
||||
void setFull() {
|
||||
lo = -S2.M_PI;
|
||||
hi = S2.M_PI;
|
||||
}
|
||||
|
||||
public static S1Interval empty() {
|
||||
S1Interval result = new S1Interval();
|
||||
result.setEmpty();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static S1Interval full() {
|
||||
S1Interval result = new S1Interval();
|
||||
result.setFull();
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Convenience method to construct an interval containing a single point. */
|
||||
public static S1Interval fromPoint(double radians) {
|
||||
if (radians == -S2.M_PI) {
|
||||
radians = S2.M_PI;
|
||||
}
|
||||
return new S1Interval(radians, radians, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to construct the minimal interval containing the two given points. This is
|
||||
* equivalent to starting with an empty interval and calling addPoint() twice, but it is more
|
||||
* efficient.
|
||||
*/
|
||||
public static S1Interval fromPointPair(double p1, double p2) {
|
||||
// assert (Math.abs(p1) <= S2.M_PI && Math.abs(p2) <= S2.M_PI);
|
||||
S1Interval result = new S1Interval();
|
||||
result.initFromPointPair(p1, p2);
|
||||
return result;
|
||||
}
|
||||
|
||||
void initFromPointPair(double p1, double p2) {
|
||||
if (p1 == -S2.M_PI) {
|
||||
p1 = S2.M_PI;
|
||||
}
|
||||
if (p2 == -S2.M_PI) {
|
||||
p2 = S2.M_PI;
|
||||
}
|
||||
if (positiveDistance(p1, p2) <= S2.M_PI) {
|
||||
this.lo = p1;
|
||||
this.hi = p2;
|
||||
} else {
|
||||
this.lo = p2;
|
||||
this.hi = p1;
|
||||
}
|
||||
}
|
||||
|
||||
public double lo() {
|
||||
return lo;
|
||||
}
|
||||
|
||||
public double hi() {
|
||||
return hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interval is valid if neither bound exceeds Pi in absolute value, and the value -Pi appears
|
||||
* only in the Empty() and full() intervals.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return (Math.abs(lo) <= S2.M_PI
|
||||
&& Math.abs(hi) <= S2.M_PI
|
||||
&& !(lo == -S2.M_PI && hi != S2.M_PI)
|
||||
&& !(hi == -S2.M_PI && lo != S2.M_PI));
|
||||
}
|
||||
|
||||
/** Returns true if the interval contains all points on the unit circle. */
|
||||
public boolean isFull() {
|
||||
return hi - lo == 2 * S2.M_PI;
|
||||
}
|
||||
|
||||
/** Returns true if the interval is empty, i.e. it contains no points. */
|
||||
public boolean isEmpty() {
|
||||
return lo - hi == 2 * S2.M_PI;
|
||||
}
|
||||
|
||||
/** Returns true if lo() > hi(). (This is true for empty intervals.) */
|
||||
public boolean isInverted() {
|
||||
return lo > hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the midpoint of the interval. For full and empty intervals, the result is arbitrary.
|
||||
*/
|
||||
public double getCenter() {
|
||||
double center = 0.5 * (lo + hi);
|
||||
if (!isInverted()) {
|
||||
return center;
|
||||
}
|
||||
// Return the center in the range (-Pi, Pi].
|
||||
return (center <= 0) ? (center + S2.M_PI) : (center - S2.M_PI);
|
||||
}
|
||||
|
||||
/** Returns the length of the interval. The length of an empty interval is negative. */
|
||||
public double getLength() {
|
||||
double length = hi - lo;
|
||||
if (length >= 0) {
|
||||
return length;
|
||||
}
|
||||
length += 2 * S2.M_PI;
|
||||
// Empty intervals have a negative length.
|
||||
return (length > 0) ? length : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the complement of the interior of the interval. An interval and its complement have the
|
||||
* same boundary but do not share any interior values. The complement operator is not a bijection,
|
||||
* since the complement of a singleton interval (containing a single value) is the same as the
|
||||
* complement of an empty interval.
|
||||
*/
|
||||
public S1Interval complement() {
|
||||
if (lo == hi) {
|
||||
return full(); // Singleton.
|
||||
}
|
||||
return new S1Interval(hi, lo, true); // Handles
|
||||
// empty and
|
||||
// full.
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the midpoint of the complement of the interval. For full and empty intervals, the result
|
||||
* is arbitrary. For a singleton interval (containing a single point), the result is its antipodal
|
||||
* point on S1.
|
||||
*/
|
||||
public double getComplementCenter() {
|
||||
if (lo() != hi()) {
|
||||
return complement().getCenter();
|
||||
} else { // Singleton.
|
||||
return (hi() <= 0) ? (hi() + S2.M_PI) : (hi() - S2.M_PI);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if the interval (which is closed) contains the point 'p'. */
|
||||
public boolean contains(double p) {
|
||||
// Works for empty, full, and singleton intervals.
|
||||
// assert (Math.abs(p) <= S2.M_PI);
|
||||
if (p == -S2.M_PI) {
|
||||
p = S2.M_PI;
|
||||
}
|
||||
return fastContains(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the interval (which is closed) contains the point 'p'. Skips the normalization
|
||||
* of 'p' from -Pi to Pi.
|
||||
*/
|
||||
public boolean fastContains(double p) {
|
||||
if (isInverted()) {
|
||||
return (p >= lo || p <= hi) && !isEmpty();
|
||||
} else {
|
||||
return p >= lo && p <= hi;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if the interior of the interval contains the point 'p'. */
|
||||
public boolean interiorContains(double p) {
|
||||
// Works for empty, full, and singleton intervals.
|
||||
// assert (Math.abs(p) <= S2.M_PI);
|
||||
if (p == -S2.M_PI) {
|
||||
p = S2.M_PI;
|
||||
}
|
||||
|
||||
if (isInverted()) {
|
||||
return p > lo || p < hi;
|
||||
} else {
|
||||
return (p > lo && p < hi) || isFull();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the interval contains the interval {@code y}. Works for empty, full, and
|
||||
* singleton intervals.
|
||||
*/
|
||||
public boolean contains(final S1Interval y) {
|
||||
// It might be helpful to compare the structure of these tests to
|
||||
// the simpler Contains(double) method above.
|
||||
|
||||
if (isInverted()) {
|
||||
if (y.isInverted()) {
|
||||
return y.lo >= lo && y.hi <= hi;
|
||||
}
|
||||
return (y.lo >= lo || y.hi <= hi) && !isEmpty();
|
||||
} else {
|
||||
if (y.isInverted()) {
|
||||
return isFull() || y.isEmpty();
|
||||
}
|
||||
return y.lo >= lo && y.hi <= hi;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the interior of this interval contains the entire interval 'y'. Note that
|
||||
* x.interiorContains(x) is true only when x is the empty or full interval, and
|
||||
* x.interiorContains(S1Interval(p,p)) is equivalent to x.InteriorContains(p).
|
||||
*/
|
||||
public boolean interiorContains(final S1Interval y) {
|
||||
if (isInverted()) {
|
||||
if (!y.isInverted()) {
|
||||
return y.lo > lo || y.hi < hi;
|
||||
}
|
||||
return (y.lo > lo && y.hi < hi) || y.isEmpty();
|
||||
} else {
|
||||
if (y.isInverted()) {
|
||||
return isFull() || y.isEmpty();
|
||||
}
|
||||
return (y.lo > lo && y.hi < hi) || isFull();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the two intervals contain any points in common. Note that the point +/-Pi has
|
||||
* two representations, so the intervals [-Pi,-3] and [2,Pi] intersect, for example.
|
||||
*/
|
||||
public boolean intersects(final S1Interval y) {
|
||||
if (isEmpty() || y.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (isInverted()) {
|
||||
// Every non-empty inverted interval contains Pi.
|
||||
return y.isInverted() || y.lo <= hi || y.hi >= lo;
|
||||
} else {
|
||||
if (y.isInverted()) {
|
||||
return y.lo <= hi || y.hi >= lo;
|
||||
}
|
||||
return y.lo <= hi && y.hi >= lo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the interior of this interval contains any point of the interval {@code y}
|
||||
* (including its boundary). Works for empty, full, and singleton intervals.
|
||||
*/
|
||||
public boolean interiorIntersects(final S1Interval y) {
|
||||
if (isEmpty() || y.isEmpty() || lo == hi) {
|
||||
return false;
|
||||
}
|
||||
if (isInverted()) {
|
||||
return y.isInverted() || y.lo < hi || y.hi > lo;
|
||||
} else {
|
||||
if (y.isInverted()) {
|
||||
return y.lo < hi || y.hi > lo;
|
||||
}
|
||||
return (y.lo < hi && y.hi > lo) || isFull();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Hausdorff distance to the given interval {@code y}. For two S1Intervals x and y,
|
||||
* this distance is defined by {@code h(x, y) = max_{p in x} min_{q in y} d(p, q)}, where {@code
|
||||
* d(.,.)} is measured along S1.
|
||||
*/
|
||||
public double getDirectedHausdorffDistance(final S1Interval y) {
|
||||
if (y.contains(this)) {
|
||||
return 0.0; // this includes the case *this is empty
|
||||
}
|
||||
if (y.isEmpty()) {
|
||||
return S2.M_PI; // maximum possible distance on S1
|
||||
}
|
||||
|
||||
double yComplementCenter = y.getComplementCenter();
|
||||
if (contains(yComplementCenter)) {
|
||||
return positiveDistance(y.hi(), yComplementCenter);
|
||||
} else {
|
||||
// The Hausdorff distance is realized by either two hi() endpoints or two
|
||||
// lo() endpoints, whichever is farther apart.
|
||||
double hiHi =
|
||||
new S1Interval(y.hi(), yComplementCenter).contains(hi())
|
||||
? positiveDistance(y.hi(), hi())
|
||||
: 0;
|
||||
double loLo =
|
||||
new S1Interval(yComplementCenter, y.lo()).contains(lo())
|
||||
? positiveDistance(lo(), y.lo())
|
||||
: 0;
|
||||
// assert (hiHi > 0 || loLo > 0);
|
||||
return Math.max(hiHi, loLo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the interval by the minimum amount necessary so that it contains the point {@code p}
|
||||
* (an angle in the range [-Pi, Pi]).
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Interval addPoint(double p) {
|
||||
// assert (Math.abs(p) <= S2.M_PI);
|
||||
if (p == -S2.M_PI) {
|
||||
p = S2.M_PI;
|
||||
}
|
||||
|
||||
if (fastContains(p)) {
|
||||
return new S1Interval(this);
|
||||
}
|
||||
|
||||
if (isEmpty()) {
|
||||
return S1Interval.fromPoint(p);
|
||||
} else {
|
||||
// Compute distance from p to each endpoint.
|
||||
double dlo = positiveDistance(p, lo);
|
||||
double dhi = positiveDistance(hi, p);
|
||||
if (dlo < dhi) {
|
||||
return new S1Interval(p, hi);
|
||||
} else {
|
||||
return new S1Interval(lo, p);
|
||||
}
|
||||
// Adding a point can never turn a non-full interval into a full one.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the closest point in the interval to the point {@code p}. The interval must be
|
||||
* non-empty.
|
||||
*/
|
||||
public double clampPoint(double p) {
|
||||
// assert (!isEmpty());
|
||||
// assert (Math.abs(p) <= S2.M_PI);
|
||||
if (p == -S2.M_PI) {
|
||||
p = S2.M_PI;
|
||||
}
|
||||
|
||||
if (fastContains(p)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
// Compute distance from p to each endpoint.
|
||||
double dlo = positiveDistance(p, lo);
|
||||
double dhi = positiveDistance(hi, p);
|
||||
return (dlo < dhi) ? lo : hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new interval that has been expanded on each side by the distance {@code margin}. If
|
||||
* "margin" is negative, then shrink the interval on each side by "margin" instead. The resulting
|
||||
* interval may be empty or full. Any expansion (positive or negative) of a full interval remains
|
||||
* full, and any expansion of an empty interval remains empty.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Interval expanded(double margin) {
|
||||
S1Interval copy = new S1Interval(this);
|
||||
copy.expandedInternal(margin);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands this interval on each side by the distance {@code margin}. If "margin" is negative,
|
||||
* then shrink the interval on each side by "margin" instead. The resulting interval may be empty
|
||||
* or full. Any expansion (positive or negative) of a full interval remains full, and any
|
||||
* expansion of an empty interval remains empty.
|
||||
*
|
||||
* <p>Package private since only S2 code should be mutating S1Intervals for now.
|
||||
*/
|
||||
void expandedInternal(double margin) {
|
||||
if (margin >= 0) {
|
||||
if (isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Check whether this interval will be full after expansion, allowing
|
||||
// for a 1-bit rounding error when computing each endpoint.
|
||||
if (getLength() + 2 * margin + 2 * S2.DBL_EPSILON >= 2 * S2.M_PI) {
|
||||
setFull();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (isFull()) {
|
||||
return;
|
||||
}
|
||||
// Check whether this interval will be empty after expansion, allowing
|
||||
// for a 1-bit rounding error when computing each endpoint.
|
||||
if (getLength() + 2 * margin - 2 * S2.DBL_EPSILON <= 0) {
|
||||
setEmpty();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
set(
|
||||
Platform.IEEEremainder(lo - margin, 2 * S2.M_PI),
|
||||
Platform.IEEEremainder(hi + margin, 2 * S2.M_PI),
|
||||
false);
|
||||
if (lo <= -S2.M_PI) {
|
||||
lo = S2.M_PI;
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the smallest interval that contains this interval and the interval {@code y}. */
|
||||
@CheckReturnValue
|
||||
public S1Interval union(S1Interval y) {
|
||||
S1Interval result = new S1Interval(this);
|
||||
result.unionInternal(y);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this interval to the union of the current interval and {@code y}.
|
||||
*
|
||||
* <p>Package private since only S2 classes are intended to mutate S1Intervals for now.
|
||||
*/
|
||||
void unionInternal(S1Interval y) {
|
||||
// The y.isFull() case is handled correctly in all cases by the code
|
||||
// below, but can follow three separate code paths depending on whether
|
||||
// this interval is inverted, is non-inverted but contains Pi, or neither.
|
||||
if (!y.isEmpty()) {
|
||||
if (fastContains(y.lo)) {
|
||||
if (fastContains(y.hi)) {
|
||||
// Either this interval contains y, or the union of the two
|
||||
// intervals is the full interval.
|
||||
if (!contains(y)) {
|
||||
setFull();
|
||||
}
|
||||
} else {
|
||||
hi = y.hi;
|
||||
}
|
||||
} else if (fastContains(y.hi)) {
|
||||
lo = y.lo;
|
||||
} else if (isEmpty() || y.fastContains(lo)) {
|
||||
// This interval contains neither endpoint of y. This means that either y
|
||||
// contains all of this interval, or the two intervals are disjoint.
|
||||
lo = y.lo;
|
||||
hi = y.hi;
|
||||
} else {
|
||||
// Check which pair of endpoints are closer together.
|
||||
double dlo = positiveDistance(y.hi, lo);
|
||||
double dhi = positiveDistance(hi, y.lo);
|
||||
if (dlo < dhi) {
|
||||
lo = y.lo;
|
||||
} else {
|
||||
hi = y.hi;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the given endpoint in this interval, which must be 0 for the low end, or 1
|
||||
* for the high end.
|
||||
*/
|
||||
public double get(int endpoint) {
|
||||
if (endpoint < 0 || endpoint > 1) {
|
||||
throw new ArrayIndexOutOfBoundsException();
|
||||
}
|
||||
return endpoint == 0 ? lo : hi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the smallest interval that contains the intersection of this interval with {@code y}.
|
||||
* Note that the region of intersection may consist of two disjoint intervals.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Interval intersection(final S1Interval y) {
|
||||
S1Interval result = new S1Interval(this);
|
||||
result.intersectionInternal(y);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this interval to the intersection of the current interval and {@code y}.
|
||||
*
|
||||
* <p>Package private since only S2 classes are intended to mutate S1Intervals for now.
|
||||
*/
|
||||
void intersectionInternal(final S1Interval y) {
|
||||
// The y.isFull() case is handled correctly in all cases by the code below, but can follow three
|
||||
// separate code paths depending on whether this interval is inverted, is non-inverted but
|
||||
// contains Pi, or neither.
|
||||
|
||||
if (y.isEmpty()) {
|
||||
this.setEmpty();
|
||||
} else if (fastContains(y.lo)) {
|
||||
if (fastContains(y.hi)) {
|
||||
// Either this interval contains y, or the region of intersection consists of two disjoint
|
||||
// subintervals. In either case, we want to set the interval to the shorter of the two
|
||||
// original intervals.
|
||||
if (y.getLength() < getLength()) {
|
||||
this.set(y.lo, y.hi, true); // isFull() code path
|
||||
}
|
||||
} else {
|
||||
this.set(y.lo, hi, true);
|
||||
}
|
||||
} else if (fastContains(y.hi)) {
|
||||
this.set(lo, y.hi, true);
|
||||
} else {
|
||||
// This interval contains neither endpoint of y. This means that either y
|
||||
// contains all of this interval, or the two intervals are disjoint.
|
||||
if (!y.fastContains(lo)) {
|
||||
// assert (!intersects(y));
|
||||
this.setEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this interval can be transformed into the interval {@code y} by moving each
|
||||
* endpoint by at most "maxError" (and without the endpoints crossing, which would invert the
|
||||
* interval). Empty and full intervals are considered to start at an arbitrary point on the unit
|
||||
* circle, thus any interval with (length <= 2*maxError) matches the empty interval, and any
|
||||
* interval with (length >= 2*Pi - 2*maxError) matches the full interval.
|
||||
*/
|
||||
public boolean approxEquals(S1Interval y, double maxError) {
|
||||
// Full and empty intervals require special cases because the "endpoints"
|
||||
// are considered to be positioned arbitrarily.
|
||||
if (isEmpty()) {
|
||||
return y.getLength() <= 2 * maxError;
|
||||
}
|
||||
if (y.isEmpty()) {
|
||||
return getLength() <= 2 * maxError;
|
||||
}
|
||||
if (isFull()) {
|
||||
return y.getLength() >= 2 * (S2.M_PI - maxError);
|
||||
}
|
||||
if (y.isFull()) {
|
||||
return getLength() >= 2 * (S2.M_PI - maxError);
|
||||
}
|
||||
|
||||
// The purpose of the last test below is to verify that moving the endpoints
|
||||
// does not invert the interval, e.g. [-1e20, 1e20] vs. [1e20, -1e20].
|
||||
return (Math.abs(Platform.IEEEremainder(y.lo - lo, 2 * S2.M_PI)) <= maxError
|
||||
&& Math.abs(Platform.IEEEremainder(y.hi - hi, 2 * S2.M_PI)) <= maxError
|
||||
&& Math.abs(getLength() - y.getLength()) <= 2 * maxError);
|
||||
}
|
||||
|
||||
/** As {@link #approxEquals(S1Interval, double)}, with a default maxError of 1e-15. */
|
||||
public boolean approxEquals(final S1Interval y) {
|
||||
return approxEquals(y, 1e-15);
|
||||
}
|
||||
|
||||
/** Returns true if two intervals contains the same set of points. */
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (that instanceof S1Interval) {
|
||||
S1Interval thatInterval = (S1Interval) that;
|
||||
return lo == thatInterval.lo && hi == thatInterval.hi;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
long value = 17;
|
||||
value = 37 * value + Double.doubleToLongBits(lo);
|
||||
value = 37 * value + Double.doubleToLongBits(hi);
|
||||
return (int) ((value >>> 32) ^ value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + this.lo + ", " + this.hi + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the distance from {@code a} to {@code b} in the range [0, 2*Pi). This is equivalent to
|
||||
* {@code drem(b - a - S2.M_PI, 2 * S2.M_PI) + S2.M_PI}, except that it is more numerically stable
|
||||
* (it does not lose precision for very small positive distances).
|
||||
*/
|
||||
public static double positiveDistance(double a, double b) {
|
||||
double d = b - a;
|
||||
if (d >= 0) {
|
||||
return d;
|
||||
}
|
||||
// We want to ensure that if b == Pi and a == (-Pi + eps),
|
||||
// the return result is approximately 2*Pi and not zero.
|
||||
return (b + S2.M_PI) - (a - S2.M_PI);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
|
||||
import androidx.core.util.Preconditions;
|
||||
|
||||
public final strictfp class S2 {
|
||||
// Declare some frequently used constants
|
||||
public static final double M_PI = Math.PI;
|
||||
public static final double M_1_PI = 1.0 / Math.PI;
|
||||
public static final double M_PI_2 = Math.PI / 2.0;
|
||||
public static final double M_PI_4 = Math.PI / 4.0;
|
||||
/** Inverse of the root of 2. */
|
||||
public static final double M_SQRT1_2 = 1 / Math.sqrt(2);
|
||||
|
||||
public static final double M_SQRT2 = Math.sqrt(2);
|
||||
public static final double M_E = Math.E;
|
||||
|
||||
/** The smallest floating-point value {@code x} such that {@code (1 + x != 1)}. */
|
||||
public static final double DBL_EPSILON;
|
||||
|
||||
static {
|
||||
double machEps = 1.0d;
|
||||
do {
|
||||
machEps /= 2.0f;
|
||||
} while ((1.0 + (machEps / 2.0)) != 1.0);
|
||||
DBL_EPSILON = machEps;
|
||||
}
|
||||
|
||||
// This point is about 66km from the north pole towards the East Siberian Sea. See the unit test
|
||||
// for more details. It is written here using constant components to avoid computational errors
|
||||
// from producting a different value than other implementations of S2.
|
||||
private static final S2Point ORIGIN =
|
||||
new S2Point(-0.0099994664350250197, 0.0025924542609324121, 0.99994664350250195);
|
||||
|
||||
// Together these flags define a cell orientation. If SWAP_MASK
|
||||
// is true, then canonical traversal order is flipped around the
|
||||
// diagonal (i.e. i and j are swapped with each other). If
|
||||
// INVERT_MASK is true, then the traversal order is rotated by 180
|
||||
// degrees (i.e. the bits of i and j are inverted, or equivalently,
|
||||
// the axis directions are reversed).
|
||||
public static final int SWAP_MASK = 0x01;
|
||||
public static final int INVERT_MASK = 0x02;
|
||||
|
||||
/** Mapping Hilbert traversal order to orientation adjustment mask. */
|
||||
private static final int[] posToOrientation = {SWAP_MASK, 0, 0, INVERT_MASK + SWAP_MASK};
|
||||
|
||||
/**
|
||||
* Returns an XOR bit mask indicating how the orientation of a child subcell is related to the
|
||||
* orientation of its parent cell. The returned value can be XOR'd with the parent cell's
|
||||
* orientation to give the orientation of the child cell.
|
||||
*
|
||||
* @param position the position of the subcell in the Hilbert traversal, in the range [0,3].
|
||||
* @return a bit mask containing some combination of {@link #SWAP_MASK} and {@link #INVERT_MASK}.
|
||||
* @throws IllegalArgumentException if position is out of bounds.
|
||||
*/
|
||||
public static int posToOrientation(int position) {
|
||||
Preconditions.checkArgument(0 <= position && position < 4);
|
||||
return posToOrientation[position];
|
||||
}
|
||||
|
||||
/** Mapping from cell orientation + Hilbert traversal to IJ-index. */
|
||||
private static final int[][] posToIj = {
|
||||
// 0 1 2 3
|
||||
{0, 1, 3, 2}, // canonical order: (0,0), (0,1), (1,1), (1,0)
|
||||
{0, 2, 3, 1}, // axes swapped: (0,0), (1,0), (1,1), (0,1)
|
||||
{3, 2, 0, 1}, // bits inverted: (1,1), (1,0), (0,0), (0,1)
|
||||
{3, 1, 0, 2}, // swapped & inverted: (1,1), (0,1), (0,0), (1,0)
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the IJ-index of the subcell at the given position in the Hilbert curve traversal with
|
||||
* the given orientation. This is the inverse of {@link #ijToPos}.
|
||||
*
|
||||
* @param orientation the subcell orientation, in the range [0,3].
|
||||
* @param position the position of the subcell in the Hilbert traversal, in the range [0,3].
|
||||
* @return the IJ-index where {@code 0->(0,0), 1->(0,1), 2->(1,0), 3->(1,1)}.
|
||||
* @throws IllegalArgumentException if either parameter is out of bounds.
|
||||
*/
|
||||
public static int posToIJ(int orientation, int position) {
|
||||
return posToIj[orientation][position];
|
||||
}
|
||||
|
||||
/** Mapping from Hilbert traversal order + cell orientation to IJ-index. */
|
||||
private static final int[][] IJ_TO_POS = {
|
||||
// (0,0) (0,1) (1,0) (1,1)
|
||||
{0, 1, 3, 2}, // canonical order
|
||||
{0, 3, 1, 2}, // axes swapped
|
||||
{2, 3, 1, 0}, // bits inverted
|
||||
{2, 1, 3, 0}, // swapped & inverted
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the order in which a specified subcell is visited by the Hilbert curve. This is the
|
||||
* inverse of {@link #posToIJ}.
|
||||
*
|
||||
* @param orientation the subcell orientation, in the range [0,3].
|
||||
* @param ijIndex the subcell index where {@code 0->(0,0), 1->(0,1), 2->(1,0), 3->(1,1)}.
|
||||
* @return the position of the subcell in the Hilbert traversal, in the range [0,3].
|
||||
* @throws IllegalArgumentException if either parameter is out of bounds.
|
||||
*/
|
||||
public static final int ijToPos(int orientation, int ijIndex) {
|
||||
return IJ_TO_POS[orientation][ijIndex];
|
||||
}
|
||||
|
||||
/** Defines an area or a length cell metric. */
|
||||
public static final class Metric {
|
||||
// NOTE: This isn't GWT serializable because writing custom field serializers for inner classes
|
||||
// is hard.
|
||||
|
||||
private final double deriv;
|
||||
private final int dim;
|
||||
|
||||
/** Defines a cell metric of the given dimension (1 == length, 2 == area). */
|
||||
public Metric(int dim, double deriv) {
|
||||
this.deriv = deriv;
|
||||
this.dim = dim;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "deriv" value of a metric is a derivative, and must be multiplied by a length or area in
|
||||
* (s,t)-space to get a useful value.
|
||||
*/
|
||||
public double deriv() {
|
||||
return deriv;
|
||||
}
|
||||
|
||||
/** Return the value of a metric for cells at the given level. */
|
||||
public double getValue(int level) {
|
||||
return Math.scalb(deriv, -dim * level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the level at which the metric has approximately the given value. For example,
|
||||
* S2::kAvgEdge.GetClosestLevel(0.1) returns the level at which the average cell edge length is
|
||||
* approximately 0.1. The return value is always a valid level.
|
||||
*/
|
||||
public int getClosestLevel(double value) {
|
||||
return getMinLevel((dim == 1 ? S2.M_SQRT2 : 2) * value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum level such that the metric is at most the given value, or
|
||||
* S2CellId::kMaxLevel if there is no such level. For example, S2::kMaxDiag.GetMinLevel(0.1)
|
||||
* returns the minimum level such that all cell diagonal lengths are 0.1 or smaller. The return
|
||||
* value is always a valid level.
|
||||
*/
|
||||
public int getMinLevel(double value) {
|
||||
if (value <= 0) {
|
||||
return S2CellId.MAX_LEVEL;
|
||||
}
|
||||
|
||||
// This code is equivalent to computing a floating-point "level"
|
||||
// value and rounding up. The getExponent() method returns the
|
||||
// exponent corresponding to a fraction in the range [1,2).
|
||||
int exponent = Platform.getExponent(value / deriv);
|
||||
int level = Math.max(0, Math.min(S2CellId.MAX_LEVEL, -(exponent >> (dim - 1))));
|
||||
// assert (level == S2CellId.MAX_LEVEL || getValue(level) <= value);
|
||||
// assert (level == 0 || getValue(level - 1) > value);
|
||||
return level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum level such that the metric is at least the given value, or zero if there
|
||||
* is no such level. For example, S2.kMinWidth.GetMaxLevel(0.1) returns the maximum level such
|
||||
* that all cells have a minimum width of 0.1 or larger. The return value is always a valid
|
||||
* level.
|
||||
*/
|
||||
public int getMaxLevel(double value) {
|
||||
if (value <= 0) {
|
||||
return S2CellId.MAX_LEVEL;
|
||||
}
|
||||
|
||||
// This code is equivalent to computing a floating-point "level"
|
||||
// value and rounding down.
|
||||
int exponent = Platform.getExponent(deriv / value);
|
||||
int level = Math.max(0, Math.min(S2CellId.MAX_LEVEL, exponent >> (dim - 1)));
|
||||
// assert (level == 0 || getValue(level) >= value);
|
||||
// assert (level == S2CellId.MAX_LEVEL || getValue(level + 1) < value);
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique "origin" on the sphere for operations that need a fixed reference point. It
|
||||
* should *not* be a point that is commonly used in edge tests in order to avoid triggering code
|
||||
* to handle degenerate cases. (This rules out the north and south poles.)
|
||||
*/
|
||||
public static S2Point origin() {
|
||||
return ORIGIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given point is approximately unit length (this is mainly useful for
|
||||
* assertions).
|
||||
*/
|
||||
public static boolean isUnitLength(S2Point p) {
|
||||
// Normalize() is guaranteed to return a vector whose L2-norm differs from 1
|
||||
// by less than 2 * DBL_EPSILON. Thus the squared L2-norm differs by less
|
||||
// than 4 * DBL_EPSILON. The actual calculated Norm2() can have up to 1.5 *
|
||||
// DBL_EPSILON of additional error. The total error of 5.5 * DBL_EPSILON
|
||||
// can then be rounded down since the result must be a representable
|
||||
// double-precision value.
|
||||
return Math.abs(p.norm2() - 1) <= 5 * DBL_EPSILON; // About 1.11e-15
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if edge AB crosses CD at a point that is interior to both edges. Properties:
|
||||
*
|
||||
* <p>(1) SimpleCrossing(b,a,c,d) == SimpleCrossing(a,b,c,d) (2) SimpleCrossing(c,d,a,b) ==
|
||||
* SimpleCrossing(a,b,c,d)
|
||||
*/
|
||||
public static boolean simpleCrossing(S2Point a, S2Point b, S2Point c, S2Point d) {
|
||||
// We compute SimpleCCW() for triangles ACB, CBD, BDA, and DAC. All
|
||||
// of these triangles need to have the same orientation (CW or CCW)
|
||||
// for an intersection to exist. Note that this is slightly more
|
||||
// restrictive than the corresponding definition for planar edges,
|
||||
// since we need to exclude pairs of line segments that would
|
||||
// otherwise "intersect" by crossing two antipodal points.
|
||||
|
||||
S2Point ab = S2Point.crossProd(a, b);
|
||||
S2Point cd = S2Point.crossProd(c, d);
|
||||
double acb = -ab.dotProd(c);
|
||||
double cbd = -cd.dotProd(b);
|
||||
double bda = ab.dotProd(d);
|
||||
double dac = cd.dotProd(a);
|
||||
|
||||
return (acb * cbd > 0) && (cbd * bda > 0) && (bda * dac > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a vector "c" that is orthogonal to the given unit-length vectors "a" and "b". This
|
||||
* function is similar to a.CrossProd(b) except that it does a better job of ensuring
|
||||
* orthogonality when "a" is nearly parallel to "b", and it returns a non-zero result even when a
|
||||
* == b or a == -b.
|
||||
*
|
||||
* <p>It satisfies the following properties (RCP == robustCrossProd):
|
||||
*
|
||||
* <p>(1) RCP(a,b) != 0 for all a, b (2) RCP(b,a) == -RCP(a,b) unless a == b or a == -b (3)
|
||||
* RCP(-a,b) == -RCP(a,b) unless a == b or a == -b (4) RCP(a,-b) == -RCP(a,b) unless a == b or a
|
||||
* == -b
|
||||
*/
|
||||
public static S2Point robustCrossProd(S2Point a, S2Point b) {
|
||||
// The direction of a.CrossProd(b) becomes unstable as (a + b) or (a - b)
|
||||
// approaches zero. This leads to situations where a.CrossProd(b) is not
|
||||
// very orthogonal to "a" and/or "b". We could fix this using Gram-Schmidt,
|
||||
// but we also want b.robustCrossProd(a) == -a.robustCrossProd(b).
|
||||
//
|
||||
// The easiest fix is to just compute the cross product of (b+a) and (b-a).
|
||||
// Mathematically, this cross product is exactly twice the cross product of
|
||||
// "a" and "b", but it has the numerical advantage that (b+a) and (b-a)
|
||||
// are always perpendicular (since "a" and "b" are unit length). This
|
||||
// yields a result that is nearly orthogonal to both "a" and "b" even if
|
||||
// these two values differ only in the lowest bit of one component.
|
||||
// assert (isUnitLength(a) && isUnitLength(b));
|
||||
S2Point x = S2Point.crossProd(S2Point.add(b, a), S2Point.sub(b, a));
|
||||
if (!x.equalsPoint(S2Point.ORIGIN)) {
|
||||
return x;
|
||||
}
|
||||
|
||||
// The only result that makes sense mathematically is to return zero, but
|
||||
// we find it more convenient to return an arbitrary orthogonal vector.
|
||||
return ortho(a);
|
||||
}
|
||||
|
||||
private static final S2Point[] ORTHO_BASES = {
|
||||
new S2Point(1, 0.0053, 0.00457), new S2Point(0.012, 1, 0.00457), new S2Point(0.012, 0.0053, 1)
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a unit-length vector that is orthogonal to {@code a}. Satisfies {@code ortho(-a) =
|
||||
* -ortho(a)} for all {@code a}.
|
||||
*/
|
||||
public static S2Point ortho(S2Point a) {
|
||||
int k = a.largestAbsComponent() - 1;
|
||||
if (k < 0) {
|
||||
k = 2;
|
||||
}
|
||||
return S2Point.normalize(S2Point.crossProd(a, ORTHO_BASES[k]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the area of triangle ABC. This method combines two different algorithms to get accurate
|
||||
* results for both large and small triangles. The maximum error is about 5e-15 (about 0.25 square
|
||||
* meters on the Earth's surface), the same as girardArea() below, but unlike that method it is
|
||||
* also accurate for small triangles. Example: when the true area is 100 square meters, area()
|
||||
* yields an error about 1 trillion times smaller than girardArea().
|
||||
*
|
||||
* <p>All points should be unit length, and no two points should be antipodal. The area is always
|
||||
* positive.
|
||||
*/
|
||||
public static double area(S2Point a, S2Point b, S2Point c) {
|
||||
// assert isUnitLength(a) && isUnitLength(b) && isUnitLength(c);
|
||||
|
||||
// This method is based on l'Huilier's theorem,
|
||||
//
|
||||
// tan(E/4) = sqrt(tan(s/2) tan((s-a)/2) tan((s-b)/2) tan((s-c)/2))
|
||||
//
|
||||
// where E is the spherical excess of the triangle (i.e. its area),
|
||||
// a, b, c, are the side lengths, and
|
||||
// s is the semiperimeter (a + b + c) / 2 .
|
||||
//
|
||||
// The only significant source of error using l'Huilier's method is the
|
||||
// cancellation error of the terms (s-a), (s-b), (s-c). This leads to a
|
||||
// *relative* error of about 1e-16 * s / min(s-a, s-b, s-c). This compares
|
||||
// to a relative error of about 1e-15 / E using Girard's formula, where E is
|
||||
// the true area of the triangle. Girard's formula can be even worse than
|
||||
// this for very small triangles, e.g. a triangle with a true area of 1e-30
|
||||
// might evaluate to 1e-5.
|
||||
//
|
||||
// So, we prefer l'Huilier's formula unless dmin < s * (0.1 * E), where
|
||||
// dmin = min(s-a, s-b, s-c). This basically includes all triangles
|
||||
// except for extremely long and skinny ones.
|
||||
//
|
||||
// Since we don't know E, we would like a conservative upper bound on
|
||||
// the triangle area in terms of s and dmin. It's possible to show that
|
||||
// E <= k1 * s * sqrt(s * dmin), where k1 = 2*sqrt(3)/Pi (about 1).
|
||||
// Using this, it's easy to show that we should always use l'Huilier's
|
||||
// method if dmin >= k2 * s^5, where k2 is about 1e-2. Furthermore,
|
||||
// if dmin < k2 * s^5, the triangle area is at most k3 * s^4, where
|
||||
// k3 is about 0.1. Since the best case error using Girard's formula
|
||||
// is about 1e-15, this means that we shouldn't even consider it unless
|
||||
// s >= 3e-4 or so.
|
||||
|
||||
// We use volatile doubles to force the compiler to truncate all of these
|
||||
// quantities to 64 bits. Otherwise it may compute a value of dmin > 0
|
||||
// simply because it chose to spill one of the intermediate values to
|
||||
// memory but not one of the others.
|
||||
final double sa = b.angle(c);
|
||||
final double sb = c.angle(a);
|
||||
final double sc = a.angle(b);
|
||||
final double s = 0.5 * (sa + sb + sc);
|
||||
if (s >= 3e-4) {
|
||||
// Consider whether Girard's formula might be more accurate.
|
||||
double s2 = s * s;
|
||||
double dmin = s - Math.max(sa, Math.max(sb, sc));
|
||||
if (dmin < 1e-2 * s * s2 * s2) {
|
||||
// This triangle is skinny enough to consider using Girard's formula. We increase the area
|
||||
// by the approximate maximum error in the Girard calculation in order to ensure that this
|
||||
// test is conservative.
|
||||
double area = girardArea(a, b, c);
|
||||
if (dmin < s * (0.1 * (area + 5e-15))) {
|
||||
return area;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use l'Huilier's formula.
|
||||
return 4
|
||||
* Math.atan(
|
||||
Math.sqrt(
|
||||
Math.max(
|
||||
0.0,
|
||||
Math.tan(0.5 * s)
|
||||
* Math.tan(0.5 * (s - sa))
|
||||
* Math.tan(0.5 * (s - sb))
|
||||
* Math.tan(0.5 * (s - sc)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the area of the triangle computed using Girard's formula. All points should be unit
|
||||
* length, and no two points should be antipodal.
|
||||
*
|
||||
* <p>This method is about twice as fast as area() but has poor relative accuracy for small
|
||||
* triangles. The maximum error is about 5e-15 (about 0.25 square meters on the Earth's surface)
|
||||
* and the average error is about 1e-15. These bounds apply to triangles of any size, even as the
|
||||
* maximum edge length of the triangle approaches 180 degrees. But note that for such triangles,
|
||||
* tiny perturbations of the input points can change the true mathematical area dramatically.
|
||||
*/
|
||||
public static double girardArea(S2Point a, S2Point b, S2Point c) {
|
||||
// This is equivalent to the usual Girard's formula but is slightly more
|
||||
// accurate, faster to compute, and handles a == b == c without a special
|
||||
// case. RobustCrossProd() is necessary to get good accuracy when two of the
|
||||
// input points are very close together.
|
||||
S2Point ab = S2.robustCrossProd(a, b);
|
||||
S2Point bc = S2.robustCrossProd(b, c);
|
||||
S2Point ac = S2.robustCrossProd(a, c);
|
||||
return Math.max(0.0, ab.angle(ac) - ab.angle(bc) + bc.angle(ac));
|
||||
}
|
||||
|
||||
/**
|
||||
* Like area(), but returns a positive value for counterclockwise triangles and a negative value
|
||||
* otherwise.
|
||||
*/
|
||||
public static double signedArea(S2Point a, S2Point b, S2Point c) {
|
||||
return S2Predicates.sign(a, b, c) * area(a, b, c);
|
||||
}
|
||||
|
||||
// About centroids:
|
||||
// ----------------
|
||||
//
|
||||
// There are several notions of the "centroid" of a triangle. First, there
|
||||
// is the planar centroid, which is simply the centroid of the ordinary
|
||||
// (non-spherical) triangle defined by the three vertices. Second, there is
|
||||
// the surface centroid, which is defined as the intersection of the three
|
||||
// medians of the spherical triangle. It is possible to show that this
|
||||
// point is simply the planar centroid projected to the surface of the
|
||||
// sphere. Finally, there is the true centroid (mass centroid), which is
|
||||
// defined as the surface integral over the spherical triangle of (x,y,z)
|
||||
// divided by the triangle area. This is the point that the triangle would
|
||||
// rotate around if it was spinning in empty space.
|
||||
//
|
||||
// The best centroid for most purposes is the true centroid. Unlike the
|
||||
// planar and surface centroids, the true centroid behaves linearly as
|
||||
// regions are added or subtracted. That is, if you split a triangle into
|
||||
// pieces and compute the average of their centroids (weighted by triangle
|
||||
// area), the result equals the centroid of the original triangle. This is
|
||||
// not true of the other centroids.
|
||||
//
|
||||
// Also note that the surface centroid may be nowhere near the intuitive
|
||||
// "center" of a spherical triangle. For example, consider the triangle
|
||||
// with vertices A=(1,eps,0), B=(0,0,1), C=(-1,eps,0) (a quarter-sphere).
|
||||
// The surface centroid of this triangle is at S=(0, 2*eps, 1), which is
|
||||
// within a distance of 2*eps of the vertex B. Note that the median from A
|
||||
// (the segment connecting A to the midpoint of BC) passes through S, since
|
||||
// this is the shortest path connecting the two endpoints. On the other
|
||||
// hand, the true centroid is at M=(0, 0.5, 0.5), which when projected onto
|
||||
// the surface is a much more reasonable interpretation of the "center" of
|
||||
// this triangle.
|
||||
|
||||
/**
|
||||
* Return the centroid of the planar triangle ABC. This can be normalized to unit length to obtain
|
||||
* the "surface centroid" of the corresponding spherical triangle, i.e. the intersection of the
|
||||
* three medians. However, note that for large spherical triangles the surface centroid may be
|
||||
* nowhere near the intuitive "center" (see example above).
|
||||
*/
|
||||
public static S2Point planarCentroid(S2Point a, S2Point b, S2Point c) {
|
||||
return new S2Point((a.x + b.x + c.x) / 3.0, (a.y + b.y + c.y) / 3.0, (a.z + b.z + c.z) / 3.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the true centroid of the spherical geodesic edge AB multiplied by the length of the
|
||||
* edge AB. As with triangles, the true centroid of a collection of edges may be computed simply
|
||||
* by summing the result of this method for each edge.
|
||||
*
|
||||
* <p>Note that the planar centroid of a geodesic edge is simply 0.5 * (a + b), while the surface
|
||||
* centroid is (a + b).normalize(). However neither of these values is appropriate for computing
|
||||
* the centroid of a collection of edges (such as a polyline).
|
||||
*
|
||||
* <p>Also note that the result of this function is defined to be {@link S2Point#ORIGIN} if the
|
||||
* edge is degenerate (and that this is intended behavior).
|
||||
*/
|
||||
public static S2Point trueCentroid(S2Point a, S2Point b) {
|
||||
// The centroid (multiplied by length) is a vector toward the midpoint of the edge, whose length
|
||||
// is twice the sine of half the angle between the two vertices.
|
||||
// Defining theta to be this angle, we have:
|
||||
S2Point vDiff = a.sub(b); // Length == 2*sin(theta)
|
||||
S2Point vSum = a.add(b); // Length == 2*cos(theta)
|
||||
double sin2 = vDiff.norm2();
|
||||
double cos2 = vSum.norm2();
|
||||
if (cos2 == 0) {
|
||||
return S2Point.ORIGIN; // Ignore antipodal edges.
|
||||
}
|
||||
return vSum.mul(Math.sqrt(sin2 / cos2)); // Length == 2*sin(theta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the true centroid of the spherical triangle ABC multiplied by the signed area of
|
||||
* spherical triangle ABC. The reasons for multiplying by the signed area are (1) this is the
|
||||
* quantity that needs to be summed to compute the centroid of a union or difference of triangles,
|
||||
* and (2) it's actually easier to calculate this way.
|
||||
*/
|
||||
public static S2Point trueCentroid(S2Point a, S2Point b, S2Point c) {
|
||||
// I couldn't find any references for computing the true centroid of a
|
||||
// spherical triangle... I have a truly marvellous demonstration of this
|
||||
// formula which this margin is too narrow to contain :)
|
||||
|
||||
// assert (isUnitLength(a) && isUnitLength(b) && isUnitLength(c));
|
||||
|
||||
// Use angle() in order to get accurate results for small triangles.
|
||||
double aAngle = b.angle(c);
|
||||
double bAngle = c.angle(a);
|
||||
double cAngle = a.angle(b);
|
||||
double ra = (aAngle == 0) ? 1 : (aAngle / Math.sin(aAngle));
|
||||
double rb = (bAngle == 0) ? 1 : (bAngle / Math.sin(bAngle));
|
||||
double rc = (cAngle == 0) ? 1 : (cAngle / Math.sin(cAngle));
|
||||
|
||||
// Now compute a point M such that:
|
||||
//
|
||||
// [Ax Ay Az] [Mx] [ra]
|
||||
// [Bx By Bz] [My] = 0.5 * det(A,B,C) * [rb]
|
||||
// [Cx Cy Cz] [Mz] [rc]
|
||||
//
|
||||
// To improve the numerical stability we subtract the first row (A) from the
|
||||
// other two rows; this reduces the cancellation error when A, B, and C are
|
||||
// very close together. Then we solve it using Cramer's rule.
|
||||
//
|
||||
// TODO(user): This code still isn't as numerically stable as it could be.
|
||||
// The biggest potential improvement is to compute B-A and C-A more
|
||||
// accurately so that (B-A)x(C-A) is always inside triangle ABC.
|
||||
S2Point x = new S2Point(a.x, b.x - a.x, c.x - a.x);
|
||||
S2Point y = new S2Point(a.y, b.y - a.y, c.y - a.y);
|
||||
S2Point z = new S2Point(a.z, b.z - a.z, c.z - a.z);
|
||||
S2Point r = new S2Point(ra, rb - ra, rc - ra);
|
||||
return new S2Point(
|
||||
0.5 * S2Point.scalarTripleProduct(r, y, z),
|
||||
0.5 * S2Point.scalarTripleProduct(r, z, x),
|
||||
0.5 * S2Point.scalarTripleProduct(r, x, y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns +1 if the edge AB is CCW around the origin, -1 if its clockwise, and 0 if the result is
|
||||
* indeterminate.
|
||||
*/
|
||||
public static int planarCCW(R2Vector a, R2Vector b) {
|
||||
double sab = (a.dotProd(b) > 0) ? -1 : 1;
|
||||
R2Vector vab = R2Vector.add(a, R2Vector.mul(b, sab));
|
||||
double da = a.norm2();
|
||||
double db = b.norm2();
|
||||
double sign;
|
||||
if (da < db || (da == db && a.lessThan(b))) {
|
||||
sign = a.crossProd(vab) * sab;
|
||||
} else {
|
||||
sign = vab.crossProd(b);
|
||||
}
|
||||
if (sign > 0) {
|
||||
return 1;
|
||||
}
|
||||
if (sign < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int planarOrderedCCW(R2Vector a, R2Vector b, R2Vector c) {
|
||||
int sum = 0;
|
||||
sum += planarCCW(a, b);
|
||||
sum += planarCCW(b, c);
|
||||
sum += planarCCW(c, a);
|
||||
if (sum > 0) {
|
||||
return 1;
|
||||
}
|
||||
if (sum < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the angle at the vertex B in the triangle ABC. The return value is always in the range
|
||||
* [0, Pi]. The points do not need to be normalized. Ensures that Angle(a,b,c) == Angle(c,b,a) for
|
||||
* all a,b,c.
|
||||
*
|
||||
* <p>The angle is undefined if A or C is diametrically opposite from B, and becomes numerically
|
||||
* unstable as the length of edge AB or BC approaches 180 degrees.
|
||||
*/
|
||||
public static double angle(S2Point a, S2Point b, S2Point c) {
|
||||
// robustCrossProd() is necessary to get good accuracy when two of the input
|
||||
// points are very close together.
|
||||
return robustCrossProd(a, b).angle(robustCrossProd(c, b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exterior angle at the vertex B in the triangle ABC. The return value is positive if
|
||||
* ABC is counterclockwise and negative otherwise. If you imagine an ant walking from A to B to C,
|
||||
* this is the angle that the ant turns at vertex B (positive = left, negative = right).
|
||||
*
|
||||
* <p>Ensures that turnAngle(a,b,c) == -turnAngle(c,b,a) for all distinct a,b,c. The result is
|
||||
* undefined if (a == b || b == c), but is either -Pi or Pi if (a == c). All points should be
|
||||
* normalized.
|
||||
*/
|
||||
public static double turnAngle(S2Point a, S2Point b, S2Point c) {
|
||||
// We use robustCrossProd() to get good accuracy when two points are very close together, and
|
||||
// S2Predicates.sign() to ensure that the sign is correct for turns that are close to 180
|
||||
// degrees.
|
||||
//
|
||||
// Unfortunately we can't save robustCrossProd(a, b) and pass it as the optional 4th argument to
|
||||
// S2Predicates.sign(), because it requires a.crossProd(b) exactly (the robust version differs
|
||||
// in magnitude).
|
||||
double angle = robustCrossProd(a, b).angle(robustCrossProd(b, c));
|
||||
|
||||
// Don't return S2Predicates.sign() * angle because it is legal to have (a == c).
|
||||
return (S2Predicates.sign(a, b, c) > 0) ? angle : -angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum error in {@link #turnAngle}. The returned value is proportional to the
|
||||
* number of vertices and the machine epsilon.
|
||||
*/
|
||||
public static double getTurningAngleMaxError(int numVertices) {
|
||||
// The maximum error can be bounded as follows:
|
||||
// 2.24 * DBL_EPSILON for robustCrossProd(b, a)
|
||||
// 2.24 * DBL_EPSILON for robustCrossProd(c, b)
|
||||
// 3.25 * DBL_EPSILON for angle()
|
||||
// 2.00 * DBL_EPSILON for each addition in the Kahan summation
|
||||
// ------------------
|
||||
// 9.73 * DBL_EPSILON
|
||||
final double maxErrorPerVertex = 9.73 * S2.DBL_EPSILON;
|
||||
return maxErrorPerVertex * numVertices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a right-handed coordinate frame (three orthonormal vectors) based on a single point,
|
||||
* which will become the third axis.
|
||||
*/
|
||||
public static Matrix3x3 getFrame(S2Point p0) {
|
||||
S2Point p1 = ortho(p0);
|
||||
S2Point p2 = S2Point.normalize(S2Point.crossProd(p1, p0));
|
||||
return Matrix3x3.fromCols(p2, p1, p0);
|
||||
}
|
||||
|
||||
/** Returns a normalized copy {@code p} after rotating it by the rotation matrix {@code r}. */
|
||||
static S2Point rotate(S2Point p, Matrix3x3 r) {
|
||||
Matrix3x3 rotated = r.mult(new Matrix3x3(1, p.x, p.y, p.z));
|
||||
return S2Point.normalize(new S2Point(rotated.get(0, 0), rotated.get(1, 0), rotated.get(2, 0)));
|
||||
}
|
||||
|
||||
/** Converts 'p' to the basis given in 'frame'. */
|
||||
static S2Point toFrame(Matrix3x3 frame, S2Point p) {
|
||||
// The inverse of an orthonormal matrix is its transpose.
|
||||
return frame.transpose().mult(Matrix3x3.fromCols(p)).getCol(0);
|
||||
}
|
||||
|
||||
/** Converts 'p' from the basis given in 'frame'. */
|
||||
static S2Point fromFrame(Matrix3x3 frame, S2Point q) {
|
||||
return frame.mult(Matrix3x3.fromCols(q)).getCol(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if two points are within the given distance of each other (mainly useful for
|
||||
* testing).
|
||||
*/
|
||||
public static boolean approxEquals(S2Point a, S2Point b, double maxError) {
|
||||
return a.angle(b) <= maxError;
|
||||
}
|
||||
|
||||
public static boolean approxEquals(S2Point a, S2Point b) {
|
||||
return approxEquals(a, b, 1e-15);
|
||||
}
|
||||
|
||||
public static boolean approxEquals(double a, double b, double maxError) {
|
||||
return Math.abs(a - b) <= maxError;
|
||||
}
|
||||
|
||||
public static boolean approxEquals(double a, double b) {
|
||||
return approxEquals(a, b, 1e-15);
|
||||
}
|
||||
|
||||
// Don't instantiate
|
||||
private S2() {}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.base.Objects;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The area of an interior, i.e. the region on the left side of an odd number of loops and
|
||||
* optionally a centroid. The area is between 0 and 4*Pi. If it has a centroid, it is the true
|
||||
* centroid of the interior multiplied by the area of the shape. Note that the centroid may not be
|
||||
* contained by the shape.
|
||||
*
|
||||
* @author dbentley@google.com (Daniel Bentley)
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final class S2AreaCentroid implements Serializable {
|
||||
|
||||
private final double area;
|
||||
private final S2Point centroid;
|
||||
|
||||
public S2AreaCentroid(double area, @Nullable S2Point centroid) {
|
||||
this.area = area;
|
||||
this.centroid = centroid;
|
||||
}
|
||||
|
||||
public double getArea() {
|
||||
return area;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public S2Point getCentroid() {
|
||||
return centroid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof S2AreaCentroid) {
|
||||
S2AreaCentroid that = (S2AreaCentroid) obj;
|
||||
return this.area == that.area && Objects.equal(this.centroid, that.centroid);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(area, centroid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
|
||||
/**
|
||||
* S2Cap represents a disc-shaped region defined by a center and radius. Technically this shape is
|
||||
* called a "spherical cap" (rather than disc) because it is not planar; the cap represents a
|
||||
* portion of the sphere that has been cut off by a plane. The boundary of the cap is the circle
|
||||
* defined by the intersection of the sphere and the plane. For containment purposes, the cap is a
|
||||
* closed set, i.e. it contains its boundary.
|
||||
*
|
||||
* <p>For the most part, you can use a spherical cap wherever you would use a disc in planar
|
||||
* geometry. The radius of the cap is measured along the surface of the sphere (rather than the
|
||||
* straight-line distance through the interior). Thus a cap of radius Pi/2 is a hemisphere, and a
|
||||
* cap of radius Pi covers the entire sphere.
|
||||
*
|
||||
* <p>A cap can also be defined by its center point and height. The height is simply the distance
|
||||
* from the center point to the cutoff plane. There is also support for "empty" and "full" caps,
|
||||
* which contain no points and all points respectively.
|
||||
*
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class S2Cap implements S2Region, Serializable {
|
||||
private final S2Point axis;
|
||||
private final S1ChordAngle radius;
|
||||
|
||||
// Caps may be constructed from either an axis and a height, or an axis and
|
||||
// an angle. To avoid ambiguity, there are no public constructors
|
||||
|
||||
private S2Cap(S2Point axis, S1ChordAngle radius) {
|
||||
this.axis = axis;
|
||||
this.radius = radius;
|
||||
// assert (isValid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cap where the radius is expressed as an S1ChordAngle. This constructor is more
|
||||
* efficient than {@link #fromAxisAngle(S2Point, S1Angle)}.
|
||||
*/
|
||||
public static S2Cap fromAxisChord(S2Point center, S1ChordAngle radius) {
|
||||
return new S2Cap(center, radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cap given its axis and the cap height, i.e. the maximum projected distance along the
|
||||
* cap axis from the cap center. 'axis' should be a unit-length vector.
|
||||
*/
|
||||
public static S2Cap fromAxisHeight(S2Point axis, double height) {
|
||||
// assert (S2.isUnitLength(axis));
|
||||
return new S2Cap(axis, S1ChordAngle.fromLength2(2 * height));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cap given its axis and the cap opening angle, i.e. maximum angle between the axis and
|
||||
* a point on the cap. 'axis' should be a unit-length vector, and 'angle' should be between 0 and
|
||||
* 180 degrees.
|
||||
*/
|
||||
public static S2Cap fromAxisAngle(S2Point axis, S1Angle angle) {
|
||||
// The "min" calculation below is necessary to handle S1Angle.INFINITY.
|
||||
// assert (S2.isUnitLength(axis));
|
||||
return fromAxisChord(
|
||||
axis, S1ChordAngle.fromS1Angle(S1Angle.radians(Math.min(angle.radians(), S2.M_PI))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cap given its axis and its area in steradians. 'axis' should be a unit-length vector,
|
||||
* and 'area' should be between 0 and 4 * M_PI.
|
||||
*/
|
||||
public static S2Cap fromAxisArea(S2Point axis, double area) {
|
||||
// assert (S2.isUnitLength(axis));
|
||||
return new S2Cap(axis, S1ChordAngle.fromLength2(area / S2.M_PI));
|
||||
}
|
||||
|
||||
/** Return an empty cap, i.e. a cap that contains no points. */
|
||||
public static S2Cap empty() {
|
||||
return new S2Cap(S2Point.X_POS, S1ChordAngle.NEGATIVE);
|
||||
}
|
||||
|
||||
/** Return a full cap, i.e. a cap that contains all points. */
|
||||
public static S2Cap full() {
|
||||
return new S2Cap(S2Point.X_POS, S1ChordAngle.STRAIGHT);
|
||||
}
|
||||
|
||||
// Accessor methods.
|
||||
public S2Point axis() {
|
||||
return axis;
|
||||
}
|
||||
|
||||
public S1ChordAngle radius() {
|
||||
return radius;
|
||||
}
|
||||
|
||||
/** Returns the height of the cap, i.e. the distance from the center point to the cutoff plane. */
|
||||
public double height() {
|
||||
return 0.5 * radius.getLength2();
|
||||
}
|
||||
|
||||
public double area() {
|
||||
return 2 * S2.M_PI * Math.max(0.0, height());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cap radius as an S1Angle. Since the cap angle is stored internally as an
|
||||
* S1ChordAngle, this method requires a trigonometric operation and may yield a slightly different
|
||||
* result than the value passed to {@link #fromAxisAngle(S2Point, S1Angle)}.
|
||||
*/
|
||||
public S1Angle angle() {
|
||||
return radius.toAngle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the axis is {@link S2#isUnitLength unit length}, and the angle is less than Pi.
|
||||
*
|
||||
* <p>Negative angles or heights are valid, and represent empty caps.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return S2.isUnitLength(axis) && radius.getLength2() <= 4;
|
||||
}
|
||||
|
||||
/** Return true if the cap is empty, i.e. it contains no points. */
|
||||
public boolean isEmpty() {
|
||||
return radius.isNegative();
|
||||
}
|
||||
|
||||
/** Return true if the cap is full, i.e. it contains all points. */
|
||||
public boolean isFull() {
|
||||
return S1ChordAngle.STRAIGHT.equals(radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the complement of the interior of the cap. A cap and its complement have the same
|
||||
* boundary but do not share any interior points. The complement operator is not a bijection,
|
||||
* since the complement of a singleton cap (containing a single point) is the same as the
|
||||
* complement of an empty cap.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S2Cap complement() {
|
||||
// The complement of a full cap is an empty cap, not a singleton.
|
||||
// Also make sure that the complement of an empty cap is full.
|
||||
if (isFull()) {
|
||||
return empty();
|
||||
}
|
||||
if (isEmpty()) {
|
||||
return full();
|
||||
}
|
||||
return fromAxisChord(S2Point.neg(axis), S1ChordAngle.fromLength2(4 - radius.getLength2()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if and only if this cap contains the given other cap (in a set containment sense,
|
||||
* e.g. every cap contains the empty cap).
|
||||
*/
|
||||
public boolean contains(S2Cap other) {
|
||||
if (isFull() || other.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
S1ChordAngle axialDistance = new S1ChordAngle(axis, other.axis);
|
||||
return radius.compareTo(S1ChordAngle.add(axialDistance, other.radius)) >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if and only if the interior of this cap intersects the given other cap. (This
|
||||
* relationship is not symmetric, since only the interior of this cap is used.)
|
||||
*/
|
||||
public boolean interiorIntersects(S2Cap other) {
|
||||
// Interior(X) intersects Y if and only if Complement(Interior(X))
|
||||
// does not contain Y.
|
||||
return !complement().contains(other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if and only if the given point is contained in the interior of the region (i.e. the
|
||||
* region excluding its boundary). 'p' should be a unit-length vector.
|
||||
*/
|
||||
public boolean interiorContains(S2Point p) {
|
||||
// assert (S2.isUnitLength(p));
|
||||
return isFull() || new S1ChordAngle(axis, p).compareTo(radius) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the cap radius if necessary to include the given point. If the cap is empty the axis
|
||||
* is set to the given point, but otherwise it is left unchanged.
|
||||
*
|
||||
* @param p must be {@link S2#isUnitLength unit length}
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S2Cap addPoint(S2Point p) {
|
||||
// assert (S2.isUnitLength(p));
|
||||
if (isEmpty()) {
|
||||
return new S2Cap(p, S1ChordAngle.ZERO);
|
||||
} else {
|
||||
// After adding p to this cap, we require that the result contains p. However we don't need to
|
||||
// do anything special to achieve this because contains() does exactly the same distance
|
||||
// calculation that we do here.
|
||||
return new S2Cap(
|
||||
axis, S1ChordAngle.fromLength2(Math.max(radius.getLength2(), axis.getDistance2(p))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the cap radius if necessary to include the given cap. If the current cap is empty, it
|
||||
* is set to the given other cap.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S2Cap addCap(S2Cap other) {
|
||||
if (isEmpty()) {
|
||||
return other;
|
||||
} else if (other.isEmpty()) {
|
||||
return this;
|
||||
} else {
|
||||
// We round up the distance to ensure that the cap is actually contained.
|
||||
// TODO(user): Do some error analysis in order to guarantee this.
|
||||
S1ChordAngle dist = S1ChordAngle.add(new S1ChordAngle(axis, other.axis), other.radius);
|
||||
S1ChordAngle roundedUp = dist.plusError(S2.DBL_EPSILON * dist.getLength2());
|
||||
return new S2Cap(axis, S1ChordAngle.max(radius, roundedUp));
|
||||
}
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////
|
||||
// S2Region interface (see {@code S2Region} for details):
|
||||
@Override
|
||||
public S2Cap getCapBound() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S2LatLngRect getRectBound() {
|
||||
if (isEmpty()) {
|
||||
return S2LatLngRect.empty();
|
||||
}
|
||||
if (isFull()) {
|
||||
return S2LatLngRect.full();
|
||||
}
|
||||
|
||||
// Convert the axis to a (lat,lng) pair, and compute the cap angle.
|
||||
S2LatLng axisLatLng = new S2LatLng(axis);
|
||||
double capAngle = angle().radians();
|
||||
|
||||
boolean allLongitudes = false;
|
||||
double[] lat = new double[2];
|
||||
double[] lng = new double[2];
|
||||
lng[0] = -S2.M_PI;
|
||||
lng[1] = S2.M_PI;
|
||||
|
||||
// Check whether cap includes the south pole.
|
||||
lat[0] = axisLatLng.lat().radians() - capAngle;
|
||||
if (lat[0] <= -S2.M_PI_2) {
|
||||
lat[0] = -S2.M_PI_2;
|
||||
allLongitudes = true;
|
||||
}
|
||||
// Check whether cap includes the north pole.
|
||||
lat[1] = axisLatLng.lat().radians() + capAngle;
|
||||
if (lat[1] >= S2.M_PI_2) {
|
||||
lat[1] = S2.M_PI_2;
|
||||
allLongitudes = true;
|
||||
}
|
||||
if (!allLongitudes) {
|
||||
// Compute the range of longitudes covered by the cap. We use the law
|
||||
// of sines for spherical triangles. Consider the triangle ABC where
|
||||
// A is the north pole, B is the center of the cap, and C is the point
|
||||
// of tangency between the cap boundary and a line of longitude. Then
|
||||
// C is a right angle, and letting a,b,c denote the sides opposite A,B,C,
|
||||
// we have sin(a)/sin(A) = sin(c)/sin(C), or sin(A) = sin(a)/sin(c).
|
||||
// Here "a" is the cap angle, and "c" is the colatitude (90 degrees
|
||||
// minus the latitude). This formula also works for negative latitudes.
|
||||
double sinA = S1ChordAngle.sin(radius);
|
||||
double sinC = Math.cos(axisLatLng.lat().radians());
|
||||
if (sinA <= sinC) {
|
||||
double angleA = Math.asin(sinA / sinC);
|
||||
lng[0] = Platform.IEEEremainder(axisLatLng.lng().radians() - angleA, 2 * S2.M_PI);
|
||||
lng[1] = Platform.IEEEremainder(axisLatLng.lng().radians() + angleA, 2 * S2.M_PI);
|
||||
}
|
||||
}
|
||||
return new S2LatLngRect(new R1Interval(lat[0], lat[1]), new S1Interval(lng[0], lng[1]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(S2Cell cell) {
|
||||
// If the cap does not contain all cell vertices, return false.
|
||||
// We check the vertices before taking the Complement() because we can't
|
||||
// accurately represent the complement of a very small cap (a height
|
||||
// of 2-epsilon is rounded off to 2).
|
||||
S2Point[] vertices = new S2Point[4];
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
vertices[k] = cell.getVertex(k);
|
||||
if (!contains(vertices[k])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Otherwise, return true if the complement of the cap does not intersect
|
||||
// the cell. (This test is slightly conservative, because technically we
|
||||
// want Complement().InteriorIntersects() here.)
|
||||
return !complement().intersects(cell, vertices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mayIntersect(S2Cell cell) {
|
||||
// If the cap contains any cell vertex, return true.
|
||||
S2Point[] vertices = new S2Point[4];
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
vertices[k] = cell.getVertex(k);
|
||||
if (contains(vertices[k])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return intersects(cell, vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the cap intersects 'cell', given that the cap vertices have already been
|
||||
* checked.
|
||||
*/
|
||||
public boolean intersects(S2Cell cell, S2Point[] vertices) {
|
||||
// Return true if this cap intersects any point of 'cell' excluding its
|
||||
// vertices (which are assumed to already have been checked).
|
||||
|
||||
// If the cap is a hemisphere or larger, the cell and the complement of the
|
||||
// cap are both convex. Therefore since no vertex of the cell is contained,
|
||||
// no other interior point of the cell is contained either.
|
||||
if (radius.compareTo(S1ChordAngle.RIGHT) >= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to check for empty caps due to the axis check just below.
|
||||
if (isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optimization: return true if the cell contains the cap axis. (This
|
||||
// allows half of the edge checks below to be skipped.)
|
||||
if (cell.contains(axis)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// At this point we know that the cell does not contain the cap axis,
|
||||
// and the cap does not contain any cell vertex. The only way that they
|
||||
// can intersect is if the cap intersects the interior of some edge.
|
||||
|
||||
double sin2Angle = S1ChordAngle.sin2(radius);
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
S2Point edge = cell.getEdgeRaw(k);
|
||||
double dot = axis.dotProd(edge);
|
||||
if (dot > 0) {
|
||||
// The axis is in the interior half-space defined by the edge. We don't
|
||||
// need to consider these edges, since if the cap intersects this edge
|
||||
// then it also intersects the edge on the opposite side of the cell
|
||||
// (because we know the axis is not contained with the cell).
|
||||
continue;
|
||||
}
|
||||
// The Norm2() factor is necessary because "edge" is not normalized.
|
||||
if (dot * dot > sin2Angle * edge.norm2()) {
|
||||
return false; // Entire cap is on the exterior side of this edge.
|
||||
}
|
||||
// Otherwise, the great circle containing this edge intersects
|
||||
// the interior of the cap. We just need to check whether the point
|
||||
// of closest approach occurs between the two edge endpoints.
|
||||
S2Point dir = S2Point.crossProd(edge, axis);
|
||||
if (dir.dotProd(vertices[k]) < 0 && dir.dotProd(vertices[(k + 1) & 3]) > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(S2Point p) {
|
||||
// The point 'p' should be a unit-length vector.
|
||||
// assert (S2.isUnitLength(p));
|
||||
return new S1ChordAngle(axis, p).compareTo(radius) <= 0;
|
||||
}
|
||||
|
||||
/** Return true if two caps are identical. */
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (that instanceof S2Cap) {
|
||||
S2Cap other = (S2Cap) that;
|
||||
return (axis.equalsPoint(other.axis) && radius.equals(other.radius))
|
||||
|| (isEmpty() && other.isEmpty())
|
||||
|| (isFull() && other.isFull());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if (isFull()) {
|
||||
return 17;
|
||||
} else if (isEmpty()) {
|
||||
return 37;
|
||||
} else {
|
||||
return 37 * (17 * 37 + axis.hashCode()) + radius.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////
|
||||
// The following static methods are convenience functions for assertions
|
||||
// and testing purposes only.
|
||||
|
||||
/**
|
||||
* Returns true if the radian angle between axes of this and 'other' is at most 'maxError', and
|
||||
* the chord distance radius between this and 'other' is at most 'maxError'.
|
||||
*/
|
||||
boolean approxEquals(S2Cap other, double maxError) {
|
||||
double r2 = radius.getLength2();
|
||||
double otherR2 = other.radius.getLength2();
|
||||
return (S2.approxEquals(axis, other.axis, maxError) && Math.abs(r2 - otherR2) <= maxError)
|
||||
|| (isEmpty() && otherR2 <= maxError)
|
||||
|| (other.isEmpty() && r2 <= maxError)
|
||||
|| (isFull() && otherR2 >= 2 - maxError)
|
||||
|| (other.isFull() && r2 >= 2 - maxError);
|
||||
}
|
||||
|
||||
boolean approxEquals(S2Cap other) {
|
||||
return approxEquals(other, 1e-14);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[Point = " + axis + " Radius = " + radius + "]";
|
||||
}
|
||||
|
||||
/** Writes this cap to the given output stream. */
|
||||
public void encode(OutputStream os) throws IOException {
|
||||
encode(new LittleEndianOutput(os));
|
||||
}
|
||||
|
||||
/** Writes this cap to the given little endian output stream. */
|
||||
void encode(LittleEndianOutput os) throws IOException {
|
||||
axis.encode(os);
|
||||
os.writeDouble(radius.getLength2());
|
||||
}
|
||||
|
||||
/** Returns a new S2Cap decoded from the given input stream. */
|
||||
public static S2Cap decode(InputStream is) throws IOException {
|
||||
return decode(new LittleEndianInput(is));
|
||||
}
|
||||
|
||||
/** Returns a new S2Cap decoded from the given little endian input stream. */
|
||||
static S2Cap decode(LittleEndianInput is) throws IOException {
|
||||
S2Point axis = S2Point.decode(is);
|
||||
S1ChordAngle chord = S1ChordAngle.fromLength2(is.readDouble());
|
||||
return S2Cap.fromAxisChord(axis, chord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.primitives.Doubles;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* An S2Cell is an S2Region object that represents a cell. Unlike S2CellIds, it supports efficient
|
||||
* containment and intersection tests. However, it is also a more expensive representation.
|
||||
*
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public final strictfp class S2Cell implements S2Region, Serializable {
|
||||
byte face;
|
||||
byte level;
|
||||
byte orientation;
|
||||
S2CellId cellId;
|
||||
double uMin;
|
||||
double uMax;
|
||||
double vMin;
|
||||
double vMax;
|
||||
|
||||
/** Default constructor used only internally. */
|
||||
S2Cell() {}
|
||||
|
||||
/**
|
||||
* An S2Cell always corresponds to a particular S2CellId. The other constructors are just
|
||||
* convenience methods.
|
||||
*/
|
||||
public S2Cell(S2CellId id) {
|
||||
init(id);
|
||||
}
|
||||
|
||||
/** Returns the cell corresponding to the given S2 cube face. */
|
||||
public static S2Cell fromFace(int face) {
|
||||
return new S2Cell(S2CellId.fromFace(face));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cell given its face (range 0..5), Hilbert curve position within that face (an
|
||||
* unsigned integer with {@link S2CellId#POS_BITS} bits), and level (range 0..kMaxLevel). The
|
||||
* given position will be modified to correspond to the Hilbert curve position at the center of
|
||||
* the returned cell. This is a static function rather than a constructor in order to indicate
|
||||
* what the arguments represent.
|
||||
*/
|
||||
public static S2Cell fromFacePosLevel(int face, long pos, int level) {
|
||||
return new S2Cell(S2CellId.fromFacePosLevel(face, pos, level));
|
||||
}
|
||||
|
||||
// Convenience methods.
|
||||
public S2Cell(S2Point p) {
|
||||
init(S2CellId.fromPoint(p));
|
||||
}
|
||||
|
||||
public S2Cell(S2LatLng ll) {
|
||||
init(S2CellId.fromLatLng(ll));
|
||||
}
|
||||
|
||||
public S2CellId id() {
|
||||
return cellId;
|
||||
}
|
||||
|
||||
public int face() {
|
||||
return face;
|
||||
}
|
||||
|
||||
public byte level() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public byte orientation() {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
/** Returns true if this cell is a leaf-cell, i.e. it has no children. */
|
||||
public boolean isLeaf() {
|
||||
return level == S2CellId.MAX_LEVEL;
|
||||
}
|
||||
|
||||
/** As {@link #getVertexRaw(int)}, except the point is normalized to unit length. */
|
||||
public S2Point getVertex(int k) {
|
||||
return S2Point.normalize(getVertexRaw(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the k<sup>th</sup> vertex of the cell (k = 0,1,2,3). Vertices are returned in CCW order
|
||||
* (lower left, lower right, upper right, upper left in the UV plane). The points are not
|
||||
* necessarily unit length.
|
||||
*/
|
||||
public S2Point getVertexRaw(int k) {
|
||||
// Vertices are returned in the order SW, SE, NE, NW.
|
||||
return S2Projections.faceUvToXyz(
|
||||
face, ((k >> 1) ^ (k & 1)) == 0 ? uMin : uMax, (k >> 1) == 0 ? vMin : vMax);
|
||||
}
|
||||
|
||||
/** As {@link #getEdgeRaw(int)}, except the point is normalized to unit length. */
|
||||
public S2Point getEdge(int k) {
|
||||
return S2Point.normalize(getEdgeRaw(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inward-facing normal of the great circle passing through the edge from vertex k to
|
||||
* vertex k+1 (mod 4). The normals returned by getEdgeRaw are not necessarily unit length.
|
||||
*/
|
||||
public S2Point getEdgeRaw(int k) {
|
||||
switch (k) {
|
||||
case 0:
|
||||
return S2Projections.getVNorm(face, vMin); // Bottom
|
||||
case 1:
|
||||
return S2Projections.getUNorm(face, uMax); // Right
|
||||
case 2:
|
||||
return S2Point.neg(S2Projections.getVNorm(face, vMax)); // Top
|
||||
default:
|
||||
return S2Point.neg(S2Projections.getUNorm(face, uMin)); // Left
|
||||
}
|
||||
}
|
||||
|
||||
/** As {@link S2CellId#getSizeIJ(int)}, using the level of this cell. */
|
||||
public int getSizeIJ() {
|
||||
return S2CellId.getSizeIJ(level());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is not a leaf cell, in which case the array, which must contain at least
|
||||
* four non-null cells in indices 0..3, will be set to the four children of this cell in traversal
|
||||
* order. Otherwise, if this is a leaf cell, false is returned without touching the array.
|
||||
*
|
||||
* <p>This method is equivalent to the following:
|
||||
*
|
||||
* <pre>
|
||||
* for (pos=0, id=childBegin(); !id.equals(childEnd()); id = id.next(), ++pos) {
|
||||
* children[i].init(id);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>except that it is more than two times faster.
|
||||
*/
|
||||
public boolean subdivide(S2Cell[] children) {
|
||||
// This function is equivalent to just iterating over the child cell ids
|
||||
// and calling the S2Cell constructor, but it is about 2.5 times faster.
|
||||
|
||||
if (cellId.isLeaf()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create four children with the appropriate bounds.
|
||||
S2CellId id = cellId.childBegin();
|
||||
R2Vector mid = getCenterUV();
|
||||
double uMid = mid.x();
|
||||
double vMid = mid.y();
|
||||
for (int pos = 0; pos < 4; ++pos, id = id.next()) {
|
||||
S2Cell child = children[pos];
|
||||
child.face = face;
|
||||
child.level = (byte) (level + 1);
|
||||
child.orientation = (byte) (orientation ^ S2.posToOrientation(pos));
|
||||
child.cellId = id;
|
||||
// We want to split the cell in half in "u" and "v". To decide which
|
||||
// side to set equal to the midpoint value, we look at cell's (i,j)
|
||||
// position within its parent. The index for "i" is in bit 1 of ij.
|
||||
int ij = S2.posToIJ(orientation, pos);
|
||||
// The dimension 0 index (i/u) is in bit 1 of ij.
|
||||
if ((ij & 0x2) != 0) {
|
||||
child.uMin = uMid;
|
||||
child.uMax = uMax;
|
||||
} else {
|
||||
child.uMin = uMin;
|
||||
child.uMax = uMid;
|
||||
}
|
||||
// The dimension 1 index (j/v) is in bit 0 of ij.
|
||||
if ((ij & 0x1) != 0) {
|
||||
child.vMin = vMid;
|
||||
child.vMax = vMax;
|
||||
} else {
|
||||
child.vMin = vMin;
|
||||
child.vMax = vMid;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the direction vector corresponding to the center in (s,t)-space of the given cell. This
|
||||
* is the point at which the cell is divided into four subcells; it is not necessarily the
|
||||
* centroid of the cell in (u,v)-space or (x,y,z)-space. The point returned by GetCenterRaw is not
|
||||
* necessarily unit length.
|
||||
*/
|
||||
public S2Point getCenter() {
|
||||
return S2Point.normalize(getCenterRaw());
|
||||
}
|
||||
|
||||
public S2Point getCenterRaw() {
|
||||
return cellId.toPointRaw();
|
||||
}
|
||||
|
||||
/** Returns the bounds of this cell in (u,v)-space. */
|
||||
public R2Rect getBoundUV() {
|
||||
R2Rect rect = new R2Rect();
|
||||
setBoundUV(rect);
|
||||
return rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bounds of this cell in (u,v)-space into 'bound'.
|
||||
*
|
||||
* <p>Package private to avoid leaking object mutation outside the api.
|
||||
*/
|
||||
void setBoundUV(R2Rect bound) {
|
||||
bound.x().set(uMin, uMax);
|
||||
bound.y().set(vMin, vMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the center of the cell in (u,v) coordinates (see {@code S2Projections}). Note that the
|
||||
* center of the cell is defined as the point at which it is recursively subdivided into four
|
||||
* children; in general, it is not at the midpoint of the (u,v) rectangle covered by the cell
|
||||
*/
|
||||
public R2Vector getCenterUV() {
|
||||
return cellId.getCenterUV();
|
||||
}
|
||||
|
||||
/** Return the average area in steradians for cells at the given level. */
|
||||
public static double averageArea(int level) {
|
||||
return PROJ.avgArea.getValue(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average area in steradians of cells at this level. This is accurate to within a
|
||||
* factor of 1.7 (for S2_QUADRATIC_PROJECTION) and is extremely cheap to compute.
|
||||
*/
|
||||
public double averageArea() {
|
||||
return averageArea(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the approximate area of this cell in steradians. This method is accurate to within 3%
|
||||
* percent for all cell sizes and accurate to within 0.1% for cells at level 5 or higher (i.e.
|
||||
* 300km square or smaller). It is moderately cheap to compute.
|
||||
*/
|
||||
public double approxArea() {
|
||||
|
||||
// All cells at the first two levels have the same area.
|
||||
if (level < 2) {
|
||||
return averageArea(level);
|
||||
}
|
||||
|
||||
// First, compute the approximate area of the cell when projected
|
||||
// perpendicular to its normal. The cross product of its diagonals gives
|
||||
// the normal, and the length of the normal is twice the projected area.
|
||||
double flatArea =
|
||||
0.5
|
||||
* S2Point.crossProd(
|
||||
S2Point.sub(getVertex(2), getVertex(0)),
|
||||
S2Point.sub(getVertex(3), getVertex(1)))
|
||||
.norm();
|
||||
|
||||
// Now, compensate for the curvature of the cell surface by pretending
|
||||
// that the cell is shaped like a spherical cap. The ratio of the
|
||||
// area of a spherical cap to the area of its projected disc turns out
|
||||
// to be 2 / (1 + sqrt(1 - r*r)) where "r" is the radius of the disc.
|
||||
// For example, when r=0 the ratio is 1, and when r=1 the ratio is 2.
|
||||
// Here we set Pi*r*r == flat_area to find the equivalent disc.
|
||||
return flatArea * 2 / (1 + Math.sqrt(1 - Math.min(S2.M_1_PI * flatArea, 1.0)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the area in steradians of this cell as accurately as possible. This method is more
|
||||
* expensive but it is accurate to 6 digits of precision even for leaf cells (whose area is
|
||||
* approximately 1e-18).
|
||||
*/
|
||||
public double exactArea() {
|
||||
S2Point v0 = getVertex(0);
|
||||
S2Point v1 = getVertex(1);
|
||||
S2Point v2 = getVertex(2);
|
||||
S2Point v3 = getVertex(3);
|
||||
return S2.area(v0, v1, v2) + S2.area(v0, v2, v3);
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////////////////
|
||||
// S2Region interface (see {@code S2Region} for details):
|
||||
|
||||
// NOTE: This should be marked as @Override, but clone() isn't present in GWT's version of
|
||||
// Object, so we can't mark it as such.
|
||||
@SuppressWarnings("MissingOverride")
|
||||
public S2Region clone() {
|
||||
S2Cell clone = new S2Cell();
|
||||
clone.face = this.face;
|
||||
clone.level = this.level;
|
||||
clone.orientation = this.orientation;
|
||||
clone.uMin = this.uMin;
|
||||
clone.uMax = this.uMax;
|
||||
clone.vMin = this.vMin;
|
||||
clone.vMax = this.vMax;
|
||||
return clone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S2Cap getCapBound() {
|
||||
// Use the cell center in (u,v)-space as the cap axis. This vector is
|
||||
// very close to GetCenter() and faster to compute. Neither one of these
|
||||
// vectors yields the bounding cap with minimal surface area, but they
|
||||
// are both pretty close.
|
||||
//
|
||||
// It's possible to show that the two vertices that are furthest from
|
||||
// the (u,v)-origin never determine the maximum cap size (this is a
|
||||
// possible future optimization).
|
||||
|
||||
S2Point center = S2Point.normalize(S2Projections.faceUvToXyz(face, getCenterUV()));
|
||||
S2Cap cap = S2Cap.fromAxisHeight(center, 0);
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
cap = cap.addPoint(getVertex(k));
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 4 cells around the equator extend to +/-45 degrees latitude at the midpoints of their top
|
||||
* and bottom edges. The two cells covering the poles extend down to +/-35.26 degrees at their
|
||||
* vertices. The maximum error in this calculation is 0.5 * DBL_EPSILON.
|
||||
*/
|
||||
private static final double POLE_MIN_LAT = Math.asin(Math.sqrt(1. / 3)) - 0.5 * S2.DBL_EPSILON;
|
||||
|
||||
@Override
|
||||
public S2LatLngRect getRectBound() {
|
||||
if (level > 0) {
|
||||
// Except for cells at level 0, the latitude and longitude extremes are
|
||||
// attained at the vertices. Furthermore, the latitude range is
|
||||
// determined by one pair of diagonally opposite vertices and the
|
||||
// longitude range is determined by the other pair.
|
||||
//
|
||||
// We first determine which corner (i,j) of the cell has the largest
|
||||
// absolute latitude. To maximize latitude, we want to find the point in
|
||||
// the cell that has the largest absolute z-coordinate and the smallest
|
||||
// absolute x- and y-coordinates. To do this we look at each coordinate
|
||||
// (u and v), and determine whether we want to minimize or maximize that
|
||||
// coordinate based on the axis direction and the cell's (u,v) quadrant.
|
||||
double u = uMin + uMax;
|
||||
double v = vMin + vMax;
|
||||
int i = (S2Projections.getUAxis(face).z == 0 ? (u < 0) : (u > 0)) ? 1 : 0;
|
||||
int j = (S2Projections.getVAxis(face).z == 0 ? (v < 0) : (v > 0)) ? 1 : 0;
|
||||
R1Interval lat =
|
||||
R1Interval.fromPointPair(
|
||||
S2LatLng.latitude(getPoint(i, j)).radians(),
|
||||
S2LatLng.latitude(getPoint(1 - i, 1 - j)).radians());
|
||||
S1Interval lng =
|
||||
S1Interval.fromPointPair(
|
||||
S2LatLng.longitude(getPoint(i, 1 - j)).radians(),
|
||||
S2LatLng.longitude(getPoint(1 - i, j)).radians());
|
||||
|
||||
// We grow the bounds slightly to make sure that the bounding rectangle
|
||||
// contains S2LatLng(P) for any point P inside the loop L defined by the
|
||||
// four *normalized* vertices. Note that normalization of a vector can
|
||||
// change its direction by up to 0.5 * DBL_EPSILON radians, and it is not
|
||||
// enough just to add Normalize() calls to the code above because the
|
||||
// latitude/longitude ranges are not necessarily determined by diagonally
|
||||
// opposite vertex pairs after normalization.
|
||||
//
|
||||
// We would like to bound the amount by which the latitude/longitude of a
|
||||
// contained point P can exceed the bounds computed above. In the case of
|
||||
// longitude, the normalization error can change the direction of rounding
|
||||
// leading to a maximum difference in longitude of 2 * DBL_EPSILON. In
|
||||
// the case of latitude, the normalization error can shift the latitude by
|
||||
// up to 0.5 * DBL_EPSILON and the other sources of error can cause the
|
||||
// two latitudes to differ by up to another 1.5 * DBL_EPSILON, which also
|
||||
// leads to a maximum difference of 2 * DBL_EPSILON.
|
||||
return new S2LatLngRect(lat, lng)
|
||||
.expanded(S2LatLng.fromRadians(2 * S2.DBL_EPSILON, 2 * S2.DBL_EPSILON))
|
||||
.polarClosure();
|
||||
}
|
||||
|
||||
// The face centers are the +X, +Y, +Z, -X, -Y, -Z axes in that order.
|
||||
// assert (((face < 3) ? 1 : -1) == S2Projections.getNorm(face).get(face % 3));
|
||||
|
||||
S2LatLngRect bound;
|
||||
switch (face) {
|
||||
case 0:
|
||||
bound =
|
||||
new S2LatLngRect(
|
||||
new R1Interval(-S2.M_PI_4, S2.M_PI_4), new S1Interval(-S2.M_PI_4, S2.M_PI_4));
|
||||
break;
|
||||
case 1:
|
||||
bound =
|
||||
new S2LatLngRect(
|
||||
new R1Interval(-S2.M_PI_4, S2.M_PI_4), new S1Interval(S2.M_PI_4, 3 * S2.M_PI_4));
|
||||
break;
|
||||
case 2:
|
||||
bound = new S2LatLngRect(new R1Interval(POLE_MIN_LAT, S2.M_PI_2), S1Interval.full());
|
||||
break;
|
||||
case 3:
|
||||
bound =
|
||||
new S2LatLngRect(
|
||||
new R1Interval(-S2.M_PI_4, S2.M_PI_4),
|
||||
new S1Interval(3 * S2.M_PI_4, -3 * S2.M_PI_4));
|
||||
break;
|
||||
case 4:
|
||||
bound =
|
||||
new S2LatLngRect(
|
||||
new R1Interval(-S2.M_PI_4, S2.M_PI_4), new S1Interval(-3 * S2.M_PI_4, -S2.M_PI_4));
|
||||
break;
|
||||
default:
|
||||
bound = new S2LatLngRect(new R1Interval(-S2.M_PI_2, -POLE_MIN_LAT), S1Interval.full());
|
||||
break;
|
||||
}
|
||||
|
||||
// Finally, we expand the bound to account for the error when a point P is
|
||||
// converted to an S2LatLng to test for containment. (The bound should be
|
||||
// large enough so that it contains the computed S2LatLng of any contained
|
||||
// point, not just the infinite-precision version.) We don't need to expand
|
||||
// longitude because longitude is calculated via a single call to atan2(),
|
||||
// which is guaranteed to be semi-monotonic. (In fact the Gnu implementation
|
||||
// is also correctly rounded, but we don't even need that here.)
|
||||
return bound.expanded(S2LatLng.fromRadians(S2.DBL_EPSILON, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mayIntersect(S2Cell cell) {
|
||||
return cellId.intersects(cell.cellId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(S2Point p) {
|
||||
// We can't just call XYZtoFaceUV, because for points that lie on the
|
||||
// boundary between two faces (i.e. u or v is +1/-1) we need to return
|
||||
// true for both adjacent cells.
|
||||
R2Vector uvPoint = S2Projections.faceXyzToUv(face, p);
|
||||
if (uvPoint == null) {
|
||||
return false;
|
||||
}
|
||||
return (uvPoint.x() >= uMin
|
||||
&& uvPoint.x() <= uMax
|
||||
&& uvPoint.y() >= vMin
|
||||
&& uvPoint.y() <= vMax);
|
||||
}
|
||||
|
||||
// The point 'p' does not need to be normalized.
|
||||
@Override
|
||||
public boolean contains(S2Cell cell) {
|
||||
return cellId.contains(cell.cellId);
|
||||
}
|
||||
|
||||
@SuppressWarnings("AndroidJdkLibsChecker")
|
||||
private double vertexChordDist2(S2Point uvw, DoubleBinaryOperator reducer) {
|
||||
double d1 = chordDist2(uvw, uMin, vMin);
|
||||
double d2 = chordDist2(uvw, uMin, vMax);
|
||||
double d3 = chordDist2(uvw, uMax, vMin);
|
||||
double d4 = chordDist2(uvw, uMax, vMax);
|
||||
return reducer.applyAsDouble(d1, reducer.applyAsDouble(d2, reducer.applyAsDouble(d3, d4)));
|
||||
}
|
||||
|
||||
/** Returns the squared chord distance from {@code uvw} to position {@code uv}. */
|
||||
private static double chordDist2(S2Point uvw, double u, double v) {
|
||||
return uvw.getDistance2(new S2Point(u, v, 1).normalize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a point {@code p} and either the lower or upper edge of the {@link S2Cell} (specified by
|
||||
* setting {@code vEnd} to false or true respectively), returns true if {@code p} is closer to the
|
||||
* interior of that edge than it is to either endpoint.
|
||||
*/
|
||||
private boolean uEdgeIsClosest(S2Point p, boolean vEnd) {
|
||||
double v = vEnd ? vMax : vMin;
|
||||
// These are the normals to the planes that are perpendicular to the edge and pass through one
|
||||
// of its two endpoints.
|
||||
S2Point dir0 = new S2Point(v * v + 1, -uMin * v, -uMin);
|
||||
S2Point dir1 = new S2Point(v * v + 1, -uMax * v, -uMax);
|
||||
return p.dotProd(dir0) > 0 && p.dotProd(dir1) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a point {@code p} and either the left or right edge of the {@link S2Cell} (specified by
|
||||
* setting {@code uEnd} to false or true respectively), returns true if {@code p} is closer to the
|
||||
* interior of that edge than it is to either endpoint.
|
||||
*/
|
||||
private boolean vEdgeIsClosest(S2Point p, boolean uEnd) {
|
||||
double u = uEnd ? uMax : uMin;
|
||||
// See comments above.
|
||||
S2Point dir0 = new S2Point(-u * vMin, u * u + 1, -vMin);
|
||||
S2Point dir1 = new S2Point(-u * vMax, u * u + 1, -vMax);
|
||||
return p.dotProd(dir0) > 0 && p.dotProd(dir1) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the dot product of a point P with the normal of a u- or v-edge at the given coordinate
|
||||
* value, returns the distance from P to that edge.
|
||||
*/
|
||||
private static double edgeDistance(double dirIJ, double uv) {
|
||||
// Let P be the target point and let R be the closest point on the given edge AB. The desired
|
||||
// distance XR can be expressed as PR^2 = PQ^2 + QR^2 where Q is the point P projected onto the
|
||||
// plane through the great circle through AB. We can compute the distance PQ^2 perpendicular to
|
||||
// the plane from "dirIJ" (the dot product of the target point P with the edge normal) and the
|
||||
// squared length of the edge normal (1 + uv**2).
|
||||
double pq2 = (dirIJ * dirIJ) / (1 + uv * uv);
|
||||
|
||||
// We can compute the distance QR as (1 - OQ) where O is the sphere origin,
|
||||
// and we can compute OQ^2 = 1 - PQ^2 using the Pythagorean theorem.
|
||||
// (This calculation loses accuracy as the angle approaches Pi/2.)
|
||||
double qr = 1 - Math.sqrt(1 - pq2);
|
||||
return pq2 + qr * qr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance from the given point to the cell. Returns zero if the point is inside the
|
||||
* cell.
|
||||
*/
|
||||
public S1ChordAngle getDistance(S2Point targetXyz) {
|
||||
return S1ChordAngle.fromLength2(getDistanceInternal(targetXyz, true));
|
||||
}
|
||||
|
||||
/** Returns the distance to the given cell. Returns zero if one cell contains the other. */
|
||||
public S1ChordAngle getDistance(S2Cell target) {
|
||||
// If the cells intersect, the distance is zero. We use the (u,v) ranges rather than
|
||||
// S2CellId.intersects() so that cells that share a partial edge or a corner are considered to
|
||||
// intersect.
|
||||
if (face == target.face
|
||||
&& uMin <= target.uMax && uMax >= target.uMin
|
||||
&& vMin <= target.vMax && vMax >= target.vMin) {
|
||||
return S1ChordAngle.ZERO;
|
||||
}
|
||||
|
||||
// Otherwise, the minimum distance always occurs between a vertex of one cell and an edge of the
|
||||
// other cell (including the edge endpoints). This represents a total of 32 possible
|
||||
// (vertex, edge) pairs.
|
||||
//
|
||||
// TODO(user): This could be optimized to be at least 5x faster by pruning the set of possible
|
||||
// closest vertex/edge pairs using the faces and (u,v) ranges of both cells.
|
||||
S2Point[] va = new S2Point[4];
|
||||
S2Point[] vb = new S2Point[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
va[i] = getVertex(i);
|
||||
vb[i] = target.getVertex(i);
|
||||
}
|
||||
S1ChordAngle minDist = S1ChordAngle.INFINITY;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
minDist = S2EdgeUtil.updateMinDistance(va[i], vb[j], vb[(j + 1) & 3], minDist);
|
||||
minDist = S2EdgeUtil.updateMinDistance(vb[i], va[j], va[(j + 1) & 3], minDist);
|
||||
}
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
/** Returns the chord distance to targetXyz, with interior distance 0 iff toInterior is true. */
|
||||
private double getDistanceInternal(S2Point targetXyz, boolean toInterior) {
|
||||
// All calculations are done in the (u,v,w) coordinates of this cell's face.
|
||||
S2Point targetUvw = S2Projections.faceXyzToUvw(face, targetXyz);
|
||||
|
||||
// Compute dot products with all four upward or rightward-facing edge normals. "dirIJ" is the
|
||||
// dot product for the edge corresponding to axis I, endpoint J. For example, dir01 is the
|
||||
// right edge of the S2Cell (corresponding to the upper endpoint of the u-axis).
|
||||
double dir00 = targetUvw.x - targetUvw.z * uMin;
|
||||
double dir01 = targetUvw.x - targetUvw.z * uMax;
|
||||
double dir10 = targetUvw.y - targetUvw.z * vMin;
|
||||
double dir11 = targetUvw.y - targetUvw.z * vMax;
|
||||
boolean inside = true;
|
||||
if (dir00 < 0) {
|
||||
inside = false; // Target is to the left of the cell.
|
||||
if (vEdgeIsClosest(targetUvw, false)) {
|
||||
return edgeDistance(-dir00, uMin);
|
||||
}
|
||||
}
|
||||
if (dir01 > 0) {
|
||||
inside = false; // Target is to the right of the cell.
|
||||
if (vEdgeIsClosest(targetUvw, true)) {
|
||||
return edgeDistance(dir01, uMax);
|
||||
}
|
||||
}
|
||||
if (dir10 < 0) {
|
||||
inside = false; // Target is below the cell.
|
||||
if (uEdgeIsClosest(targetUvw, false)) {
|
||||
return edgeDistance(-dir10, vMin);
|
||||
}
|
||||
}
|
||||
if (dir11 > 0) {
|
||||
inside = false; // Target is above the cell.
|
||||
if (uEdgeIsClosest(targetUvw, true)) {
|
||||
return edgeDistance(dir11, vMax);
|
||||
}
|
||||
}
|
||||
if (inside) {
|
||||
if (toInterior) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Although you might think of S2Cells as rectangles, they are actually arbitrary
|
||||
// quadrilaterals after they are projected onto the sphere. Therefore the simplest approach is
|
||||
// just to find the minimum distance to any of the four edges.
|
||||
return Doubles.min(
|
||||
edgeDistance(-dir00, uMin),
|
||||
edgeDistance(dir01, uMax),
|
||||
edgeDistance(-dir10, vMin),
|
||||
edgeDistance(dir11, vMax));
|
||||
}
|
||||
|
||||
// Otherwise, the closest point is one of the four cell vertices. Note that
|
||||
// it is *not* trivial to narrow down the candidates based on the edge sign
|
||||
// tests above, because (1) the edges don't meet at right angles and (2)
|
||||
// there are points on the far side of the sphere that are both above *and*
|
||||
// below the cell, etc.
|
||||
return vertexChordDist2(targetUvw, Math::min);
|
||||
}
|
||||
|
||||
/** Returns the maximum distance from the cell (including its interior) to the given point. */
|
||||
public S1ChordAngle getMaxDistance(S2Point target) {
|
||||
// First check the 4 cell vertices. If all are within the hemisphere centered around target,
|
||||
// the max distance will be to one of these vertices.
|
||||
S2Point targetUvw = S2Projections.faceXyzToUvw(face, target);
|
||||
double maxDist = vertexChordDist2(targetUvw, Math::max);
|
||||
if (maxDist <= S1ChordAngle.RIGHT.getLength2()) {
|
||||
return S1ChordAngle.fromLength2(maxDist);
|
||||
}
|
||||
|
||||
// Otherwise, find the minimum distance d_min to the antipodal point and the maximum distance
|
||||
// will be Pi - d_min.
|
||||
return S1ChordAngle.sub(S1ChordAngle.STRAIGHT, getDistance(target.neg()));
|
||||
}
|
||||
|
||||
/** Returns the maximum distance from the cell (including its interior) to the given edge AB. */
|
||||
public S1ChordAngle getMaxDistance(S2Point a, S2Point b) {
|
||||
// If the maximum distance from both endpoints to the cell is less than Pi/2 then the maximum
|
||||
// distance from the edge to the cell is the maximum of the two endpoint distances.
|
||||
S1ChordAngle da = getMaxDistance(a);
|
||||
S1ChordAngle db = getMaxDistance(b);
|
||||
S1ChordAngle maxDist = da.compareTo(db) < 0 ? db : da;
|
||||
// TODO(eengle): Use a thresholded distance via S2Predicates.
|
||||
if (maxDist.compareTo(S1ChordAngle.RIGHT) <= 0) {
|
||||
return maxDist;
|
||||
} else {
|
||||
return S1ChordAngle.sub(S1ChordAngle.STRAIGHT, getDistanceToEdge(a.neg(), b.neg()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the maximum distance from the cell, including interior, to the given target cell. */
|
||||
public S1ChordAngle getMaxDistance(S2Cell target) {
|
||||
// If the antipodal target intersects the cell, the distance is S1ChordAngle.STRAIGHT.
|
||||
// The antipodal UV is the transpose of the original UV, interpreted within the opposite face.
|
||||
if (face == (target.face >= 3 ? target.face - 3 : target.face + 3)
|
||||
&& uMin <= target.vMax && uMax >= target.vMin
|
||||
&& vMin <= target.uMax && vMax >= target.uMin) {
|
||||
return S1ChordAngle.STRAIGHT;
|
||||
}
|
||||
|
||||
// Otherwise, the maximum distance always occurs between a vertex of one cell and an edge of the
|
||||
// other cell (including the edge endpoints). This represents a total of 32 possible
|
||||
// (vertex, edge) pairs.
|
||||
//
|
||||
// TODO(eengle): When the maximum distance is at most Pi/2, the maximum is always attained
|
||||
// between a pair of vertices, and this could be made much faster by testing each vertex pair
|
||||
// once rather than the current 4 times.
|
||||
S2Point[] va = new S2Point[4];
|
||||
S2Point[] vb = new S2Point[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
va[i] = getVertex(i);
|
||||
vb[i] = target.getVertex(i);
|
||||
}
|
||||
S1ChordAngle maxDist = S1ChordAngle.NEGATIVE;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
maxDist = S2EdgeUtil.updateMaxDistance(va[i], vb[j], vb[(j + 1) & 3], maxDist);
|
||||
maxDist = S2EdgeUtil.updateMaxDistance(vb[i], va[j], va[(j + 1) & 3], maxDist);
|
||||
}
|
||||
}
|
||||
return maxDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance from the cell to the given edge AB, or zero if the edge intersects
|
||||
* the cell interior.
|
||||
*/
|
||||
public S1ChordAngle getDistanceToEdge(S2Point a, S2Point b) {
|
||||
// Possible optimizations:
|
||||
// - Currently the (cell vertex, edge endpoint) distances are computed twice each, and the
|
||||
// length of AB is computed 4 times.
|
||||
// - To fix this, refactor GetDistance(target) so that it skips calculating the distance to
|
||||
// each cell vertex. Instead, compute the cell vertices and distances in this function, and
|
||||
// add a low-level UpdateMinDistance that allows the XA, XB, and AB distances to be passed
|
||||
// in.
|
||||
// - It might also be more efficient to do all calculations in UVW-space, since this would
|
||||
// involve transforming 2 points rather than 4.
|
||||
|
||||
// First, check the minimum distance to the edge endpoints A and B. (This also detects whether
|
||||
// either endpoint is inside the cell.)
|
||||
S1ChordAngle minDist = Ordering.natural().min(getDistance(a), getDistance(b));
|
||||
if (minDist.isZero()) {
|
||||
return minDist;
|
||||
}
|
||||
|
||||
// Otherwise, check whether the edge crosses the cell boundary.
|
||||
S2Point[] v = new S2Point[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
v[i] = getVertex(i);
|
||||
}
|
||||
S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(a, b, v[3]);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (crosser.robustCrossing(v[i]) >= 0) {
|
||||
return S1ChordAngle.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, check whether the minimum distance occurs between a cell vertex and the interior of
|
||||
// the edge AB. (Some of this work is redundant, since it also checks the distance to the
|
||||
// endpoints A and B again.)
|
||||
//
|
||||
// Note that we don't need to check the distance from the interior of AB to the interior of a
|
||||
// cell edge, because the only way that this distance can be minimal is if the two edges cross
|
||||
// (already checked above).
|
||||
for (int i = 0; i < 4; i++) {
|
||||
minDist = S2EdgeUtil.updateMinDistance(v[i], a, b, minDist);
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
|
||||
/** Returns the distance from the cell boundary to the given point. */
|
||||
public S1ChordAngle getBoundaryDistance(S2Point target) {
|
||||
return S1ChordAngle.fromLength2(getDistanceInternal(target, false));
|
||||
}
|
||||
|
||||
private void init(S2CellId id) {
|
||||
// Set cell properties from the ID and the FaceIJ of the ID.
|
||||
cellId = id;
|
||||
face = (byte) id.face();
|
||||
long ijo = id.toIJOrientation();
|
||||
orientation = (byte) S2CellId.getOrientation(ijo);
|
||||
level = (byte) id.level();
|
||||
int i = S2CellId.getI(ijo);
|
||||
int j = S2CellId.getJ(ijo);
|
||||
int cellSize = id.getSizeIJ();
|
||||
this.uMin = S2Projections.PROJ.ijToUV(i, cellSize);
|
||||
this.uMax = S2Projections.PROJ.ijToUV(i + cellSize, cellSize);
|
||||
this.vMin = S2Projections.PROJ.ijToUV(j, cellSize);
|
||||
this.vMax = S2Projections.PROJ.ijToUV(j + cellSize, cellSize);
|
||||
}
|
||||
|
||||
private S2Point getPoint(int i, int j) {
|
||||
return S2Projections.faceUvToXyz(face, i == 0 ? uMin : uMax, j == 0 ? vMin : vMax);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + face + ", " + level + ", " + orientation + ", " + cellId + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int value = 17;
|
||||
value = 37 * (37 * (37 * value + face) + orientation) + level;
|
||||
return 37 * value + id().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (that instanceof S2Cell) {
|
||||
S2Cell thatCell = (S2Cell) that;
|
||||
return this.face == thatCell.face
|
||||
&& this.level == thatCell.level
|
||||
&& this.orientation == thatCell.orientation
|
||||
&& this.cellId.equals(thatCell.cellId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A double function of two double parameters. */
|
||||
// TODO(eengle): Remove this once the android JDK has this interface.
|
||||
private interface DoubleBinaryOperator {
|
||||
/** Returns the result of this function applied to {@code a} and {@code b}. */
|
||||
double applyAsDouble(double a, double b);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import java.util.AbstractList;
|
||||
|
||||
/**
|
||||
* A list of {@link S2CellId}s, and specialized methods for directly operating on the encoded form.
|
||||
*/
|
||||
@GwtCompatible
|
||||
abstract class S2CellIdVector extends AbstractList<S2CellId> {
|
||||
|
||||
/**
|
||||
* Returns the index of the first element {@code x} such that {@code (x >= target)}, or {@link
|
||||
* #size()} if no such element exists.
|
||||
*
|
||||
* <p>The list must be sorted into ascending order prior to making this call. If it is not sorted,
|
||||
* the results are undefined.
|
||||
*/
|
||||
abstract int lowerBound(S2CellId target);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2018 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes;
|
||||
import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor;
|
||||
import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Longs;
|
||||
import com.google.common.primitives.UnsignedLongs;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
/** An encoder/decoder of {@link List<S2CellId>}s. */
|
||||
@GwtCompatible
|
||||
class S2CellIdVectorCoder implements S2Coder<List<S2CellId>> {
|
||||
|
||||
/** An instance of an {@code S2CellIdVectorCoder}. */
|
||||
static final S2CellIdVectorCoder INSTANCE = new S2CellIdVectorCoder();
|
||||
|
||||
@Override
|
||||
public void encode(List<S2CellId> values, OutputStream output) throws IOException {
|
||||
// The encoding format is as follows:
|
||||
//
|
||||
// byte 0, bits 0-2: baseBytes
|
||||
// byte 0, bits 3-7: shiftCode
|
||||
// byte 1: extended shiftCode (only written for odd shift >= 5)
|
||||
// 0-7 bytes: base
|
||||
// values.size() encoded uint64s of deltas (encoded by UintVectorCoder.UINT64.encode)
|
||||
//
|
||||
// base consists of 0-7 bytes, and is always shifted so its bytes are the most-significant
|
||||
// bytes of a uint64 (little-endian).
|
||||
//
|
||||
// shift is in the range 0-56. shift is odd only if all S2CellIds are at the same
|
||||
// level, in which case the bit at position (shift - 1) in base is automatically set to 1.
|
||||
//
|
||||
// base (3 bits) and shift (6 bits) are encoded in either one or two bytes as follows:
|
||||
// - if (shift <= 4 or shift is even), then 1 byte
|
||||
// - else 2 bytes
|
||||
//
|
||||
// (shift == 1) means that all S2CellIds are leaf cells, and (shift == 2) means that
|
||||
// all S2CellIds are at level 29.
|
||||
|
||||
long valuesOr = 0L;
|
||||
long valuesAnd = ~0L;
|
||||
long valuesMin = ~0L;
|
||||
long valuesMax = 0L;
|
||||
|
||||
for (S2CellId cellId : values) {
|
||||
valuesOr |= cellId.id();
|
||||
valuesAnd &= cellId.id();
|
||||
valuesMin = UnsignedLongs.min(valuesMin, cellId.id());
|
||||
valuesMax = UnsignedLongs.max(valuesMax, cellId.id());
|
||||
}
|
||||
|
||||
long base = 0L;
|
||||
// The number of bytes required to encode base.
|
||||
int baseBytes = 0;
|
||||
int shift = 0;
|
||||
// The bit position of the most-significant bit of the largest delta.
|
||||
int maxDeltaMsb = 0;
|
||||
|
||||
if (UnsignedLongs.compare(valuesOr, 0) > 0) {
|
||||
// We only allow even shift, unless all values have the same low bit (in which case shift is
|
||||
// odd and the preceding bit is implicitly on). There is no point in allowing shifts > 56
|
||||
// because deltas are encoded in at least 1 byte each.
|
||||
shift = Math.min(56, Long.numberOfTrailingZeros(valuesOr) & ~1);
|
||||
if ((valuesAnd & (1L << shift)) != 0) {
|
||||
// All S2CellIds are at the same level.
|
||||
shift++;
|
||||
}
|
||||
|
||||
// base consists of the baseBytes most-significant bytes of the minimum S2CellId. We consider
|
||||
// all possible values of baseBytes (0-7) and choose the one that minimizes the total encoding
|
||||
// size.
|
||||
// The best encoding size so far.
|
||||
int minBytes = -1;
|
||||
for (int tmpBaseBytes = 0; tmpBaseBytes < 8; tmpBaseBytes++) {
|
||||
// The base value being tested (first tmpBaseBytes of valuesMin).
|
||||
long tmpBase = valuesMin & ~(~0L >>> (8 * tmpBaseBytes));
|
||||
// The most-significant bit position of the largest delta (or zero if there are no deltas
|
||||
// [i.e., if values.size == 0]).
|
||||
int tmpMaxDeltaMsb =
|
||||
Math.max(0, 63 - Long.numberOfLeadingZeros((valuesMax - tmpBase) >>> shift));
|
||||
// The total size of the variable portion of the encoding.
|
||||
int candidateBytes = tmpBaseBytes + values.size() * ((tmpMaxDeltaMsb >> 3) + 1);
|
||||
|
||||
if (UnsignedLongs.compare(candidateBytes, minBytes) < 0) {
|
||||
base = tmpBase;
|
||||
baseBytes = tmpBaseBytes;
|
||||
maxDeltaMsb = tmpMaxDeltaMsb;
|
||||
minBytes = candidateBytes;
|
||||
}
|
||||
}
|
||||
// It takes one extra byte to encode odd shifts (i.e., the case where all S2CellIds are at the
|
||||
// same level), so we check whether we can get the same encoding size per delta using an even
|
||||
// shift.
|
||||
if (((shift & 1) != 0) && (maxDeltaMsb & 7) != 7) {
|
||||
shift--;
|
||||
}
|
||||
}
|
||||
assert (shift <= 56);
|
||||
|
||||
// shift and baseBytes are encoded in 1 or 2 bytes.
|
||||
// shiftCode is 5 bits, values:
|
||||
// - <= 28 represent even shifts in the range 0-56.
|
||||
// - 29, 30 represent odd shifts 1 and 3.
|
||||
// - 31 indicates that the shift is odd and encoded in the next byte.
|
||||
int shiftCode = shift >> 1;
|
||||
if ((shift & 1) != 0) {
|
||||
shiftCode = Math.min(31, shiftCode + 29);
|
||||
}
|
||||
output.write((byte) ((shiftCode << 3) | baseBytes));
|
||||
if (shiftCode == 31) {
|
||||
// shift is always odd, so 3 bits unused.
|
||||
output.write((byte) (shift >> 1));
|
||||
}
|
||||
|
||||
// Encode the baseBytes most-significant bytes of base.
|
||||
long baseCode = base >>> (64 - 8 * Math.max(1, baseBytes));
|
||||
EncodedInts.encodeUintWithLength(output, baseCode, baseBytes);
|
||||
|
||||
// Encode the vector of deltas.
|
||||
long tmpBase = base;
|
||||
long tmpShift = shift;
|
||||
UintVectorCoder.UINT64.encode(
|
||||
new Longs() {
|
||||
@Override
|
||||
public long get(int position) {
|
||||
return (values.get(position).id() - tmpBase) >>> tmpShift;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return values.size();
|
||||
}
|
||||
},
|
||||
output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public S2CellIdVector decode(Bytes data, Cursor cursor) {
|
||||
// See encode for documentation on the encoding format.
|
||||
|
||||
// Invert the encoding of (shiftCode, baseBytes).
|
||||
int shiftCodeBaseBytes = data.get(cursor.position++) & 0xff;
|
||||
int shiftCode = shiftCodeBaseBytes >> 3;
|
||||
if (shiftCode == 31) {
|
||||
shiftCode = 29 + (data.get(cursor.position++) & 0xff);
|
||||
}
|
||||
|
||||
// Decode the baseBytes most-significant bytes of base.
|
||||
int baseBytes = shiftCodeBaseBytes & 7;
|
||||
long base = data.readUintWithLength(cursor, baseBytes);
|
||||
base <<= 64 - 8 * Math.max(1, baseBytes);
|
||||
|
||||
// Invert the encoding of shiftCode.
|
||||
long shift;
|
||||
if (shiftCode >= 29) {
|
||||
shift = 2L * (shiftCode - 29) + 1;
|
||||
base |= 1L << (shift - 1);
|
||||
} else {
|
||||
shift = 2L * shiftCode;
|
||||
}
|
||||
|
||||
long tmpBase = base;
|
||||
Longs deltas = UintVectorCoder.UINT64.decode(data, cursor);
|
||||
return new S2CellIdVector() {
|
||||
@Override
|
||||
public int size() {
|
||||
return deltas.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S2CellId get(int index) {
|
||||
return new S2CellId((deltas.get(index) << shift) + tmpBase);
|
||||
}
|
||||
|
||||
@Override
|
||||
int lowerBound(S2CellId target) {
|
||||
if (UnsignedLongs.compare(target.id(), tmpBase) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (target.greaterOrEquals(S2CellId.end(S2CellId.MAX_LEVEL))) {
|
||||
return size();
|
||||
}
|
||||
int low = 0;
|
||||
int high = deltas.length();
|
||||
long needle = (target.id() - tmpBase + (1L << shift) - 1) >>> shift;
|
||||
|
||||
// Binary search for the index of the first element in deltas that is >= needle.
|
||||
while (low < high) {
|
||||
int mid = (low + high) >> 1;
|
||||
long value = deltas.get(mid);
|
||||
if (UnsignedLongs.compare(value, needle) < 0) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
/*
|
||||
* Copyright 2019 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.util.AbstractList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A collection of (cellId, label) pairs. The S2CellIds may be overlapping or contain duplicate
|
||||
* values. For example, an S2CellIndex could store a collection of S2CellUnions, where each
|
||||
* S2CellUnion has its own label. Labels are 32-bit non-negative integers, and are typically used to
|
||||
* map the results of queries back to client data structures.
|
||||
*
|
||||
* <p>To build an S2CellIndex, call add() for each (cellId, label) pair, and then call the build()
|
||||
* method. There is also a convenience method to associate a label with each cell in a union.
|
||||
*
|
||||
* <p>Note that the index is not dynamic; the contents of the index cannot be changed once it has
|
||||
* been built. However, the same memory space can be reused by {@link #clear clearing} the index.
|
||||
*
|
||||
* <p>There are several options for retrieving data from the index. The simplest is to use a
|
||||
* built-in method such as getIntersectingLabels, which returns the labels of all cells that
|
||||
* intersect a given target S2CellUnion. Alternatively, you can access the index contents directly.
|
||||
*
|
||||
* <p>Internally, the index consists of a set of non-overlapping leaf cell ranges that subdivide the
|
||||
* sphere and such that each range intersects a particular set of (cellId, label) pairs. Data is
|
||||
* accessed using the following iterator types:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link RangeIterator}: used to seek and iterate over the non-overlapping leaf cell ranges.
|
||||
* <li>{@link NonEmptyRangeIterator}: like RangeIterator, but skips ranges whose contents are
|
||||
* empty.
|
||||
* <li>{@link ContentsIterator}: iterates over the (cellId, label) pairs that intersect a given
|
||||
* range.
|
||||
* <li>{@link CellIterator}: iterates over the entire set of (cellId, label) pairs.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that these are low-level, efficient types intended mainly for implementing new query
|
||||
* classes. Most clients should use either the built-in methods such as {@link
|
||||
* #visitIntersectingCells} and {@link #getIntersectingLabels}.
|
||||
*/
|
||||
@GwtCompatible
|
||||
public class S2CellIndex {
|
||||
/**
|
||||
* A tree of (cellId, label) pairs such that if X is an ancestor of Y, then X.cellId contains
|
||||
* Y.cellId. The contents of a given range of leaf cells can be represented by pointing to a node
|
||||
* of this tree.
|
||||
*/
|
||||
private final List<CellNode> cellNodes = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The last element of nodes is a sentinel value, which is necessary in order to represent the
|
||||
* range covered by the previous element.
|
||||
*/
|
||||
private final ArrayList<RangeNode> rangeNodes = new ArrayList<>();
|
||||
|
||||
/** Returns the number of (cellId, label) pairs in the index. */
|
||||
public int numCells() {
|
||||
return cellNodes.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given (cellId, label) pair to the index. Note that the index is not valid until
|
||||
* {@link #build} is called.
|
||||
*
|
||||
* <p>The S2CellIds in the index may overlap (including duplicate values). Duplicate (cellId,
|
||||
* label) pairs are also allowed, although query tools often remove duplicates.
|
||||
*
|
||||
* <p>Results are undefined unless all cells are {@link S2CellId#isValid valid}.
|
||||
*/
|
||||
public void add(S2CellId cellId, int label) {
|
||||
assert cellId.isValid();
|
||||
assert label >= 0;
|
||||
cellNodes.add(new CellNode(cellId, label, -1));
|
||||
}
|
||||
|
||||
/** Convenience function that adds a collection of cells with the same label. */
|
||||
public void add(Iterable<S2CellId> cellIds, int label) {
|
||||
for (S2CellId cellId : cellIds) {
|
||||
add(cellId, label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the index. This method may only be called once. No iterators may be used until the index
|
||||
* is built.
|
||||
*/
|
||||
public void build() {
|
||||
// Create two deltas for each (cellId, label) pair: one to add the pair to the stack (at the
|
||||
// start of its leaf cell range), and one to remove it from the stack (at the end of its leaf
|
||||
// cell range).
|
||||
Delta[] deltas = new Delta[2 * cellNodes.size() + 2];
|
||||
int size = 0;
|
||||
for (CellNode node : cellNodes) {
|
||||
deltas[size++] = new Delta(node.cellId.rangeMin(), node.cellId, node.label);
|
||||
deltas[size++] = new Delta(node.cellId.rangeMax().next(), S2CellId.sentinel(), -1);
|
||||
}
|
||||
|
||||
// We also create two special deltas to ensure that a RangeNode is emitted at the beginning and
|
||||
// end of the S2CellId range.
|
||||
deltas[size++] = new Delta(S2CellId.begin(S2CellId.MAX_LEVEL), S2CellId.none(), -1);
|
||||
deltas[size++] = new Delta(S2CellId.end(S2CellId.MAX_LEVEL), S2CellId.none(), -1);
|
||||
Arrays.sort(deltas, Delta.BY_START_CELL_NEG_LABEL);
|
||||
|
||||
// Now walk through the deltas to build the leaf cell ranges and cell tree (which is essentially
|
||||
// a permanent form of the "stack" described above).
|
||||
cellNodes.clear();
|
||||
rangeNodes.ensureCapacity(deltas.length);
|
||||
int contents = -1;
|
||||
for (int i = 0; i < deltas.length; ) {
|
||||
// Process all the deltas associated with the current startId.
|
||||
S2CellId startId = deltas[i].startId;
|
||||
for (; i < deltas.length && deltas[i].startId.equals(startId); i++) {
|
||||
if (deltas[i].label >= 0) {
|
||||
cellNodes.add(new CellNode(deltas[i].cellId, deltas[i].label, contents));
|
||||
contents = cellNodes.size() - 1;
|
||||
} else if (deltas[i].cellId.equals(S2CellId.sentinel())) {
|
||||
contents = cellNodes.get(contents).parent;
|
||||
}
|
||||
}
|
||||
rangeNodes.add(new RangeNode(startId, contents));
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns an iterator over the cells of this index. */
|
||||
public CellIterator cells() {
|
||||
Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first.");
|
||||
return new CellIterator();
|
||||
}
|
||||
|
||||
/** Returns an iterator over the ranges of this index. */
|
||||
public RangeIterator ranges() {
|
||||
Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first.");
|
||||
return new RangeIterator();
|
||||
}
|
||||
|
||||
/** Returns an iterator over the non-empty ranges of this index. */
|
||||
public NonEmptyRangeIterator nonEmptyRanges() {
|
||||
return new NonEmptyRangeIterator();
|
||||
}
|
||||
|
||||
/** Returns an iterator over the contents of this index. */
|
||||
public ContentsIterator contents() {
|
||||
Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first.");
|
||||
return new ContentsIterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* To build the cell tree and leaf cell ranges, we maintain a stack of (cellId, label) pairs that
|
||||
* contain the current leaf cell. This class represents an instruction to push or pop a (cellId,
|
||||
* label) pair.
|
||||
*
|
||||
* <p>If label >= 0, the (cellId, label) pair is pushed on the stack. If cellId ==
|
||||
* S2CellId.SENTINEL, a pair is popped from the stack. Otherwise the stack is unchanged but a
|
||||
* RangeNode is still emitted.
|
||||
*/
|
||||
private static final class Delta {
|
||||
/**
|
||||
* Deltas are sorted first by startId, then in reverse order by cellId, and then by label. This
|
||||
* is necessary to ensure that (1) larger cells are pushed on the stack before smaller cells,
|
||||
* and (2) cells are popped off the stack before any new cells are added.
|
||||
*/
|
||||
public static final Comparator<Delta> BY_START_CELL_NEG_LABEL = (a, b) -> {
|
||||
int result = a.startId.compareTo(b.startId);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = -a.cellId.compareTo(b.cellId);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return Integer.compare(a.label, b.label);
|
||||
};
|
||||
|
||||
private final S2CellId startId;
|
||||
private final S2CellId cellId;
|
||||
private final int label;
|
||||
|
||||
private Delta(S2CellId startId, S2CellId cellId, int label) {
|
||||
this.startId = startId;
|
||||
this.cellId = cellId;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears the index so that it can be re-used. */
|
||||
public void clear() {
|
||||
cellNodes.clear();
|
||||
rangeNodes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits all (cellId, label) pairs in the given index that intersect the given S2CellUnion
|
||||
* "target" and returns true, or terminates early and returns false if {@link CellVisitor#visit}
|
||||
* ever returns false.
|
||||
*
|
||||
* <p>Each (cellId, label) pair in the index is visited at most once. If the index contains
|
||||
* duplicates, then each copy is visited.
|
||||
*/
|
||||
public boolean visitIntersectingCells(S2CellUnion target, CellVisitor visitor) {
|
||||
if (target.size() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ContentsIterator contents = contents();
|
||||
RangeIterator range = ranges();
|
||||
for (int i = 0; i < target.size(); ) {
|
||||
S2CellId id = target.cellId(i);
|
||||
|
||||
// seek the range to this target cell when necessary.
|
||||
if (range.limitId().lessOrEquals(id.rangeMin())) {
|
||||
range.seek(id.rangeMin());
|
||||
}
|
||||
|
||||
// Visit contents of this range that intersect this cell.
|
||||
for (; range.startId().lessOrEquals(id.rangeMax()); range.next()) {
|
||||
for (contents.startUnion(range); !contents.done(); contents.next()) {
|
||||
if (!visitor.visit(contents.cellId(), contents.label())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the next target cell is also contained by the leaf cell range that we just
|
||||
// processed. If so, we can skip over all such cells using binary search. This speeds up
|
||||
// benchmarks by 2-10x when the average number of intersecting cells is small (< 1).
|
||||
i++;
|
||||
if (i != target.size()) {
|
||||
id = target.cellId(i);
|
||||
if (id.rangeMax().lessThan(range.startId())) {
|
||||
// Skip to the first target cell that extends past the previous range.
|
||||
i = S2ShapeUtil.lowerBound(i + 1, target.size(),
|
||||
j -> range.startId().greaterThan(target.cellId(j)));
|
||||
if (target.cellId(i - 1).rangeMax().greaterOrEquals(range.startId())) {
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Returns the distinct sorted labels that intersect the given target. */
|
||||
public Labels getIntersectingLabels(S2CellUnion target) {
|
||||
Labels result = new Labels();
|
||||
getIntersectingLabels(target, result);
|
||||
result.normalize();
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Appends labels intersecting 'target', in unspecified order, with possible duplicates. */
|
||||
public void getIntersectingLabels(S2CellUnion target, Labels results) {
|
||||
visitIntersectingCells(target, (cellId, label) -> results.add(label));
|
||||
}
|
||||
|
||||
/** A function that is called with each (cellId, label) pair to be visited. */
|
||||
public interface CellVisitor {
|
||||
/** Provides a (cellId, label) pair to this visitor, which may return true to keep searching. */
|
||||
boolean visit(S2CellId cellId, int label);
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of labels that can be grown by {@link #getIntersectingLabels(S2CellUnion, Labels)} and
|
||||
* shrunk via {@link #clear} or {@link #normalize}. May contain duplicates or be unsorted unless
|
||||
* {@link #normalize} is called.
|
||||
*/
|
||||
public static class Labels extends AbstractList<Integer> {
|
||||
private int[] labels = new int[8];
|
||||
private int size;
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
size = 0;
|
||||
}
|
||||
|
||||
private boolean add(int label) {
|
||||
if (labels.length == size) {
|
||||
labels = Arrays.copyOf(labels, size * 2);
|
||||
}
|
||||
labels[size++] = label;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer get(int index) {
|
||||
return getInt(index);
|
||||
}
|
||||
|
||||
/** As {@link #get(int)} but without the overhead of boxing. */
|
||||
public int getInt(int index) {
|
||||
return labels[index];
|
||||
}
|
||||
|
||||
/** Sorts the labels and removes duplicates. */
|
||||
public void normalize() {
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
Arrays.sort(labels, 0, size);
|
||||
int lastIndex = 0;
|
||||
for (int i = 1; i < size; i++) {
|
||||
if (labels[lastIndex] != labels[i]) {
|
||||
labels[++lastIndex] = labels[i];
|
||||
}
|
||||
}
|
||||
size = lastIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a node in the (cellId, label) tree. Cells are organized in a tree such that the
|
||||
* ancestors of a given node contain that node.
|
||||
*/
|
||||
private static final class CellNode {
|
||||
private S2CellId cellId;
|
||||
private int label;
|
||||
private int parent;
|
||||
|
||||
private CellNode(S2CellId cellId, int label, int parent) {
|
||||
this.cellId = cellId;
|
||||
this.label = label;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
private void setFrom(CellNode node) {
|
||||
this.cellId = node.cellId;
|
||||
this.label = node.label;
|
||||
this.parent = node.parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** An iterator over all (cellId, label) pairs in an unspecified order. */
|
||||
public final class CellIterator {
|
||||
/** Offset into {@link S2CellIndex#cellNodes}. */
|
||||
private int offset;
|
||||
/** Current node pointed to by 'offset', or null if {@link #done}. */
|
||||
private CellNode cell;
|
||||
|
||||
// Initializes a CellIterator for the given S2CellIndex, positioned at the first cell (if any).
|
||||
private CellIterator() {
|
||||
Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first.");
|
||||
seek(0);
|
||||
}
|
||||
|
||||
/** Returns the S2CellId of the current (cellId, label) pair. */
|
||||
public S2CellId cellId() {
|
||||
assert !done();
|
||||
return cell.cellId;
|
||||
}
|
||||
|
||||
/** Returns the label of the current (cellId, label) pair. */
|
||||
public int label() {
|
||||
assert !done();
|
||||
return cell.label;
|
||||
}
|
||||
|
||||
/** Returns true if all (cellId, label) pairs have been visited. */
|
||||
public boolean done() {
|
||||
return offset == cellNodes.size();
|
||||
}
|
||||
|
||||
/** Advances this iterator to the next (cellId, label) pair. */
|
||||
public void next() {
|
||||
assert !done();
|
||||
seek(offset + 1);
|
||||
}
|
||||
|
||||
/** Sets the offset and sets 'cell' accordingly. */
|
||||
private void seek(int offset) {
|
||||
this.offset = offset;
|
||||
this.cell = done() ? null : cellNodes.get(offset);
|
||||
}
|
||||
}
|
||||
|
||||
/** A range of leaf S2CellIds, from the level 30 leaf cell of this range to the next range. */
|
||||
private static class RangeNode {
|
||||
/** First leaf cell contained by this range. */
|
||||
private final S2CellId startId;
|
||||
/** Index in {@link S2CellIndex#cellNodes} for the cells that overlap this range. */
|
||||
private final int contents;
|
||||
|
||||
private RangeNode(S2CellId startId, int contents) {
|
||||
this.startId = startId;
|
||||
this.contents = contents;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An iterator that seeks and iterates over a set of non-overlapping leaf cell ranges that cover
|
||||
* the entire sphere. The indexed (s2cell_id, label) pairs that intersect the current leaf cell
|
||||
* range can be visited using ContentsIterator (see below).
|
||||
*/
|
||||
public class RangeIterator {
|
||||
/** Offset into {@link S2CellIndex#rangeNodes}. */
|
||||
private int offset;
|
||||
/** Current node pointed to by 'offset'. */
|
||||
private RangeNode node = rangeNodes.get(offset);
|
||||
|
||||
/**
|
||||
* Returns the start of the current range of leaf S2CellIds. When {@link #done}, this returns
|
||||
* the {@link S2CellId#end end} of the {@link S2CellId#MAX_LEVEL max level} of cells, so that
|
||||
* most loops may test this method instead of done().
|
||||
*/
|
||||
public S2CellId startId() {
|
||||
return node.startId;
|
||||
}
|
||||
|
||||
/** The (non-inclusive) end of the current range of leaf S2CellIds. */
|
||||
public S2CellId limitId() {
|
||||
assert (!done());
|
||||
return rangeNodes.get(offset + 1).startId;
|
||||
}
|
||||
|
||||
/** Returns true if the iterator is positioned beyond the last valid range. */
|
||||
public boolean done() {
|
||||
// Note that the last element of rangeNodes is a sentinel value.
|
||||
return offset >= rangeNodes.size() - 1;
|
||||
}
|
||||
|
||||
/** Positions this iterator at the first range of leaf cells (if any). */
|
||||
public void begin() {
|
||||
seekAndLoad(0);
|
||||
}
|
||||
|
||||
/** Positions the iterator so that done() is true. */
|
||||
public void finish() {
|
||||
// Note that the last element of rangeNodes is a sentinel value.
|
||||
seekAndLoad(rangeNodes.size() - 1);
|
||||
}
|
||||
|
||||
/** Advances the iterator to the next range of leaf cells. */
|
||||
public void next() {
|
||||
assert (!done());
|
||||
seekAndLoad(offset + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the iterator was already positioned at the beginning, otherwise positions
|
||||
* the iterator at the previous entry and returns true.
|
||||
*/
|
||||
public boolean prev() {
|
||||
if (offset == 0) {
|
||||
return false;
|
||||
}
|
||||
seekAndLoad(offset - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the iterator at the range containing "target". Such a range exists as long as the
|
||||
* target is a valid leaf cell.
|
||||
*
|
||||
* @param target a valid leaf (level 30) cell to seek to
|
||||
*/
|
||||
public void seek(S2CellId target) {
|
||||
assert target.isLeaf();
|
||||
seekAndLoad(S2ShapeUtil.upperBound(0, rangeNodes.size(),
|
||||
i -> target.lessThan(rangeNodes.get(i).startId)) - 1);
|
||||
}
|
||||
|
||||
/** Returns true if no (s2cell_id, label) pairs intersect this range, or if {@link #done}. */
|
||||
public boolean isEmpty() {
|
||||
return node.contents == ContentsIterator.DONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances this iterator 'n' times and returns true, or if doing so would advance this iterator
|
||||
* past the end, leaves the iterator unmodified and returns false.
|
||||
*/
|
||||
public boolean advance(int n) {
|
||||
// Note that the last element of rangeNodes is a sentinel value.
|
||||
if (n >= rangeNodes.size() - 1 - offset) {
|
||||
return false;
|
||||
}
|
||||
seekAndLoad(offset + n);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void seekAndLoad(int offset) {
|
||||
this.offset = offset;
|
||||
this.node = rangeNodes.get(offset);
|
||||
}
|
||||
}
|
||||
|
||||
/** As {@link RangeIterator} but only visits range nodes that overlap (cellId, label) pairs. */
|
||||
public class NonEmptyRangeIterator extends RangeIterator {
|
||||
@Override
|
||||
public void begin() {
|
||||
super.begin();
|
||||
while (isEmpty() && !done()) {
|
||||
super.next();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void next() {
|
||||
do {
|
||||
super.next();
|
||||
} while (isEmpty() && !done());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prev() {
|
||||
while (super.prev()) {
|
||||
if (!isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Return the iterator to its original position.
|
||||
if (isEmpty() && !done()) {
|
||||
next();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Positions the iterator at the range that contains or follows "target", or at the end if no
|
||||
// such range exists. (Note that start_id() may still be called in the latter case.)
|
||||
@Override
|
||||
public void seek(S2CellId target) {
|
||||
super.seek(target);
|
||||
while (isEmpty() && !done()) {
|
||||
super.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An iterator that visits the (cellId, label) pairs that cover a set of leaf cell ranges (see
|
||||
* RangeIterator). To use it, construct an instance or {@link #clear} an existing instance, and
|
||||
* {@link #startUnion} to visit the contents of each desired leaf cell range.
|
||||
*
|
||||
* <p>Note that when multiple leaf cell ranges are visited, this class only guarantees that each
|
||||
* result will be reported at least once, i.e. duplicate values may be suppressed. If you want
|
||||
* duplicate values to be reported again, be sure to call {@link #clear} first.
|
||||
*
|
||||
* <p>In particular, the implementation guarantees that when multiple leaf cell ranges are visited
|
||||
* in monotonically increasing order, then each (cellId, label) pair is reported exactly once.
|
||||
*/
|
||||
public class ContentsIterator {
|
||||
/** A special label indicating that {@link #done} is true. */
|
||||
private static final int DONE = -1;
|
||||
|
||||
/**
|
||||
* The value of it.startId() from the previous call to startUnion(). This is used to check
|
||||
* whether these values are monotonically increasing.
|
||||
*/
|
||||
private S2CellId prevStartId;
|
||||
|
||||
/**
|
||||
* The maximum index within {@link #cellNodes} visited during the previous call to startUnion().
|
||||
* This is used to eliminate duplicate values when startUnion() is called multiple times.
|
||||
*/
|
||||
private int nodeCutoff;
|
||||
|
||||
/**
|
||||
* The maximum index within {@link #cellNodes} visited during the current call to startUnion().
|
||||
* This is used to update nodeCutoff.
|
||||
*/
|
||||
private int nextNodeCutoff;
|
||||
|
||||
/** A copy of the current node in the cell tree. */
|
||||
private final CellNode node = new CellNode(null, DONE, -1);
|
||||
|
||||
/** Creates a new iterator. Call {@link #startUnion} next. */
|
||||
private ContentsIterator() {
|
||||
clear();
|
||||
}
|
||||
|
||||
/** Clears all state with respect to which range(s) have been visited. */
|
||||
public void clear() {
|
||||
prevStartId = S2CellId.none();
|
||||
nodeCutoff = -1;
|
||||
nextNodeCutoff = -1;
|
||||
setDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the ContentsIterator at the first (cellId, label) pair that covers the given leaf
|
||||
* cell range. Note that when multiple leaf cell ranges are visited using the same
|
||||
* ContentsIterator, duplicate values may be suppressed. If you don't want this behavior, call
|
||||
* clear() first.
|
||||
*/
|
||||
public void startUnion(RangeIterator range) {
|
||||
if (range.startId().lessThan(prevStartId)) {
|
||||
// Can't automatically eliminate duplicates.
|
||||
nodeCutoff = -1;
|
||||
}
|
||||
prevStartId = range.startId();
|
||||
int contents = range.node.contents;
|
||||
if (contents <= nodeCutoff) {
|
||||
setDone();
|
||||
} else {
|
||||
node.setFrom(cellNodes.get(contents));
|
||||
}
|
||||
|
||||
// When visiting ancestors, we can stop as soon as the node index is smaller than any
|
||||
// previously visited node index. Because indexes are assigned using a preorder traversal,
|
||||
// such nodes are guaranteed to have already been reported.
|
||||
nextNodeCutoff = contents;
|
||||
}
|
||||
|
||||
/** Returns the S2CellId of the current (cellId, label) pair. */
|
||||
public S2CellId cellId() {
|
||||
assert !done();
|
||||
return node.cellId;
|
||||
}
|
||||
|
||||
/** Returns the label of the current (cellId, label) pair. */
|
||||
public int label() {
|
||||
assert !done();
|
||||
return node.label;
|
||||
}
|
||||
|
||||
/** Returns true if all (cellId, label) pairs have been visited. */
|
||||
public boolean done() {
|
||||
return node.label == DONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the iterator to the next (cellId, label) pair covered by the current leaf cell
|
||||
* range.
|
||||
*/
|
||||
public void next() {
|
||||
assert !done();
|
||||
if (node.parent <= nodeCutoff) {
|
||||
// We have already processed this node and its ancestors.
|
||||
nodeCutoff = nextNodeCutoff;
|
||||
setDone();
|
||||
} else {
|
||||
node.setFrom(cellNodes.get(node.parent));
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the current node label to DONE to indicate that iteration has finished. */
|
||||
private void setDone() {
|
||||
node.label = DONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,837 @@
|
||||
/*
|
||||
* Copyright 2005 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.mogo.eagle.core.utilcode.geometry;
|
||||
|
||||
import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ;
|
||||
|
||||
import com.google.common.annotations.GwtCompatible;
|
||||
import com.google.common.collect.Lists;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An S2CellUnion is a region consisting of cells of various sizes. Typically a cell union is used
|
||||
* to approximate some other shape. There is a tradeoff between the accuracy of the approximation
|
||||
* and how many cells are used. Unlike polygons, cells have a fixed hierarchical structure. This
|
||||
* makes them more suitable for optimizations based on preprocessing.
|
||||
*
|
||||
* <p>An S2CellUnion is represented as a vector of sorted, non-overlapping S2CellIds. By default the
|
||||
* vector is also "normalized", meaning that groups of 4 child cells have been replaced by their
|
||||
* parent cell whenever possible. S2CellUnions are not required to be normalized, but certain
|
||||
* operations will return different results if they are not, e.g. {@link #contains(S2CellUnion)}.
|
||||
*
|
||||
*/
|
||||
@GwtCompatible(serializable = true)
|
||||
public strictfp class S2CellUnion implements S2Region, Iterable<S2CellId>, Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final byte LOSSLESS_ENCODING_VERSION = 1;
|
||||
|
||||
/** The CellIds that form the Union */
|
||||
private ArrayList<S2CellId> cellIds = new ArrayList<S2CellId>();
|
||||
|
||||
public S2CellUnion() {}
|
||||
|
||||
/**
|
||||
* Populates a cell union with the given S2CellIds, and then calls normalize(). This directly uses
|
||||
* the input list, without copying it.
|
||||
*/
|
||||
public void initFromCellIds(ArrayList<S2CellId> cellIds) {
|
||||
initRawCellIds(cellIds);
|
||||
normalize();
|
||||
}
|
||||
|
||||
/** Populates a cell union with the given 64-bit cell ids, and then calls normalize(). */
|
||||
public void initFromIds(List<Long> cellIds) {
|
||||
initRawIds(cellIds);
|
||||
normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a cell union with the given S2CellIds. The input list is copied, and then cleared.
|
||||
*/
|
||||
public void initSwap(List<S2CellId> cellIds) {
|
||||
initRawSwap(cellIds);
|
||||
normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a cell union with the given S2CellIds. This does not call normalize, see {@link
|
||||
* #initRawSwap} for details. This directly uses the input list, without copying it.
|
||||
*/
|
||||
public void initRawCellIds(ArrayList<S2CellId> cellIds) {
|
||||
this.cellIds = cellIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a cell union with the given 64 bit cell ids. This does not call normalize, see {@link
|
||||
* #initRawSwap} for details. The input list is copied.
|
||||
*/
|
||||
// TODO(eengle): Make a constructed S2CellUnion immutable, and port other init methods from C++.
|
||||
public void initRawIds(List<Long> cellIds) {
|
||||
int size = cellIds.size();
|
||||
this.cellIds = new ArrayList<S2CellId>(size);
|
||||
for (Long id : cellIds) {
|
||||
this.cellIds.add(new S2CellId(id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Like the initFrom*() constructors, but does not call normalize(). The cell union *must* be
|
||||
* normalized before doing any calculations with it, so it is the caller's * responsibility to
|
||||
* make sure that the input is normalized. This method is useful when converting cell unions to
|
||||
* another representation and back.
|
||||
*
|
||||
* <p>The input list is copied, and then cleared.
|
||||
*/
|
||||
public void initRawSwap(List<S2CellId> cellIds) {
|
||||
this.cellIds = new ArrayList<S2CellId>(cellIds);
|
||||
cellIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cell union that corresponds to a continuous range of cell ids. The output is a
|
||||
* normalized collection of cell ids that covers the leaf cells between "minId" and "maxId"
|
||||
* inclusive.
|
||||
*
|
||||
* <p>Requires that minId.isLeaf(), maxId.isLeaf(), and minId <= maxId.
|
||||
*/
|
||||
public void initFromMinMax(S2CellId minId, S2CellId maxId) {
|
||||
// assert minId.isLeaf();
|
||||
// assert maxId.isLeaf();
|
||||
// assert minId.compareTo(maxId) <= 0;
|
||||
// assert minId.isValid() && maxId.isValid();
|
||||
initFromBeginEnd(minId, maxId.next());
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #initFromMinMax(S2CellId, S2CellId)}, except that the union covers the range of leaf
|
||||
* cells from "begin" (inclusive) to "end" (exclusive.) If {@code begin.equals(end)}, the result
|
||||
* is empty.
|
||||
*
|
||||
* <p>Requires that begin.isLeaf(), end.isLeaf(), and begin <= end.
|
||||
*/
|
||||
public void initFromBeginEnd(S2CellId begin, S2CellId end) {
|
||||
// assert (begin.isLeaf());
|
||||
// assert (end.isLeaf());
|
||||
// assert (begin.compareTo(end) <= 0);
|
||||
|
||||
// We repeatedly add the largest cell we can, in sorted order.
|
||||
cellIds.clear();
|
||||
for (S2CellId nextBegin = begin; nextBegin.compareTo(end) < 0; ) {
|
||||
// assert(nextBegin.isLeaf());
|
||||
|
||||
// Find the largest cell that starts at "next_begin" and ends before "end".
|
||||
S2CellId nextId = nextBegin;
|
||||
while (!nextId.isFace()
|
||||
&& nextId.parent().rangeMin().equals(nextBegin)
|
||||
&& nextId.parent().rangeMax().compareTo(end) < 0) {
|
||||
nextId = nextId.parent();
|
||||
}
|
||||
cellIds.add(nextId);
|
||||
nextBegin = nextId.rangeMax().next();
|
||||
}
|
||||
|
||||
// The output should already be sorted and normalized.
|
||||
// assert(!normalize());
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return cellIds.size();
|
||||
}
|
||||
|
||||
/** Convenience methods for accessing the individual cell ids. */
|
||||
public S2CellId cellId(int i) {
|
||||
return cellIds.get(i);
|
||||
}
|
||||
|
||||
/** Enable iteration over the union's cells. */
|
||||
@Override
|
||||
public Iterator<S2CellId> iterator() {
|
||||
return cellIds.iterator();
|
||||
}
|
||||
|
||||
/** Direct access to the underlying vector for iteration . */
|
||||
public ArrayList<S2CellId> cellIds() {
|
||||
return cellIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the cell union is valid, meaning that the S2CellIds are non-overlapping and
|
||||
* sorted in increasing order.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
for (int i = 1; i < cellIds.size(); i++) {
|
||||
if (cellIds.get(i - 1).rangeMax().compareTo(cellIds.get(i).rangeMin()) >= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the cell union is normalized, meaning that it {@link #isValid()} is true and
|
||||
* that no four cells have a common parent.
|
||||
*
|
||||
* <p>Certain operations such as {@link #contains(S2CellUnion)} may return a different result if
|
||||
* the cell union is not normalized.
|
||||
*/
|
||||
public boolean isNormalized() {
|
||||
for (int i = 1; i < cellIds.size(); i++) {
|
||||
if (cellIds.get(i - 1).rangeMax().compareTo(cellIds.get(i).rangeMin()) >= 0) {
|
||||
return false;
|
||||
}
|
||||
if (i >= 3
|
||||
&& areSiblings(
|
||||
cellIds.get(i - 3), cellIds.get(i - 2),
|
||||
cellIds.get(i - 1), cellIds.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given four cells have a common parent.
|
||||
*
|
||||
* <p>Requires the four cells are distinct.
|
||||
*/
|
||||
private static boolean areSiblings(S2CellId a, S2CellId b, S2CellId c, S2CellId d) {
|
||||
// A necessary (but not sufficient) condition is that the XOR of the four cells must be zero.
|
||||
// This is also very fast to test.
|
||||
if ((a.id() ^ b.id() ^ c.id()) != d.id()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now we do a slightly more expensive but exact test. First, compute a mask that blocks out
|
||||
// the two bits that encode the child position of "id" with respect to its parent, then check
|
||||
// that the other three children all agree with "mask".
|
||||
long mask = d.lowestOnBit() << 1;
|
||||
mask = ~(mask + (mask << 1));
|
||||
long idMasked = d.id() & mask;
|
||||
return !d.isFace()
|
||||
&& (a.id() & mask) == idMasked
|
||||
&& (b.id() & mask) == idMasked
|
||||
&& (c.id() & mask) == idMasked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces "output" with an expanded version of the cell union where any cells whose level is
|
||||
* less than "min_level" or where (level - min_level) is not a multiple of "level_mod" are
|
||||
* replaced by their children, until either both of these conditions are satisfied or the maximum
|
||||
* level is reached.
|
||||
*
|
||||
* <p>This method allows a covering generated by S2RegionCoverer using min_level() or level_mod()
|
||||
* constraints to be stored as a normalized cell union (which allows various geometric
|
||||
* computations to be done) and then converted back to the original list of cell ids that
|
||||
* satisfies the desired constraints.
|
||||
*/
|
||||
public void denormalize(int minLevel, int levelMod, ArrayList<S2CellId> output) {
|
||||
// assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL);
|
||||
// assert (levelMod >= 1 && levelMod <= 3);
|
||||
|
||||
output.clear();
|
||||
output.ensureCapacity(size());
|
||||
for (S2CellId id : this) {
|
||||
int level = id.level();
|
||||
int newLevel = Math.max(minLevel, level);
|
||||
if (levelMod > 1) {
|
||||
// Round up so that (new_level - min_level) is a multiple of level_mod.
|
||||
// (Note that S2CellId::kMaxLevel is a multiple of 1, 2, and 3.)
|
||||
newLevel += (S2CellId.MAX_LEVEL - (newLevel - minLevel)) % levelMod;
|
||||
newLevel = Math.min(S2CellId.MAX_LEVEL, newLevel);
|
||||
}
|
||||
if (newLevel == level) {
|
||||
output.add(id);
|
||||
} else {
|
||||
S2CellId end = id.childEnd(newLevel);
|
||||
for (id = id.childBegin(newLevel); !id.equals(end); id = id.next()) {
|
||||
output.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are more than "excess" elements of the cell_ids() vector that are allocated but
|
||||
* unused, reallocate the array to eliminate the excess space. This reduces memory usage when many
|
||||
* cell unions need to be held in memory at once.
|
||||
*/
|
||||
public void pack() {
|
||||
cellIds.trimToSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the cell union contains the given cell id. Containment is defined with respect
|
||||
* to regions, e.g. a cell contains its 4 children. This is a fast operation (logarithmic in the
|
||||
* size of the cell union).
|
||||
*
|
||||
* <p>CAVEAT: If you have constructed a valid but non-normalized S2CellUnion, note that groups of
|
||||
* 4 child cells are <em>not</em> considered to contain their parent cell. To get this behavior
|
||||
* you must construct a normalized cell union, or call {@link #normalize()} prior to this method.
|
||||
*/
|
||||
public boolean contains(S2CellId id) {
|
||||
// This is an exact test. Each cell occupies a linear span of the S2
|
||||
// space-filling curve, and the cell id is simply the position at the center
|
||||
// of this span. The cell union ids are sorted in increasing order along
|
||||
// the space-filling curve. So we simply find the pair of cell ids that
|
||||
// surround the given cell id (using binary search). There is containment
|
||||
// if and only if one of these two cell ids contains this cell.
|
||||
|
||||
int pos = Collections.binarySearch(cellIds, id);
|
||||
if (pos < 0) {
|
||||
pos = -pos - 1;
|
||||
}
|
||||
if (pos < cellIds.size() && cellIds.get(pos).rangeMin().lessOrEquals(id)) {
|
||||
return true;
|
||||
}
|
||||
return pos != 0 && cellIds.get(pos - 1).rangeMax().greaterOrEquals(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the cell union intersects the given cell id. This is a fast operation
|
||||
* (logarithmic in the size of the cell union).
|
||||
*/
|
||||
public boolean intersects(S2CellId id) {
|
||||
// This is an exact test; see the comments for Contains() above.
|
||||
int pos = Collections.binarySearch(cellIds, id);
|
||||
|
||||
if (pos < 0) {
|
||||
pos = -pos - 1;
|
||||
}
|
||||
|
||||
if (pos < cellIds.size() && cellIds.get(pos).rangeMin().lessOrEquals(id.rangeMax())) {
|
||||
return true;
|
||||
}
|
||||
return pos != 0 && cellIds.get(pos - 1).rangeMax().greaterOrEquals(id.rangeMin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cell union contains {@code that}.
|
||||
*
|
||||
* <p>CAVEAT: If you have constructed a valid but non-normalized S2CellUnion, note that groups of
|
||||
* 4 child cells are <em>not</em> considered to contain their parent cell. To get this behavior
|
||||
* you must construct a normalized cell union, or call {@link #normalize()} prior to this method.
|
||||
*/
|
||||
public boolean contains(S2CellUnion that) {
|
||||
S2CellUnion result = new S2CellUnion();
|
||||
result.getIntersection(this, that);
|
||||
return result.cellIds.equals(that.cellIds);
|
||||
}
|
||||
|
||||
/** This is a fast operation (logarithmic in the size of the cell union). */
|
||||
@Override
|
||||
public boolean contains(S2Cell cell) {
|
||||
return contains(cell.id());
|
||||
}
|
||||
|
||||
/** Return true if this cell union intersects {@code union}. */
|
||||
public boolean intersects(S2CellUnion union) {
|
||||
S2CellUnion result = new S2CellUnion();
|
||||
result.getIntersection(this, union);
|
||||
return result.size() > 0;
|
||||
}
|
||||
|
||||
/** Sets this cell union to the union of {@code x} and {@code y}. */
|
||||
public void getUnion(S2CellUnion x, S2CellUnion y) {
|
||||
// assert (x != this && y != this);
|
||||
cellIds.clear();
|
||||
cellIds.ensureCapacity(x.size() + y.size());
|
||||
cellIds.addAll(x.cellIds);
|
||||
cellIds.addAll(y.cellIds);
|
||||
normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specialized version of GetIntersection() that gets the intersection of a cell union with the
|
||||
* given cell id. This can be useful for "splitting" a cell union into chunks.
|
||||
*
|
||||
* <p><b>Note:</b> {@code x} and {@code y} must be normalized.
|
||||
*/
|
||||
public void getIntersection(S2CellUnion x, S2CellId id) {
|
||||
// assert (x != this);
|
||||
cellIds.clear();
|
||||
if (x.contains(id)) {
|
||||
cellIds.add(id);
|
||||
} else {
|
||||
int pos = Collections.binarySearch(x.cellIds, id.rangeMin());
|
||||
|
||||
if (pos < 0) {
|
||||
pos = -pos - 1;
|
||||
}
|
||||
|
||||
S2CellId idmax = id.rangeMax();
|
||||
int size = x.cellIds.size();
|
||||
while (pos < size && x.cellIds.get(pos).lessOrEquals(idmax)) {
|
||||
cellIds.add(x.cellIds.get(pos++));
|
||||
}
|
||||
}
|
||||
// assert isNormalized() || !x.isNormalized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this cell union to the intersection of the two given cell unions. Requires: x !=
|
||||
* this and y != this.
|
||||
*
|
||||
* <p><b>Note:</b> {@code x} and {@code y} must be normalized.
|
||||
*/
|
||||
public void getIntersection(S2CellUnion x, S2CellUnion y) {
|
||||
getIntersection(x.cellIds, y.cellIds, cellIds);
|
||||
// The output is normalized as long as at least one input is normalized.
|
||||
// assert isNormalized() || (!x.isNormalized() && !y.isNormalized());
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@code #getIntersection(S2CellUnion, S2CellUnion)}, but works directly with lists of
|
||||
* S2CellIds, and this method has slightly more relaxed normalization requirements: the input
|
||||
* vectors may contain groups of 4 child cells that all have the same parent. (In a normalized
|
||||
* S2CellUnion, such groups are always replaced by the parent cell.)
|
||||
*
|
||||
* <p><b>Note:</b> {@code x} and {@code y} must be sorted.
|
||||
*/
|
||||
public static void getIntersection(List<S2CellId> x, List<S2CellId> y, List<S2CellId> results) {
|
||||
// assert (x != results && y != results);
|
||||
|
||||
// This is a fairly efficient calculation that uses binary search to skip
|
||||
// over sections of both input vectors. It takes constant time if all the
|
||||
// cells of "x" come before or after all the cells of "y" in S2CellId order.
|
||||
results.clear();
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
while (i < x.size() && j < y.size()) {
|
||||
S2CellId xCell = x.get(i);
|
||||
S2CellId xMin = xCell.rangeMin();
|
||||
S2CellId yCell = y.get(j);
|
||||
S2CellId yMin = yCell.rangeMin();
|
||||
if (xMin.greaterThan(yMin)) {
|
||||
// Either j->contains(xCell) or the two cells are disjoint.
|
||||
if (xCell.lessOrEquals(yCell.rangeMax())) {
|
||||
results.add(xCell);
|
||||
i++;
|
||||
} else {
|
||||
// Advance "j" to the first cell possibly contained by xCell.
|
||||
j = indexedBinarySearch(y, xMin, j + 1);
|
||||
// The previous cell *(j-1) may now contain xCell.
|
||||
if (xCell.lessOrEquals(y.get(j - 1).rangeMax())) {
|
||||
--j;
|
||||
}
|
||||
}
|
||||
} else if (yMin.greaterThan(xMin)) {
|
||||
// Identical to the code above with "i" and "j" reversed.
|
||||
if (yCell.lessOrEquals(xCell.rangeMax())) {
|
||||
results.add(yCell);
|
||||
j++;
|
||||
} else {
|
||||
i = indexedBinarySearch(x, yMin, i + 1);
|
||||
if (yCell.lessOrEquals(x.get(i - 1).rangeMax())) {
|
||||
--i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// "i" and "j" have the same rangeMin(), so one contains the other.
|
||||
if (xCell.lessThan(yCell)) {
|
||||
results.add(xCell);
|
||||
i++;
|
||||
} else {
|
||||
results.add(yCell);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Initiaizes this cell union to the difference of the two given cell unions. */
|
||||
public void getDifference(S2CellUnion x, S2CellUnion y) {
|
||||
// TODO(user): this is approximately O(N*log(N)), but could probably use similar techniques as
|
||||
// getIntersection() to be more efficient.
|
||||
cellIds.clear();
|
||||
for (S2CellId id : x) {
|
||||
getDifferenceInternal(id, y);
|
||||
}
|
||||
// The output is normalized as long as the first argument is normalized.
|
||||
// assert isNormalized() || !x.isNormalized();
|
||||
}
|
||||
|
||||
private void getDifferenceInternal(S2CellId cell, S2CellUnion y) {
|
||||
// Add the difference between cell and y to cellIds. If they intersect but the difference is
|
||||
// non-empty, divide and conquer.
|
||||
if (!y.intersects(cell)) {
|
||||
cellIds.add(cell);
|
||||
} else if (!y.contains(cell)) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
getDifferenceInternal(cell.child(i), y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just as normal binary search, except that it allows specifying the starting value for the lower
|
||||
* bound.
|
||||
*
|
||||
* @return The position of the searched element in the list (if found), or the position where the
|
||||
* element could be inserted without violating the order.
|
||||
*/
|
||||
private static int indexedBinarySearch(List<S2CellId> l, S2CellId key, int low) {
|
||||
int high = l.size() - 1;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = (low + high) >> 1;
|
||||
S2CellId midVal = l.get(mid);
|
||||
int cmp = midVal.compareTo(key);
|
||||
|
||||
if (cmp < 0) {
|
||||
low = mid + 1;
|
||||
} else if (cmp > 0) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
return mid; // key found
|
||||
}
|
||||
}
|
||||
return low; // key not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the cell union such that it contains all cells of the given level that are adjacent to
|
||||
* any cell of the original union. Two cells are defined as adjacent if their boundaries have any
|
||||
* points in common, i.e. most cells have 8 adjacent cells (not counting the cell itself).
|
||||
*
|
||||
* <p>Note that the size of the output is exponential in "level". For example, if level == 20 and
|
||||
* the input has a cell at level 10, there will be on the order of 4000 adjacent cells in the
|
||||
* output. For most applications the Expand(min_fraction, min_distance) method below is easier to
|
||||
* use.
|
||||
*/
|
||||
public void expand(int level) {
|
||||
ArrayList<S2CellId> output = new ArrayList<S2CellId>();
|
||||
long levelLsb = S2CellId.lowestOnBitForLevel(level);
|
||||
for (int i = size(); --i >= 0; ) {
|
||||
S2CellId id = cellId(i);
|
||||
if (id.lowestOnBit() < levelLsb) {
|
||||
id = id.parent(level);
|
||||
// Optimization: skip over any cells contained by this one. This is
|
||||
// especially important when very small regions are being expanded.
|
||||
while (i > 0 && id.contains(cellId(i - 1))) {
|
||||
--i;
|
||||
}
|
||||
}
|
||||
output.add(id);
|
||||
id.getAllNeighbors(level, output);
|
||||
}
|
||||
initSwap(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the cell union such that it contains all points whose distance to the cell union is at
|
||||
* most minRadius, but do not use cells that are more than maxLevelDiff levels higher than the
|
||||
* largest cell in the input. The second parameter controls the tradeoff between accuracy and
|
||||
* output size when a large region is being expanded by a small amount (e.g. expanding Canada by
|
||||
* 1km).
|
||||
*
|
||||
* <p>For example, if maxLevelDiff == 4, the region will always be expanded by approximately 1/16
|
||||
* the width of its largest cell. Note that in the worst case, the number of cells in the output
|
||||
* can be up to 4 * (1 + 2 ** maxLevelDiff) times larger than the number of cells in the input.
|
||||
*/
|
||||
public void expand(S1Angle minRadius, int maxLevelDiff) {
|
||||
int minLevel = S2CellId.MAX_LEVEL;
|
||||
for (S2CellId id : this) {
|
||||
minLevel = Math.min(minLevel, id.level());
|
||||
}
|
||||
// Find the maximum level such that all cells are at least "min_radius"
|
||||
// wide.
|
||||
int radiusLevel = PROJ.minWidth.getMaxLevel(minRadius.radians());
|
||||
if (radiusLevel == 0 && minRadius.radians() > PROJ.minWidth.getValue(0)) {
|
||||
// The requested expansion is greater than the width of a face cell.
|
||||
// The easiest way to handle this is to expand twice.
|
||||
expand(0);
|
||||
}
|
||||
expand(Math.min(minLevel + maxLevelDiff, radiusLevel));
|
||||
}
|
||||
|
||||
// NOTE: This should be marked as @Override, but clone() isn't present in GWT's version of
|
||||
// Object, so we can't mark it as such.
|
||||
@SuppressWarnings("MissingOverride")
|
||||
public S2Region clone() {
|
||||
S2CellUnion copy = new S2CellUnion();
|
||||
copy.initRawCellIds(Lists.newArrayList(cellIds));
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S2Cap getCapBound() {
|
||||
// Compute the approximate centroid of the region. This won't produce the
|
||||
// bounding cap of minimal area, but it should be close enough.
|
||||
if (cellIds.isEmpty()) {
|
||||
return S2Cap.empty();
|
||||
}
|
||||
S2Point centroid = S2Point.ORIGIN;
|
||||
for (S2CellId id : this) {
|
||||
double area = S2Cell.averageArea(id.level());
|
||||
centroid = S2Point.add(centroid, S2Point.mul(id.toPoint(), area));
|
||||
}
|
||||
if (centroid.equalsPoint(S2Point.ORIGIN)) {
|
||||
centroid = S2Point.X_POS;
|
||||
} else {
|
||||
centroid = S2Point.normalize(centroid);
|
||||
}
|
||||
|
||||
// Use the centroid as the cap axis, and expand the cap angle so that it
|
||||
// contains the bounding caps of all the individual cells. Note that it is
|
||||
// *not* sufficient to just bound all the cell vertices because the bounding
|
||||
// cap may be concave (i.e. cover more than one hemisphere).
|
||||
S2Cap cap = S2Cap.fromAxisChord(centroid, S1ChordAngle.ZERO);
|
||||
for (S2CellId id : this) {
|
||||
cap = cap.addCap(new S2Cell(id).getCapBound());
|
||||
}
|
||||
return cap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public S2LatLngRect getRectBound() {
|
||||
S2LatLngRect.Builder builder = S2LatLngRect.Builder.empty();
|
||||
for (S2CellId id : this) {
|
||||
builder.union(new S2Cell(id).getRectBound());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** This is a fast operation (logarithmic in the size of the cell union). */
|
||||
@Override
|
||||
public boolean mayIntersect(S2Cell cell) {
|
||||
return intersects(cell.id());
|
||||
}
|
||||
|
||||
/**
|
||||
* The point 'p' does not need to be normalized. This is a fast operation (logarithmic in the size
|
||||
* of the cell union).
|
||||
*/
|
||||
@Override
|
||||
public boolean contains(S2Point p) {
|
||||
return contains(S2CellId.fromPoint(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of leaf cells covered by the union. This will be no more than 6*2^60 for the whole
|
||||
* sphere.
|
||||
*
|
||||
* @return the number of leaf cells covered by the union
|
||||
*/
|
||||
public long leafCellsCovered() {
|
||||
long numLeaves = 0;
|
||||
for (S2CellId cellId : cellIds) {
|
||||
int invertedLevel = S2CellId.MAX_LEVEL - cellId.level();
|
||||
numLeaves += (1L << (invertedLevel << 1));
|
||||
}
|
||||
return numLeaves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate this cell union's area by summing the average area of each contained cell's average
|
||||
* area, using {@link S2Cell#averageArea()}. This is equivalent to the number of leaves covered,
|
||||
* multiplied by the average area of a leaf.
|
||||
*
|
||||
* <p>Note that {@link S2Cell#averageArea()} does not take into account distortion of cell, and
|
||||
* thus may be off by up to a factor of 1.7. NOTE: Since this is proportional to
|
||||
* LeafCellsCovered(), it is always better to use the other function if all you care about is the
|
||||
* relative average area between objects.
|
||||
*
|
||||
* @return the sum of the average area of each contained cell's average area
|
||||
*/
|
||||
public double averageBasedArea() {
|
||||
return S2Cell.averageArea(S2CellId.MAX_LEVEL) * leafCellsCovered();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates this cell union's area by summing the approximate area for each contained cell,
|
||||
* using {@link S2Cell#approxArea()}.
|
||||
*
|
||||
* @return approximate area of the cell union
|
||||
*/
|
||||
public double approxArea() {
|
||||
double area = 0;
|
||||
for (S2CellId cellId : cellIds) {
|
||||
area += new S2Cell(cellId).approxArea();
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates this cell union's area by summing the exact area for each contained cell, using the
|
||||
* {@link S2Cell#exactArea()}.
|
||||
*
|
||||
* @return the exact area of the cell union
|
||||
*/
|
||||
public double exactArea() {
|
||||
double area = 0;
|
||||
for (S2CellId cellId : cellIds) {
|
||||
area += new S2Cell(cellId).exactArea();
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
/** Return true if two cell unions are identical. */
|
||||
@Override
|
||||
public boolean equals(Object that) {
|
||||
if (!(that instanceof S2CellUnion)) {
|
||||
return false;
|
||||
}
|
||||
S2CellUnion union = (S2CellUnion) that;
|
||||
return this.cellIds.equals(union.cellIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int value = 17;
|
||||
for (S2CellId id : this) {
|
||||
value = 37 * value + id.hashCode();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the cell union by discarding cells that are contained by other cells, replacing
|
||||
* groups of 4 child cells by their parent cell whenever possible, and sorting all the cell ids in
|
||||
* increasing order. Returns true if the number of cells was reduced.
|
||||
*
|
||||
* <p>This method *must* be called before doing any calculations on the cell union, such as
|
||||
* Intersects() or Contains().
|
||||
*
|
||||
* @return true if the normalize operation had any effect on the cell union, false if the union
|
||||
* was already normalized
|
||||
*/
|
||||
public boolean normalize() {
|
||||
return normalize(cellIds);
|
||||
}
|
||||
|
||||
/** Like {@link #normalize()}, but works directly with a vector of S2CellIds. */
|
||||
public static boolean normalize(List<S2CellId> ids) {
|
||||
// Optimize the representation by looking for cases where all subcells of a parent cell are
|
||||
// present.
|
||||
Collections.sort(ids);
|
||||
int out = 0;
|
||||
for (int i = 0; i < ids.size(); i++) {
|
||||
S2CellId id = ids.get(i);
|
||||
|
||||
// Check whether this cell is contained by the previous cell.
|
||||
if (out > 0 && ids.get(out - 1).contains(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Discard any previous cells contained by this cell.
|
||||
while (out > 0 && id.contains(ids.get(out - 1))) {
|
||||
out--;
|
||||
}
|
||||
|
||||
// Check whether the last 3 elements of "output" plus "id" can be collapsed into a single
|
||||
// parent cell.
|
||||
while (out >= 3) {
|
||||
// A necessary (but not sufficient) condition is that the XOR of the
|
||||
// four cells must be zero. This is also very fast to test.
|
||||
if ((ids.get(out - 3).id() ^ ids.get(out - 2).id() ^ ids.get(out - 1).id()) != id.id()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Now we do a slightly more expensive but exact test. First, compute a
|
||||
// mask that blocks out the two bits that encode the child position of
|
||||
// "id" with respect to its parent, then check that the other three
|
||||
// children all agree with "mask.
|
||||
long mask = id.lowestOnBit() << 1;
|
||||
mask = ~(mask + (mask << 1));
|
||||
long idMasked = (id.id() & mask);
|
||||
if ((ids.get(out - 3).id() & mask) != idMasked
|
||||
|| (ids.get(out - 2).id() & mask) != idMasked
|
||||
|| (ids.get(out - 1).id() & mask) != idMasked
|
||||
|| id.isFace()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Replace four children by their parent cell.
|
||||
id = id.parent();
|
||||
out -= 3;
|
||||
}
|
||||
|
||||
ids.set(out++, id);
|
||||
}
|
||||
|
||||
int size = ids.size();
|
||||
boolean trimmed = out < size;
|
||||
while (out < size) {
|
||||
size--;
|
||||
ids.remove(size);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a simple lossless encoding of this cell union to the given output stream. This encoding
|
||||
* uses 1 byte for a version number, and N+1 64-bit longs where the first is the number of longs
|
||||
* that follow.
|
||||
*
|
||||
* @throws IOException there is a problem writing to the underlying stream
|
||||
*/
|
||||
public void encode(OutputStream output) throws IOException {
|
||||
encode(new LittleEndianOutput(output));
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #encode(OutputStream)}, but avoids creating a little endian output helper.
|
||||
*
|
||||
* <p>Use this method if a number of S2 objects will be encoded to the same underlying stream.
|
||||
*/
|
||||
public void encode(LittleEndianOutput output) throws IOException {
|
||||
output.writeByte(LOSSLESS_ENCODING_VERSION);
|
||||
output.writeLong(cellIds.size());
|
||||
for (S2CellId cellId : this) {
|
||||
output.writeLong(cellId.id());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an S2CellUnion encoded with Encode(). Returns true on success.
|
||||
*
|
||||
* @throws IOException there is a problem reading from the underlying stream, the version number
|
||||
* doesn't match, or the number of elements to read is not between 0 and 2^31-1.
|
||||
*/
|
||||
public static S2CellUnion decode(InputStream input) throws IOException {
|
||||
return decode(new LittleEndianInput(input));
|
||||
}
|
||||
|
||||
/**
|
||||
* As {@link #decode(InputStream)}, but avoids creating a little endian input helper.
|
||||
*
|
||||
* <p>Use this method if a number of S2 objects will be decoded from the same underlying stream.
|
||||
*/
|
||||
public static S2CellUnion decode(LittleEndianInput input) throws IOException {
|
||||
// Should contain at least version and vector length.
|
||||
byte version = input.readByte();
|
||||
if (version != LOSSLESS_ENCODING_VERSION) {
|
||||
throw new IOException("Unrecognized version number " + version);
|
||||
}
|
||||
long numCells = input.readLong();
|
||||
if (numCells < 0 || numCells > Integer.MAX_VALUE) {
|
||||
throw new IOException("Unsupported number of cells encountered: " + numCells);
|
||||
}
|
||||
S2CellUnion result = new S2CellUnion();
|
||||
for (int i = 0; i < numCells; i++) {
|
||||
result.cellIds().add(new S2CellId(input.readLong()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user