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:
yangyakun
2022-06-22 09:19:48 +08:00
163 changed files with 38802 additions and 900 deletions

View File

@@ -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);
}

View File

@@ -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
// 无状态

View File

@@ -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

View File

@@ -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()) {

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}
/**
* 绘制
*

View File

@@ -15,4 +15,9 @@ public interface IBusPassengerMapDirectionView {
* 清除路径线
*/
void clearPolyline();
/**
* 设置路径中起终点marker
*/
void setLineMarker();
}

View File

@@ -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();
}

View File

@@ -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("已收车");

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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()) {

View File

@@ -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;

View File

@@ -15,4 +15,9 @@ public interface ITaxiPassengerMapDirectionView {
* 清除路径线
*/
void clearPolyline();
/**
* 设置路径中起终点marker
*/
void setLineMarker();
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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}")
}

View File

@@ -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(

View File

@@ -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());
}
}

View File

@@ -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 -> {
}
}

View File

@@ -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);
}

View File

@@ -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() {
}
});
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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)
}
/**

View File

@@ -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()
}
}

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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")

View File

@@ -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 {
// 如果传入的不是关闭显示,则设置倒计时,定时关闭红框警示

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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()

View File

@@ -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() +
'}';
}
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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;

View File

@@ -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 +
'}';
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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) {

View File

@@ -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
}
})
}

View File

@@ -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();

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}

View File

@@ -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 +
'}';
}
}

View File

@@ -117,4 +117,11 @@ object FunctionBuildConfig {
@JvmField
var skinMode = 0
/**
* 是否进行异常上报提示
*/
@Volatile
@JvmField
var isReportWarning = true
}

View File

@@ -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"

View File

@@ -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,

View File

@@ -17,6 +17,11 @@ interface IMoGoAutopilotIdentifyListener {
*/
fun onAutopilotIdentifyDataUpdate(trafficData: List<MessagePad.TrackedObject>?) {}
/**
* planning识别感知预警物体
*/
fun onAutopilotIdentifyPlanningObj(planningObjects: List<MessagePad.PlanningObject>?){}
/**
* 报警信息
*

View File

@@ -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();
}

View File

@@ -205,6 +205,11 @@ interface IMoGoWaringProvider : IMoGoHmiViewProxy {
*/
fun showModifyBindingcarDialog()
/**
* 展示升级app弹框
*/
fun showUpgradeDialog(name: String, url: String)
/**
* 呈现工控机升级确认框
*/

View File

@@ -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)
}
}
/**
* 报警信息 回调
*/

View File

@@ -269,6 +269,10 @@ object CallerHmiManager : CallerBase() {
waringProviderApi?.showModifyBindingcarDialog()
}
fun showUpgradeDialog(name: String, url: String) {
waringProviderApi?.showUpgradeDialog(name, url)
}
/**
* 呈现工控机升级确认框
*/

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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() + "]";
}
}

View File

@@ -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 + ")";
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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() {}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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